@indigoai-us/hq-cloud 6.15.53 → 6.15.55
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-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +54 -7
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.test.js +315 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/sync/state-store.js +1 -1
- package/dist/sync/state-store.js.map +1 -1
- package/dist/sync/state-store.test.js +69 -0
- package/dist/sync/state-store.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -5078,6 +5078,27 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
5078
5078
|
function startDrainLoop(runPass) {
|
|
5079
5079
|
return startDrainLoopWithArgv(runPass, watchArgv);
|
|
5080
5080
|
}
|
|
5081
|
+
function startBatchDrainLoop(runPass) {
|
|
5082
|
+
const watcher = makeBatchWatcherStub();
|
|
5083
|
+
const sleep = makeSteppableSleep();
|
|
5084
|
+
let triggerShutdown = () => { };
|
|
5085
|
+
const loop = runRunnerWithLoop(watchArgv, {
|
|
5086
|
+
runPass,
|
|
5087
|
+
clock: new FakeClock(),
|
|
5088
|
+
createWatcher: () => watcher,
|
|
5089
|
+
sleep: sleep.sleep,
|
|
5090
|
+
onShutdownSignal: (handler) => {
|
|
5091
|
+
triggerShutdown = handler;
|
|
5092
|
+
return () => { };
|
|
5093
|
+
},
|
|
5094
|
+
});
|
|
5095
|
+
return {
|
|
5096
|
+
loop,
|
|
5097
|
+
watcher,
|
|
5098
|
+
tick: sleep.tick,
|
|
5099
|
+
shutdown: () => triggerShutdown(),
|
|
5100
|
+
};
|
|
5101
|
+
}
|
|
5081
5102
|
function startDrainLoopWithArgv(runPass, argv) {
|
|
5082
5103
|
const watcher = makeWatcherStub();
|
|
5083
5104
|
const sleep = makeSteppableSleep();
|
|
@@ -5099,6 +5120,293 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
5099
5120
|
shutdown: () => triggerShutdown(),
|
|
5100
5121
|
};
|
|
5101
5122
|
}
|
|
5123
|
+
function captureFullReconcileDecisions() {
|
|
5124
|
+
const records = [];
|
|
5125
|
+
const spy = vi
|
|
5126
|
+
.spyOn(process.stderr, "write")
|
|
5127
|
+
.mockImplementation((chunk) => {
|
|
5128
|
+
if (typeof chunk === "string" && chunk.startsWith("{")) {
|
|
5129
|
+
const parsed = JSON.parse(String(chunk));
|
|
5130
|
+
if (typeof parsed === "object" &&
|
|
5131
|
+
parsed !== null &&
|
|
5132
|
+
parsed.type === "full-reconcile-decision") {
|
|
5133
|
+
records.push(parsed);
|
|
5134
|
+
}
|
|
5135
|
+
}
|
|
5136
|
+
return true;
|
|
5137
|
+
});
|
|
5138
|
+
return {
|
|
5139
|
+
records,
|
|
5140
|
+
clear: () => records.splice(0),
|
|
5141
|
+
restore: () => spy.mockRestore(),
|
|
5142
|
+
};
|
|
5143
|
+
}
|
|
5144
|
+
const allTriggerFlags = (matched = []) => ({
|
|
5145
|
+
"event-push-surfaces-inactive": matched.includes("event-push-surfaces-inactive"),
|
|
5146
|
+
"first-tick": matched.includes("first-tick"),
|
|
5147
|
+
"bare-wake": matched.includes("bare-wake"),
|
|
5148
|
+
"batch-over-limit": matched.includes("batch-over-limit"),
|
|
5149
|
+
"overflow-routes-unknown": matched.includes("overflow-routes-unknown"),
|
|
5150
|
+
"scheduled-interval": matched.includes("scheduled-interval"),
|
|
5151
|
+
});
|
|
5152
|
+
function expectFullReconcileDecision(record, mode, reasons) {
|
|
5153
|
+
expect(record).toMatchObject({
|
|
5154
|
+
type: "full-reconcile-decision",
|
|
5155
|
+
mode,
|
|
5156
|
+
reasons,
|
|
5157
|
+
triggers: allTriggerFlags(reasons),
|
|
5158
|
+
});
|
|
5159
|
+
}
|
|
5160
|
+
it("logs first-tick as the sole full-reconcile trigger when event push is off", async () => {
|
|
5161
|
+
const capture = captureFullReconcileDecisions();
|
|
5162
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5163
|
+
const sleep = makeSteppableSleep();
|
|
5164
|
+
let shutdown = () => { };
|
|
5165
|
+
const loop = runRunnerWithLoop([
|
|
5166
|
+
"--companies", "--watch", "--direction", "both", "--hq-root", "/tmp/hq",
|
|
5167
|
+
"--poll-remote-ms", "60000",
|
|
5168
|
+
], {
|
|
5169
|
+
runPass,
|
|
5170
|
+
sleep: sleep.sleep,
|
|
5171
|
+
onShutdownSignal: (handler) => {
|
|
5172
|
+
shutdown = handler;
|
|
5173
|
+
return () => { };
|
|
5174
|
+
},
|
|
5175
|
+
});
|
|
5176
|
+
await flushLoopMicrotasks();
|
|
5177
|
+
expectFullReconcileDecision(capture.records[0], "full", ["first-tick"]);
|
|
5178
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
5179
|
+
shutdown();
|
|
5180
|
+
sleep.tick();
|
|
5181
|
+
await loop;
|
|
5182
|
+
capture.restore();
|
|
5183
|
+
});
|
|
5184
|
+
it("logs event-push-surfaces-inactive without silently attributing the first tick", async () => {
|
|
5185
|
+
const saved = process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
5186
|
+
process.env.HQ_SYNC_FULL_RECONCILE_MS = "10800000";
|
|
5187
|
+
const capture = captureFullReconcileDecisions();
|
|
5188
|
+
try {
|
|
5189
|
+
const sleep = makeSteppableSleep();
|
|
5190
|
+
let shutdown = () => { };
|
|
5191
|
+
const runPass = vi.fn().mockResolvedValue(PARTIAL_SYNC_EXIT);
|
|
5192
|
+
const loop = runRunnerWithLoop(watchArgv, {
|
|
5193
|
+
runPass,
|
|
5194
|
+
monotonicNow: () => 0,
|
|
5195
|
+
sleep: sleep.sleep,
|
|
5196
|
+
onShutdownSignal: (handler) => {
|
|
5197
|
+
shutdown = handler;
|
|
5198
|
+
return () => { };
|
|
5199
|
+
},
|
|
5200
|
+
});
|
|
5201
|
+
await flushLoopMicrotasks();
|
|
5202
|
+
capture.clear();
|
|
5203
|
+
runPass.mockClear();
|
|
5204
|
+
sleep.tick();
|
|
5205
|
+
await flushLoopMicrotasks();
|
|
5206
|
+
expectFullReconcileDecision(capture.records[0], "full", [
|
|
5207
|
+
"event-push-surfaces-inactive",
|
|
5208
|
+
]);
|
|
5209
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
5210
|
+
shutdown();
|
|
5211
|
+
await loop;
|
|
5212
|
+
}
|
|
5213
|
+
finally {
|
|
5214
|
+
capture.restore();
|
|
5215
|
+
if (saved === undefined)
|
|
5216
|
+
delete process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
5217
|
+
else
|
|
5218
|
+
process.env.HQ_SYNC_FULL_RECONCILE_MS = saved;
|
|
5219
|
+
}
|
|
5220
|
+
});
|
|
5221
|
+
it("reports every matching trigger instead of short-circuiting at the first", async () => {
|
|
5222
|
+
const capture = captureFullReconcileDecisions();
|
|
5223
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5224
|
+
const { loop, shutdown } = startDrainLoop(runPass);
|
|
5225
|
+
await flushLoopMicrotasks();
|
|
5226
|
+
expectFullReconcileDecision(capture.records[0], "full", [
|
|
5227
|
+
"event-push-surfaces-inactive",
|
|
5228
|
+
"first-tick",
|
|
5229
|
+
]);
|
|
5230
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
5231
|
+
shutdown();
|
|
5232
|
+
await loop;
|
|
5233
|
+
capture.restore();
|
|
5234
|
+
});
|
|
5235
|
+
it("logs a bare watcher wake as the sole full-reconcile trigger", async () => {
|
|
5236
|
+
const capture = captureFullReconcileDecisions();
|
|
5237
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5238
|
+
const { loop, watcher, shutdown } = startDrainLoop(runPass);
|
|
5239
|
+
await flushLoopMicrotasks();
|
|
5240
|
+
capture.clear();
|
|
5241
|
+
runPass.mockClear();
|
|
5242
|
+
watcher.emit();
|
|
5243
|
+
await flushLoopMicrotasks();
|
|
5244
|
+
expectFullReconcileDecision(capture.records[0], "full", ["bare-wake"]);
|
|
5245
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
5246
|
+
shutdown();
|
|
5247
|
+
await loop;
|
|
5248
|
+
capture.restore();
|
|
5249
|
+
});
|
|
5250
|
+
it("logs batch-over-limit as the sole full-reconcile trigger", async () => {
|
|
5251
|
+
const saved = process.env.HQ_SYNC_EVENT_BATCH_LIMIT;
|
|
5252
|
+
process.env.HQ_SYNC_EVENT_BATCH_LIMIT = "2";
|
|
5253
|
+
const capture = captureFullReconcileDecisions();
|
|
5254
|
+
try {
|
|
5255
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5256
|
+
const { loop, watcher, tick, shutdown } = startBatchDrainLoop(runPass);
|
|
5257
|
+
await flushLoopMicrotasks();
|
|
5258
|
+
capture.clear();
|
|
5259
|
+
runPass.mockClear();
|
|
5260
|
+
watcher.emit("companies/indigo/a.md", {
|
|
5261
|
+
paths: new Map([
|
|
5262
|
+
["/tmp/hq/companies/indigo/a.md", "companies/indigo/a.md"],
|
|
5263
|
+
["/tmp/hq/companies/indigo/b.md", "companies/indigo/b.md"],
|
|
5264
|
+
["/tmp/hq/companies/indigo/c.md", "companies/indigo/c.md"],
|
|
5265
|
+
]),
|
|
5266
|
+
});
|
|
5267
|
+
tick();
|
|
5268
|
+
await flushLoopMicrotasks();
|
|
5269
|
+
expectFullReconcileDecision(capture.records[0], "full", ["batch-over-limit"]);
|
|
5270
|
+
expect(capture.records[0]?.batchPathCount).toBe(3);
|
|
5271
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
5272
|
+
shutdown();
|
|
5273
|
+
await loop;
|
|
5274
|
+
}
|
|
5275
|
+
finally {
|
|
5276
|
+
capture.restore();
|
|
5277
|
+
if (saved === undefined)
|
|
5278
|
+
delete process.env.HQ_SYNC_EVENT_BATCH_LIMIT;
|
|
5279
|
+
else
|
|
5280
|
+
process.env.HQ_SYNC_EVENT_BATCH_LIMIT = saved;
|
|
5281
|
+
}
|
|
5282
|
+
});
|
|
5283
|
+
it("logs overflow-routes-unknown as the sole full-reconcile trigger", async () => {
|
|
5284
|
+
const capture = captureFullReconcileDecisions();
|
|
5285
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5286
|
+
const { loop, watcher, tick, shutdown } = startBatchDrainLoop(runPass);
|
|
5287
|
+
await flushLoopMicrotasks();
|
|
5288
|
+
capture.clear();
|
|
5289
|
+
runPass.mockClear();
|
|
5290
|
+
watcher.emit("companies/indigo/a.md", {
|
|
5291
|
+
paths: new Map([["/tmp/hq/companies/indigo/a.md", "companies/indigo/a.md"]]),
|
|
5292
|
+
overflowed: true,
|
|
5293
|
+
});
|
|
5294
|
+
tick();
|
|
5295
|
+
await flushLoopMicrotasks();
|
|
5296
|
+
expectFullReconcileDecision(capture.records[0], "full", [
|
|
5297
|
+
"overflow-routes-unknown",
|
|
5298
|
+
]);
|
|
5299
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
5300
|
+
shutdown();
|
|
5301
|
+
await loop;
|
|
5302
|
+
capture.restore();
|
|
5303
|
+
});
|
|
5304
|
+
it("logs scheduled-interval as the sole full-reconcile trigger on the legacy cadence", async () => {
|
|
5305
|
+
const saved = process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
5306
|
+
delete process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
5307
|
+
const capture = captureFullReconcileDecisions();
|
|
5308
|
+
try {
|
|
5309
|
+
const sleep = makeSteppableSleep();
|
|
5310
|
+
let shutdown = () => { };
|
|
5311
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5312
|
+
const loop = runRunnerWithLoop([
|
|
5313
|
+
"--companies", "--watch", "--direction", "both", "--hq-root", "/tmp/hq",
|
|
5314
|
+
"--poll-remote-ms", "60000",
|
|
5315
|
+
], {
|
|
5316
|
+
runPass,
|
|
5317
|
+
sleep: sleep.sleep,
|
|
5318
|
+
onShutdownSignal: (handler) => {
|
|
5319
|
+
shutdown = handler;
|
|
5320
|
+
return () => { };
|
|
5321
|
+
},
|
|
5322
|
+
});
|
|
5323
|
+
await flushLoopMicrotasks();
|
|
5324
|
+
capture.clear();
|
|
5325
|
+
runPass.mockClear();
|
|
5326
|
+
for (let tick = 0; tick < 8; tick++) {
|
|
5327
|
+
sleep.tick();
|
|
5328
|
+
await flushLoopMicrotasks();
|
|
5329
|
+
}
|
|
5330
|
+
capture.clear();
|
|
5331
|
+
runPass.mockClear();
|
|
5332
|
+
sleep.tick();
|
|
5333
|
+
await flushLoopMicrotasks();
|
|
5334
|
+
expectFullReconcileDecision(capture.records[0], "full", [
|
|
5335
|
+
"scheduled-interval",
|
|
5336
|
+
]);
|
|
5337
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
5338
|
+
shutdown();
|
|
5339
|
+
await loop;
|
|
5340
|
+
}
|
|
5341
|
+
finally {
|
|
5342
|
+
capture.restore();
|
|
5343
|
+
if (saved === undefined)
|
|
5344
|
+
delete process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
5345
|
+
else
|
|
5346
|
+
process.env.HQ_SYNC_FULL_RECONCILE_MS = saved;
|
|
5347
|
+
}
|
|
5348
|
+
});
|
|
5349
|
+
it("logs scoped passes with their batch path count and preserves the scoped route", async () => {
|
|
5350
|
+
const capture = captureFullReconcileDecisions();
|
|
5351
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5352
|
+
const { loop, watcher, shutdown } = startDrainLoop(runPass);
|
|
5353
|
+
await flushLoopMicrotasks();
|
|
5354
|
+
capture.clear();
|
|
5355
|
+
runPass.mockClear();
|
|
5356
|
+
watcher.emit("companies/indigo/knowledge/a.md");
|
|
5357
|
+
await flushLoopMicrotasks();
|
|
5358
|
+
expectFullReconcileDecision(capture.records[0], "scoped", []);
|
|
5359
|
+
expect(capture.records[0]?.batchPathCount).toBe(1);
|
|
5360
|
+
expect(runPass.mock.calls.map((call) => call[0])).not.toContainEqual(fullArgv);
|
|
5361
|
+
expect(runPass).toHaveBeenCalledTimes(2);
|
|
5362
|
+
shutdown();
|
|
5363
|
+
await loop;
|
|
5364
|
+
capture.restore();
|
|
5365
|
+
});
|
|
5366
|
+
it("reports interval diagnostics while the configured interval has not elapsed", async () => {
|
|
5367
|
+
const saved = process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
5368
|
+
process.env.HQ_SYNC_FULL_RECONCILE_MS = "1000";
|
|
5369
|
+
const capture = captureFullReconcileDecisions();
|
|
5370
|
+
try {
|
|
5371
|
+
const clockState = { now: 0 };
|
|
5372
|
+
const sleep = makeSteppableSleep();
|
|
5373
|
+
let shutdown = () => { };
|
|
5374
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5375
|
+
const loop = runRunnerWithLoop(watchArgv, {
|
|
5376
|
+
runPass,
|
|
5377
|
+
monotonicNow: () => clockState.now,
|
|
5378
|
+
createWatcher: () => makeBatchWatcherStub(),
|
|
5379
|
+
sleep: sleep.sleep,
|
|
5380
|
+
onShutdownSignal: (handler) => {
|
|
5381
|
+
shutdown = handler;
|
|
5382
|
+
return () => { };
|
|
5383
|
+
},
|
|
5384
|
+
});
|
|
5385
|
+
await flushLoopMicrotasks();
|
|
5386
|
+
capture.clear();
|
|
5387
|
+
runPass.mockClear();
|
|
5388
|
+
clockState.now = 999;
|
|
5389
|
+
sleep.tick();
|
|
5390
|
+
await flushLoopMicrotasks();
|
|
5391
|
+
expectFullReconcileDecision(capture.records[0], "scoped", []);
|
|
5392
|
+
expect(capture.records[0]).toMatchObject({
|
|
5393
|
+
pollTick: 2,
|
|
5394
|
+
configuredIntervalMs: 1000,
|
|
5395
|
+
elapsedSinceLastCompletedFullReconcileMs: 999,
|
|
5396
|
+
triggers: allTriggerFlags(),
|
|
5397
|
+
});
|
|
5398
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullPullArgv]);
|
|
5399
|
+
shutdown();
|
|
5400
|
+
await loop;
|
|
5401
|
+
}
|
|
5402
|
+
finally {
|
|
5403
|
+
capture.restore();
|
|
5404
|
+
if (saved === undefined)
|
|
5405
|
+
delete process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
5406
|
+
else
|
|
5407
|
+
process.env.HQ_SYNC_FULL_RECONCILE_MS = saved;
|
|
5408
|
+
}
|
|
5409
|
+
});
|
|
5102
5410
|
it("startup runs one full reconcile, then watcher edits interrupt the pending wait", async () => {
|
|
5103
5411
|
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5104
5412
|
const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
|
|
@@ -5747,6 +6055,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
5747
6055
|
}
|
|
5748
6056
|
});
|
|
5749
6057
|
it("a throwing targeted pass falls back to one full reconcile instead of dropping the change", async () => {
|
|
6058
|
+
const capture = captureFullReconcileDecisions();
|
|
5750
6059
|
const runPass = vi.fn(async (argv) => {
|
|
5751
6060
|
if (argv.includes("pull") && argv.includes("--scope-path")) {
|
|
5752
6061
|
throw new Error("scoped pull exploded");
|
|
@@ -5755,6 +6064,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
5755
6064
|
});
|
|
5756
6065
|
const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
|
|
5757
6066
|
await flushLoopMicrotasks();
|
|
6067
|
+
capture.clear();
|
|
5758
6068
|
runPass.mockClear();
|
|
5759
6069
|
watcher.emit("companies/indigo/knowledge/a.md");
|
|
5760
6070
|
tick();
|
|
@@ -5784,9 +6094,14 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
5784
6094
|
// …and the loop degrades to the full reconcile, never dropping the tick.
|
|
5785
6095
|
fullArgv,
|
|
5786
6096
|
]);
|
|
6097
|
+
expect(capture.records).toHaveLength(1);
|
|
6098
|
+
expectFullReconcileDecision(capture.records[0], "full", [
|
|
6099
|
+
"targeted-pass-failed",
|
|
6100
|
+
]);
|
|
5787
6101
|
shutdown();
|
|
5788
6102
|
tick();
|
|
5789
6103
|
await loop;
|
|
6104
|
+
capture.restore();
|
|
5790
6105
|
});
|
|
5791
6106
|
it("wires the event debounce config (default 15s quiet / 120s max-wait) into the watcher factory", async () => {
|
|
5792
6107
|
const captured = [];
|