@indigoai-us/hq-cloud 6.16.58 → 6.16.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -138,7 +138,7 @@ import { readJournal, writeJournal, getEntry, listJournals, } from "../journal.j
138
138
  import { SqsPushReceiver } from "../sync/push-receiver.js";
139
139
  import { buildTargetedPullArgv } from "../bin/sync-runner-watch-routes.js";
140
140
  import { ReconcileCursorStore } from "../sync/reconcile-cursor.js";
141
- import { setObjectIOFactory } from "../object-io.js";
141
+ import { RateLimitedError, setObjectIOFactory } from "../object-io.js";
142
142
  /**
143
143
  * Read a journal the way production does.
144
144
  *
@@ -4117,6 +4117,129 @@ describe("sync", () => {
4117
4117
  const journal = journalAt(journalPath);
4118
4118
  expect(journal.files["docs/out-of-scope.md"]).toBeDefined();
4119
4119
  });
4120
+ it("defers a tombstone as a diagnostic error when HEAD throws RateLimitedError", async () => {
4121
+ // Production (2026-09-18): vault 100/hr throttle during HEAD verify
4122
+ // was emitted as a hard type:"error", so the company landed in
4123
+ // errors[] and hq-sync-runner exited 2 even though the tombstone
4124
+ // was already dropped from this pass. Rate-limit deferrals reuse
4125
+ // the diagnostic marker; the file stays, the journal stays, and
4126
+ // no hard error is emitted.
4127
+ const companyRoot = path.join(tmpDir, "companies", "acme");
4128
+ const skillDir = path.join(companyRoot, "skills", "wbr-finalize");
4129
+ fs.mkdirSync(skillDir, { recursive: true });
4130
+ const skillPath = path.join(skillDir, "SKILL.md");
4131
+ const body = "skill body";
4132
+ fs.writeFileSync(skillPath, body);
4133
+ const crypto = await import("node:crypto");
4134
+ const baselineHash = crypto.createHash("sha256").update(body).digest("hex");
4135
+ fs.writeFileSync(journalPath, JSON.stringify({
4136
+ version: "1",
4137
+ lastSync: new Date(Date.now() - 60_000).toISOString(),
4138
+ files: {
4139
+ "skills/wbr-finalize/SKILL.md": {
4140
+ hash: baselineHash,
4141
+ size: body.length,
4142
+ syncedAt: new Date(Date.now() - 60_000).toISOString(),
4143
+ direction: "down",
4144
+ remoteEtag: "e-skill",
4145
+ },
4146
+ },
4147
+ }));
4148
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([]);
4149
+ // Drop sibling Once/implementation leftovers so verify HEAD is this throw.
4150
+ vi.mocked(s3Module.headRemoteFile).mockReset();
4151
+ let headVerified = false;
4152
+ vi.mocked(s3Module.headRemoteFile).mockImplementationOnce(async (_ctx, key) => {
4153
+ expect(key).toBe("skills/wbr-finalize/SKILL.md");
4154
+ headVerified = true;
4155
+ throw new RateLimitedError("skills/wbr-finalize/SKILL.md", "get");
4156
+ });
4157
+ try {
4158
+ const events = [];
4159
+ const result = await sync({
4160
+ company: "acme",
4161
+ vaultConfig: mockConfig,
4162
+ hqRoot: tmpDir,
4163
+ onEvent: (e) => events.push(e),
4164
+ });
4165
+ expect(headVerified).toBe(true);
4166
+ expect(s3Module.headRemoteFile).toHaveBeenCalledTimes(1);
4167
+ expect(s3Module.headRemoteFile).toHaveBeenCalledWith(expect.anything(), "skills/wbr-finalize/SKILL.md");
4168
+ expect(fs.existsSync(skillPath)).toBe(true);
4169
+ expect(result.filesTombstoned).toBe(0);
4170
+ expect(journalAt(journalPath).files["skills/wbr-finalize/SKILL.md"]).toBeDefined();
4171
+ const errorEvents = events.filter((e) => e.type === "error");
4172
+ expect(errorEvents).toHaveLength(1);
4173
+ expect(errorEvents[0]).toMatchObject({
4174
+ type: "error",
4175
+ diagnostic: true,
4176
+ path: "skills/wbr-finalize/SKILL.md",
4177
+ message: expect.stringMatching(/tombstone HEAD verify failed \(deferring\):.*RateLimited/),
4178
+ });
4179
+ expect(events.some((e) => e.type === "error" && e.diagnostic !== true)).toBe(false);
4180
+ }
4181
+ finally {
4182
+ vi.mocked(s3Module.headRemoteFile).mockReset();
4183
+ vi.mocked(s3Module.headRemoteFile).mockResolvedValue(null);
4184
+ }
4185
+ });
4186
+ it("still emits a hard error when tombstone HEAD throws a generic Error", async () => {
4187
+ const companyRoot = path.join(tmpDir, "companies", "acme");
4188
+ fs.mkdirSync(path.join(companyRoot, "docs"), { recursive: true });
4189
+ const localPath = path.join(companyRoot, "docs", "peer-deleted.md");
4190
+ const body = "still here";
4191
+ fs.writeFileSync(localPath, body);
4192
+ const crypto = await import("node:crypto");
4193
+ const baselineHash = crypto.createHash("sha256").update(body).digest("hex");
4194
+ fs.writeFileSync(journalPath, JSON.stringify({
4195
+ version: "1",
4196
+ lastSync: new Date(Date.now() - 60_000).toISOString(),
4197
+ files: {
4198
+ "docs/peer-deleted.md": {
4199
+ hash: baselineHash,
4200
+ size: body.length,
4201
+ syncedAt: new Date(Date.now() - 60_000).toISOString(),
4202
+ direction: "down",
4203
+ remoteEtag: "e-del",
4204
+ },
4205
+ },
4206
+ }));
4207
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([]);
4208
+ vi.mocked(s3Module.headRemoteFile).mockReset();
4209
+ let headVerified = false;
4210
+ vi.mocked(s3Module.headRemoteFile).mockImplementationOnce(async (_ctx, key) => {
4211
+ expect(key).toBe("docs/peer-deleted.md");
4212
+ headVerified = true;
4213
+ throw new Error("S3 boom");
4214
+ });
4215
+ try {
4216
+ const events = [];
4217
+ const result = await sync({
4218
+ company: "acme",
4219
+ vaultConfig: mockConfig,
4220
+ hqRoot: tmpDir,
4221
+ onEvent: (e) => events.push(e),
4222
+ });
4223
+ expect(headVerified).toBe(true);
4224
+ expect(s3Module.headRemoteFile).toHaveBeenCalledTimes(1);
4225
+ expect(s3Module.headRemoteFile).toHaveBeenCalledWith(expect.anything(), "docs/peer-deleted.md");
4226
+ expect(fs.existsSync(localPath)).toBe(true);
4227
+ expect(result.filesTombstoned).toBe(0);
4228
+ expect(journalAt(journalPath).files["docs/peer-deleted.md"]).toBeDefined();
4229
+ const errorEvents = events.filter((e) => e.type === "error");
4230
+ expect(errorEvents).toHaveLength(1);
4231
+ expect(errorEvents[0]).toMatchObject({
4232
+ type: "error",
4233
+ path: "docs/peer-deleted.md",
4234
+ message: expect.stringContaining("tombstone HEAD verify failed (deferring):"),
4235
+ });
4236
+ expect(errorEvents[0]).not.toHaveProperty("diagnostic");
4237
+ }
4238
+ finally {
4239
+ vi.mocked(s3Module.headRemoteFile).mockReset();
4240
+ vi.mocked(s3Module.headRemoteFile).mockResolvedValue(null);
4241
+ }
4242
+ });
4120
4243
  it("retains the journal entry when tombstone unlink fails with EACCES (Codex P1 — Bug #9 follow-up)", async () => {
4121
4244
  // Codex review on PR #24 caught this: pre-fix the tombstone branch
4122
4245
  // dropped the journal entry unconditionally even when the local