@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.
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +31 -13
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +65 -0
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +4 -2
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +29 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/sync.d.ts +8 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +27 -5
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +124 -1
- package/dist/cli/sync.test.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/sync.test.js
CHANGED
|
@@ -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
|