@indigoai-us/hq-cloud 6.15.64 → 6.15.65
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 +19 -0
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +329 -55
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +8 -0
- 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 +582 -1
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/watcher.d.ts +17 -0
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +74 -1
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +57 -0
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -14,7 +14,7 @@ import { runRunner, runRunnerWithLoop, resolveDeletePolicy, resolveSkipPersonal,
|
|
|
14
14
|
import { PresignObjectIO, S3SdkObjectIO } from "../object-io.js";
|
|
15
15
|
import { setBandwidthGovernorForTesting } from "../bandwidth.js";
|
|
16
16
|
import { FakeClock, TreeWatcher, } from "../watcher.js";
|
|
17
|
-
import { adaptivePollMs, advanceMonotonicDeadline, DEFAULT_EVENT_BATCH_LIMIT, DEFAULT_FULL_RECONCILE_MS, EVENT_BATCH_LIMIT_ENV, FULL_RECONCILE_MS_ENV, journalEntriesForWatcherDelete, resolveEventBatchLimit, resolveFullReconcileMs, shouldQuarantineWatcherDeleteIntents, WATCHER_REDISCOVERY_RETRY_MS, WATCHER_REDISCOVERY_RETRY_MAX_MS, } from "./sync-runner-watch-loop.js";
|
|
17
|
+
import { adaptivePollMs, advanceMonotonicDeadline, DEFAULT_EVENT_BATCH_LIMIT, DEFAULT_FAST_LANE_MAX_BYTES, DEFAULT_FAST_LANE_MAX_FILES, DEFAULT_FULL_RECONCILE_MS, EVENT_BATCH_LIMIT_ENV, FAST_LANE_MAX_BYTES_ENV, FAST_LANE_MAX_FILES_ENV, FULL_RECONCILE_MS_ENV, isFastLocalBatch, isFastReceiverBatch, journalEntriesForWatcherDelete, resolveEventBatchLimit, resolveFullReconcileMs, shouldQuarantineWatcherDeleteIntents, WATCHER_REDISCOVERY_RETRY_MS, WATCHER_REDISCOVERY_RETRY_MAX_MS, } from "./sync-runner-watch-loop.js";
|
|
18
18
|
import { PERSONAL_VAULT_JOURNAL_SLUG, readJournal, writeJournal, } from "../journal.js";
|
|
19
19
|
import { HQ_CLOUD_VERSION } from "../version.js";
|
|
20
20
|
import { lockPathFor, OPERATION_LOCKED_EXIT } from "../operation-lock.js";
|
|
@@ -4340,6 +4340,63 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
4340
4340
|
triggerShutdown();
|
|
4341
4341
|
await loop;
|
|
4342
4342
|
});
|
|
4343
|
+
it("pushes a file created before watcher ready through the warm-up catch-up", async () => {
|
|
4344
|
+
const watcher = makeBatchWatcherStub();
|
|
4345
|
+
let markReady = () => { };
|
|
4346
|
+
watcher.onReady = (listener) => {
|
|
4347
|
+
markReady = listener;
|
|
4348
|
+
return () => { };
|
|
4349
|
+
};
|
|
4350
|
+
watcher.collectWarmupCatchup = vi.fn(() => ({
|
|
4351
|
+
batch: {
|
|
4352
|
+
paths: new Map([[
|
|
4353
|
+
"/tmp/hq/companies/indigo/knowledge/created-during-warmup.md",
|
|
4354
|
+
"companies/indigo/knowledge/created-during-warmup.md",
|
|
4355
|
+
]]),
|
|
4356
|
+
changes: new Map([[
|
|
4357
|
+
"/tmp/hq/companies/indigo/knowledge/created-during-warmup.md",
|
|
4358
|
+
{ kind: "add" },
|
|
4359
|
+
]]),
|
|
4360
|
+
},
|
|
4361
|
+
scannedPaths: 1,
|
|
4362
|
+
}));
|
|
4363
|
+
const sleep = makeSteppableSleep();
|
|
4364
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
4365
|
+
let shutdown = () => { };
|
|
4366
|
+
const loop = runRunnerWithLoop([
|
|
4367
|
+
"--company", "indigo", "--watch", "--event-push", "--direction", "pull",
|
|
4368
|
+
"--hq-root", "/tmp/hq", "--poll-remote-ms", "60000",
|
|
4369
|
+
], {
|
|
4370
|
+
runPass,
|
|
4371
|
+
createWatcher: () => watcher,
|
|
4372
|
+
sleep: sleep.sleep,
|
|
4373
|
+
onShutdownSignal: (handler) => {
|
|
4374
|
+
shutdown = handler;
|
|
4375
|
+
return () => { };
|
|
4376
|
+
},
|
|
4377
|
+
});
|
|
4378
|
+
try {
|
|
4379
|
+
await flushLoopMicrotasks();
|
|
4380
|
+
markReady();
|
|
4381
|
+
await flushLoopMicrotasks();
|
|
4382
|
+
expect(watcher.collectWarmupCatchup).toHaveBeenCalledOnce();
|
|
4383
|
+
expect(runPass.mock.calls.map(([argv]) => argv)).toEqual([
|
|
4384
|
+
["--company", "indigo", "--direction", "pull", "--hq-root", "/tmp/hq"],
|
|
4385
|
+
[
|
|
4386
|
+
"--company", "indigo", "--direction", "push",
|
|
4387
|
+
"--scope-path", "knowledge/created-during-warmup.md", "--hq-root", "/tmp/hq",
|
|
4388
|
+
],
|
|
4389
|
+
[
|
|
4390
|
+
"--company", "indigo", "--direction", "pull",
|
|
4391
|
+
"--scope-path", "knowledge/created-during-warmup.md", "--hq-root", "/tmp/hq",
|
|
4392
|
+
],
|
|
4393
|
+
]);
|
|
4394
|
+
}
|
|
4395
|
+
finally {
|
|
4396
|
+
shutdown();
|
|
4397
|
+
await loop;
|
|
4398
|
+
}
|
|
4399
|
+
});
|
|
4343
4400
|
it("keeps every event surface off until a result proves maintenance completed", async () => {
|
|
4344
4401
|
const watcher = makeWatcherStub();
|
|
4345
4402
|
const sleep = makeSteppableSleep();
|
|
@@ -4592,6 +4649,528 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
4592
4649
|
triggerShutdown();
|
|
4593
4650
|
await loop;
|
|
4594
4651
|
});
|
|
4652
|
+
it("fast lane: a small watcher batch dispatches during a slow bulk pass", async () => {
|
|
4653
|
+
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-watch-"));
|
|
4654
|
+
const relativePath = "companies/indigo/realtime.md";
|
|
4655
|
+
const absolutePath = path.join(hqRoot, relativePath);
|
|
4656
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
4657
|
+
fs.writeFileSync(absolutePath, "tiny realtime edit");
|
|
4658
|
+
const watcher = makeBatchWatcherStub();
|
|
4659
|
+
let triggerShutdown = () => { };
|
|
4660
|
+
let releaseBulk = () => { };
|
|
4661
|
+
const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
|
|
4662
|
+
const order = [];
|
|
4663
|
+
let fullPasses = 0;
|
|
4664
|
+
const runPass = vi.fn().mockImplementation(async (passArgv) => {
|
|
4665
|
+
if (passArgv.includes("--scope-path")) {
|
|
4666
|
+
order.push("fast");
|
|
4667
|
+
return completedPassOutcome();
|
|
4668
|
+
}
|
|
4669
|
+
fullPasses += 1;
|
|
4670
|
+
if (fullPasses === 2) {
|
|
4671
|
+
order.push("bulk");
|
|
4672
|
+
await bulkGate;
|
|
4673
|
+
}
|
|
4674
|
+
return completedPassOutcome();
|
|
4675
|
+
});
|
|
4676
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "both", "--hq-root", hqRoot], {
|
|
4677
|
+
runPass,
|
|
4678
|
+
clock: new FakeClock(),
|
|
4679
|
+
createWatcher: () => watcher,
|
|
4680
|
+
sleep: () => new Promise(() => { }),
|
|
4681
|
+
onShutdownSignal: (handler) => {
|
|
4682
|
+
triggerShutdown = handler;
|
|
4683
|
+
return () => { };
|
|
4684
|
+
},
|
|
4685
|
+
});
|
|
4686
|
+
await flushLoopMicrotasks();
|
|
4687
|
+
watcher.emit(); // starts the second, deliberately blocked full pass
|
|
4688
|
+
await flushLoopMicrotasks();
|
|
4689
|
+
expect(order).toEqual(["bulk"]);
|
|
4690
|
+
watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
|
|
4691
|
+
await flushLoopMicrotasks(30);
|
|
4692
|
+
// Regression: origin/main leaves this as ["bulk"] until releaseBulk().
|
|
4693
|
+
expect(order).toEqual(["bulk", "fast", "fast"]);
|
|
4694
|
+
releaseBulk();
|
|
4695
|
+
await flushLoopMicrotasks();
|
|
4696
|
+
triggerShutdown();
|
|
4697
|
+
await loop;
|
|
4698
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
4699
|
+
});
|
|
4700
|
+
it("fast lane: a small receiver pull dispatches during a slow bulk pass", async () => {
|
|
4701
|
+
const watcher = makeWatcherStub();
|
|
4702
|
+
let triggerShutdown = () => { };
|
|
4703
|
+
let receiverSync;
|
|
4704
|
+
let releaseBulk = () => { };
|
|
4705
|
+
const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
|
|
4706
|
+
const order = [];
|
|
4707
|
+
let fullPasses = 0;
|
|
4708
|
+
const runPass = vi.fn().mockImplementation(async (passArgv) => {
|
|
4709
|
+
if (passArgv.includes("--scope-path")) {
|
|
4710
|
+
order.push("fast-pull");
|
|
4711
|
+
return completedPassOutcome();
|
|
4712
|
+
}
|
|
4713
|
+
fullPasses += 1;
|
|
4714
|
+
if (fullPasses === 2) {
|
|
4715
|
+
order.push("bulk");
|
|
4716
|
+
await bulkGate;
|
|
4717
|
+
}
|
|
4718
|
+
return completedPassOutcome();
|
|
4719
|
+
});
|
|
4720
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq-fast-lane-receiver"], {
|
|
4721
|
+
runPass,
|
|
4722
|
+
createWatcher: () => watcher,
|
|
4723
|
+
createReceiver: ({ syncBatchFn }) => {
|
|
4724
|
+
receiverSync = syncBatchFn;
|
|
4725
|
+
return { connected: true, start: async () => { }, dispose: async () => { } };
|
|
4726
|
+
},
|
|
4727
|
+
sleep: () => new Promise(() => { }),
|
|
4728
|
+
onShutdownSignal: (handler) => {
|
|
4729
|
+
triggerShutdown = handler;
|
|
4730
|
+
return () => { };
|
|
4731
|
+
},
|
|
4732
|
+
});
|
|
4733
|
+
await flushLoopMicrotasks();
|
|
4734
|
+
watcher.emit();
|
|
4735
|
+
await flushLoopMicrotasks();
|
|
4736
|
+
expect(order).toEqual(["bulk"]);
|
|
4737
|
+
await receiverSync({
|
|
4738
|
+
events: [{
|
|
4739
|
+
kind: "upsert", relativePath: "companies/acme/realtime.md", contentHash: "sha256:x",
|
|
4740
|
+
mtime: "2026-08-26T00:00:00.000Z", originDeviceId: "peer", originTenantId: "acme",
|
|
4741
|
+
sequenceNumber: 1, eventTimestamp: "2026-08-26T00:00:00.000Z",
|
|
4742
|
+
}],
|
|
4743
|
+
signal: new AbortController().signal,
|
|
4744
|
+
});
|
|
4745
|
+
expect(order).toEqual(["bulk", "fast-pull"]);
|
|
4746
|
+
releaseBulk();
|
|
4747
|
+
await flushLoopMicrotasks();
|
|
4748
|
+
triggerShutdown();
|
|
4749
|
+
await loop;
|
|
4750
|
+
});
|
|
4751
|
+
it("fast lane: retains the root lock until an outliving fast pass completes", async () => {
|
|
4752
|
+
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-lock-"));
|
|
4753
|
+
const relativePath = "companies/indigo/realtime.md";
|
|
4754
|
+
const absolutePath = path.join(hqRoot, relativePath);
|
|
4755
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
4756
|
+
fs.writeFileSync(absolutePath, "tiny realtime edit");
|
|
4757
|
+
const watcher = makeBatchWatcherStub();
|
|
4758
|
+
let triggerShutdown = () => { };
|
|
4759
|
+
let releaseBulk = () => { };
|
|
4760
|
+
let releaseFast = () => { };
|
|
4761
|
+
let markFastStarted = () => { };
|
|
4762
|
+
const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
|
|
4763
|
+
const fastGate = new Promise((resolve) => { releaseFast = resolve; });
|
|
4764
|
+
const fastStarted = new Promise((resolve) => { markFastStarted = resolve; });
|
|
4765
|
+
let fullPasses = 0;
|
|
4766
|
+
const runPass = vi.fn().mockImplementation(async (passArgv) => {
|
|
4767
|
+
if (passArgv.includes("--scope-path")) {
|
|
4768
|
+
markFastStarted();
|
|
4769
|
+
await fastGate;
|
|
4770
|
+
return completedPassOutcome();
|
|
4771
|
+
}
|
|
4772
|
+
fullPasses += 1;
|
|
4773
|
+
if (fullPasses === 2)
|
|
4774
|
+
await bulkGate;
|
|
4775
|
+
return completedPassOutcome();
|
|
4776
|
+
});
|
|
4777
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "both", "--hq-root", hqRoot], {
|
|
4778
|
+
runPass,
|
|
4779
|
+
clock: new FakeClock(),
|
|
4780
|
+
createWatcher: () => watcher,
|
|
4781
|
+
sleep: () => new Promise(() => { }),
|
|
4782
|
+
onShutdownSignal: (handler) => {
|
|
4783
|
+
triggerShutdown = handler;
|
|
4784
|
+
return () => { };
|
|
4785
|
+
},
|
|
4786
|
+
});
|
|
4787
|
+
await flushLoopMicrotasks();
|
|
4788
|
+
watcher.emit();
|
|
4789
|
+
await flushLoopMicrotasks();
|
|
4790
|
+
watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
|
|
4791
|
+
await fastStarted;
|
|
4792
|
+
releaseBulk();
|
|
4793
|
+
await flushLoopMicrotasks(20);
|
|
4794
|
+
// The slow pass has finished, but its fast bypass still mutates. A second
|
|
4795
|
+
// sync/rescue must not acquire the root operation lock in this interval.
|
|
4796
|
+
expect(fs.existsSync(lockPathFor(hqRoot))).toBe(true);
|
|
4797
|
+
releaseFast();
|
|
4798
|
+
await flushLoopMicrotasks(20);
|
|
4799
|
+
expect(fs.existsSync(lockPathFor(hqRoot))).toBe(false);
|
|
4800
|
+
triggerShutdown();
|
|
4801
|
+
await loop;
|
|
4802
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
4803
|
+
});
|
|
4804
|
+
it("fast lane: an absent unlinkDir subtree is always slow-lane", () => {
|
|
4805
|
+
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-unlink-dir-"));
|
|
4806
|
+
const deletedDir = path.join(hqRoot, "companies/indigo/archive");
|
|
4807
|
+
fs.mkdirSync(deletedDir, { recursive: true });
|
|
4808
|
+
fs.rmSync(deletedDir, { recursive: true, force: true });
|
|
4809
|
+
expect(isFastLocalBatch([deletedDir], new Map([[deletedDir, { kind: "unlinkDir" }]]))).toBe(false);
|
|
4810
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
4811
|
+
});
|
|
4812
|
+
it("fast lane: --skip-personal rejects a selected-out personal watcher batch", async () => {
|
|
4813
|
+
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-route-"));
|
|
4814
|
+
const relativePath = "knowledge/private.md";
|
|
4815
|
+
const absolutePath = path.join(hqRoot, relativePath);
|
|
4816
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
4817
|
+
fs.writeFileSync(absolutePath, "private");
|
|
4818
|
+
const watcher = makeBatchWatcherStub();
|
|
4819
|
+
let triggerShutdown = () => { };
|
|
4820
|
+
let releaseBulk = () => { };
|
|
4821
|
+
const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
|
|
4822
|
+
let fullPasses = 0;
|
|
4823
|
+
const runPass = vi.fn().mockImplementation(async (passArgv) => {
|
|
4824
|
+
if (passArgv.includes("--scope-path"))
|
|
4825
|
+
return completedPassOutcome();
|
|
4826
|
+
fullPasses += 1;
|
|
4827
|
+
if (fullPasses === 2)
|
|
4828
|
+
await bulkGate;
|
|
4829
|
+
return completedPassOutcome();
|
|
4830
|
+
});
|
|
4831
|
+
const loop = runRunnerWithLoop(["--companies", "--skip-personal", "--watch", "--event-push", "--direction", "both", "--hq-root", hqRoot], {
|
|
4832
|
+
runPass,
|
|
4833
|
+
clock: new FakeClock(),
|
|
4834
|
+
createWatcher: () => watcher,
|
|
4835
|
+
sleep: () => new Promise(() => { }),
|
|
4836
|
+
onShutdownSignal: (handler) => {
|
|
4837
|
+
triggerShutdown = handler;
|
|
4838
|
+
return () => { };
|
|
4839
|
+
},
|
|
4840
|
+
});
|
|
4841
|
+
await flushLoopMicrotasks();
|
|
4842
|
+
watcher.emit();
|
|
4843
|
+
await flushLoopMicrotasks();
|
|
4844
|
+
runPass.mockClear();
|
|
4845
|
+
watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
|
|
4846
|
+
await flushLoopMicrotasks(20);
|
|
4847
|
+
expect(runPass.mock.calls).toEqual([]);
|
|
4848
|
+
releaseBulk();
|
|
4849
|
+
await flushLoopMicrotasks();
|
|
4850
|
+
triggerShutdown();
|
|
4851
|
+
await loop;
|
|
4852
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
4853
|
+
});
|
|
4854
|
+
it("fast lane: signals the V2 scheduler for an immediate watcher dispatch", async () => {
|
|
4855
|
+
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-signal-"));
|
|
4856
|
+
const relativePath = "companies/indigo/realtime.md";
|
|
4857
|
+
const absolutePath = path.join(hqRoot, relativePath);
|
|
4858
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
4859
|
+
fs.writeFileSync(absolutePath, "tiny realtime edit");
|
|
4860
|
+
const watcher = makeBatchWatcherStub();
|
|
4861
|
+
const scheduler = {
|
|
4862
|
+
signal: vi.fn(),
|
|
4863
|
+
checkHighWater: vi.fn().mockResolvedValue(undefined),
|
|
4864
|
+
dispose: vi.fn().mockResolvedValue(undefined),
|
|
4865
|
+
};
|
|
4866
|
+
let triggerShutdown = () => { };
|
|
4867
|
+
let releaseBulk = () => { };
|
|
4868
|
+
const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
|
|
4869
|
+
let fullPasses = 0;
|
|
4870
|
+
const runPass = vi.fn().mockImplementation(async (passArgv) => {
|
|
4871
|
+
if (passArgv.includes("--scope-path"))
|
|
4872
|
+
return completedPassOutcome();
|
|
4873
|
+
fullPasses += 1;
|
|
4874
|
+
if (fullPasses === 2)
|
|
4875
|
+
await bulkGate;
|
|
4876
|
+
return completedPassOutcome();
|
|
4877
|
+
});
|
|
4878
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "both", "--hq-root", hqRoot], {
|
|
4879
|
+
runPass,
|
|
4880
|
+
clock: new FakeClock(),
|
|
4881
|
+
createWatcher: () => watcher,
|
|
4882
|
+
startRealtimeScheduler: async () => scheduler,
|
|
4883
|
+
sleep: () => new Promise(() => { }),
|
|
4884
|
+
onShutdownSignal: (handler) => {
|
|
4885
|
+
triggerShutdown = handler;
|
|
4886
|
+
return () => { };
|
|
4887
|
+
},
|
|
4888
|
+
});
|
|
4889
|
+
await flushLoopMicrotasks(20);
|
|
4890
|
+
watcher.emit();
|
|
4891
|
+
await flushLoopMicrotasks();
|
|
4892
|
+
scheduler.signal.mockClear();
|
|
4893
|
+
watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
|
|
4894
|
+
await flushLoopMicrotasks(20);
|
|
4895
|
+
expect(scheduler.signal).toHaveBeenCalledWith("watcher");
|
|
4896
|
+
releaseBulk();
|
|
4897
|
+
await flushLoopMicrotasks();
|
|
4898
|
+
triggerShutdown();
|
|
4899
|
+
await loop;
|
|
4900
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
4901
|
+
});
|
|
4902
|
+
it("fast lane: applies the receiver cap once across all eligible routes", async () => {
|
|
4903
|
+
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-receiver-cap-"));
|
|
4904
|
+
const watcher = makeWatcherStub();
|
|
4905
|
+
let receiverSync;
|
|
4906
|
+
let triggerShutdown = () => { };
|
|
4907
|
+
let releaseBulk = () => { };
|
|
4908
|
+
const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
|
|
4909
|
+
let fullPasses = 0;
|
|
4910
|
+
const runPass = vi.fn().mockImplementation(async (passArgv) => {
|
|
4911
|
+
if (passArgv.includes("--scope-path"))
|
|
4912
|
+
return completedPassOutcome();
|
|
4913
|
+
fullPasses += 1;
|
|
4914
|
+
if (fullPasses === 2)
|
|
4915
|
+
await bulkGate;
|
|
4916
|
+
return completedPassOutcome();
|
|
4917
|
+
});
|
|
4918
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", hqRoot], {
|
|
4919
|
+
runPass,
|
|
4920
|
+
createWatcher: () => watcher,
|
|
4921
|
+
createReceiver: ({ syncBatchFn }) => {
|
|
4922
|
+
receiverSync = syncBatchFn;
|
|
4923
|
+
return { connected: true, start: async () => { }, dispose: async () => { } };
|
|
4924
|
+
},
|
|
4925
|
+
sleep: () => new Promise(() => { }),
|
|
4926
|
+
onShutdownSignal: (handler) => {
|
|
4927
|
+
triggerShutdown = handler;
|
|
4928
|
+
return () => { };
|
|
4929
|
+
},
|
|
4930
|
+
});
|
|
4931
|
+
await flushLoopMicrotasks();
|
|
4932
|
+
watcher.emit();
|
|
4933
|
+
await flushLoopMicrotasks();
|
|
4934
|
+
runPass.mockClear();
|
|
4935
|
+
const events = [
|
|
4936
|
+
...Array.from({ length: 17 }, (_, index) => ({
|
|
4937
|
+
kind: "upsert",
|
|
4938
|
+
relativePath: `companies/acme/a-${index}.md`,
|
|
4939
|
+
contentHash: "sha256:x",
|
|
4940
|
+
mtime: "2026-08-26T00:00:00.000Z",
|
|
4941
|
+
originDeviceId: "peer",
|
|
4942
|
+
originTenantId: "acme",
|
|
4943
|
+
sequenceNumber: index,
|
|
4944
|
+
eventTimestamp: "2026-08-26T00:00:00.000Z",
|
|
4945
|
+
})),
|
|
4946
|
+
...Array.from({ length: 16 }, (_, index) => ({
|
|
4947
|
+
kind: "upsert",
|
|
4948
|
+
relativePath: `companies/beta/b-${index}.md`,
|
|
4949
|
+
contentHash: "sha256:x",
|
|
4950
|
+
mtime: "2026-08-26T00:00:00.000Z",
|
|
4951
|
+
originDeviceId: "peer",
|
|
4952
|
+
originTenantId: "beta",
|
|
4953
|
+
sequenceNumber: index + 17,
|
|
4954
|
+
eventTimestamp: "2026-08-26T00:00:00.000Z",
|
|
4955
|
+
})),
|
|
4956
|
+
];
|
|
4957
|
+
const receiverDrain = receiverSync({
|
|
4958
|
+
events,
|
|
4959
|
+
signal: new AbortController().signal,
|
|
4960
|
+
});
|
|
4961
|
+
await flushLoopMicrotasks(20);
|
|
4962
|
+
expect(runPass).not.toHaveBeenCalled();
|
|
4963
|
+
releaseBulk();
|
|
4964
|
+
await receiverDrain;
|
|
4965
|
+
triggerShutdown();
|
|
4966
|
+
await loop;
|
|
4967
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
4968
|
+
});
|
|
4969
|
+
it("fast lane: queues slow work behind a receiver pass that owns the root lock", async () => {
|
|
4970
|
+
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-slow-queue-"));
|
|
4971
|
+
const watcher = makeWatcherStub();
|
|
4972
|
+
let receiverSync;
|
|
4973
|
+
let triggerShutdown = () => { };
|
|
4974
|
+
let releaseFast = () => { };
|
|
4975
|
+
let markFastStarted = () => { };
|
|
4976
|
+
const fastGate = new Promise((resolve) => { releaseFast = resolve; });
|
|
4977
|
+
const fastStarted = new Promise((resolve) => { markFastStarted = resolve; });
|
|
4978
|
+
const runPass = vi.fn().mockImplementation(async (passArgv) => {
|
|
4979
|
+
if (passArgv.includes("--scope-path")) {
|
|
4980
|
+
markFastStarted();
|
|
4981
|
+
await fastGate;
|
|
4982
|
+
return completedPassOutcome();
|
|
4983
|
+
}
|
|
4984
|
+
return completedPassOutcome();
|
|
4985
|
+
});
|
|
4986
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--lock-timeout", "0", "--hq-root", hqRoot], {
|
|
4987
|
+
runPass,
|
|
4988
|
+
createWatcher: () => watcher,
|
|
4989
|
+
createReceiver: ({ syncBatchFn }) => {
|
|
4990
|
+
receiverSync = syncBatchFn;
|
|
4991
|
+
return { connected: true, start: async () => { }, dispose: async () => { } };
|
|
4992
|
+
},
|
|
4993
|
+
sleep: () => new Promise(() => { }),
|
|
4994
|
+
onShutdownSignal: (handler) => {
|
|
4995
|
+
triggerShutdown = handler;
|
|
4996
|
+
return () => { };
|
|
4997
|
+
},
|
|
4998
|
+
});
|
|
4999
|
+
let loopSettled = false;
|
|
5000
|
+
void loop.then(() => { loopSettled = true; });
|
|
5001
|
+
await flushLoopMicrotasks();
|
|
5002
|
+
const receiverDrain = receiverSync({
|
|
5003
|
+
events: [{
|
|
5004
|
+
kind: "upsert",
|
|
5005
|
+
relativePath: "companies/acme/remote.md",
|
|
5006
|
+
contentHash: "sha256:x",
|
|
5007
|
+
mtime: "2026-08-26T00:00:00.000Z",
|
|
5008
|
+
originDeviceId: "peer",
|
|
5009
|
+
originTenantId: "acme",
|
|
5010
|
+
sequenceNumber: 1,
|
|
5011
|
+
eventTimestamp: "2026-08-26T00:00:00.000Z",
|
|
5012
|
+
}],
|
|
5013
|
+
signal: new AbortController().signal,
|
|
5014
|
+
});
|
|
5015
|
+
await fastStarted;
|
|
5016
|
+
// This wake requests a slow reconcile while the receiver owns the lock.
|
|
5017
|
+
watcher.emit();
|
|
5018
|
+
await flushLoopMicrotasks(20);
|
|
5019
|
+
expect(loopSettled).toBe(false);
|
|
5020
|
+
releaseFast();
|
|
5021
|
+
await receiverDrain;
|
|
5022
|
+
triggerShutdown();
|
|
5023
|
+
await loop;
|
|
5024
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
5025
|
+
});
|
|
5026
|
+
it("fast lane: keeps disjoint journal rows under a 16-path interleave with a bulk pass", async () => {
|
|
5027
|
+
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-journal-"));
|
|
5028
|
+
const paths = Array.from({ length: 16 }, (_, index) => {
|
|
5029
|
+
const relativePath = `companies/indigo/realtime-${index}.md`;
|
|
5030
|
+
const absolutePath = path.join(hqRoot, relativePath);
|
|
5031
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
5032
|
+
fs.writeFileSync(absolutePath, "tiny realtime edit");
|
|
5033
|
+
return [absolutePath, relativePath];
|
|
5034
|
+
});
|
|
5035
|
+
writeJournal("indigo", { version: "2", lastSync: "", pulls: [], files: {} });
|
|
5036
|
+
const watcher = makeBatchWatcherStub();
|
|
5037
|
+
let triggerShutdown = () => { };
|
|
5038
|
+
let releaseBulk = () => { };
|
|
5039
|
+
const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
|
|
5040
|
+
let fullPasses = 0;
|
|
5041
|
+
const runPass = vi.fn().mockImplementation(async (passArgv) => {
|
|
5042
|
+
if (passArgv.includes("--scope-path")) {
|
|
5043
|
+
const fast = readJournal("indigo");
|
|
5044
|
+
for (let index = 0; index < paths.length; index++) {
|
|
5045
|
+
fast.files[`realtime-${index}.md`] = {
|
|
5046
|
+
hash: `fast-${index}`, size: 1, syncedAt: "", direction: "up",
|
|
5047
|
+
};
|
|
5048
|
+
}
|
|
5049
|
+
writeJournal("indigo", fast);
|
|
5050
|
+
return completedPassOutcome();
|
|
5051
|
+
}
|
|
5052
|
+
fullPasses += 1;
|
|
5053
|
+
if (fullPasses === 2) {
|
|
5054
|
+
const slow = readJournal("indigo");
|
|
5055
|
+
await bulkGate;
|
|
5056
|
+
for (let index = 0; index < paths.length; index++) {
|
|
5057
|
+
slow.files[`bulk-${index}.md`] = {
|
|
5058
|
+
hash: `slow-${index}`, size: 1, syncedAt: "", direction: "up",
|
|
5059
|
+
};
|
|
5060
|
+
}
|
|
5061
|
+
writeJournal("indigo", slow);
|
|
5062
|
+
}
|
|
5063
|
+
return completedPassOutcome();
|
|
5064
|
+
});
|
|
5065
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "push", "--hq-root", hqRoot], {
|
|
5066
|
+
runPass,
|
|
5067
|
+
clock: new FakeClock(),
|
|
5068
|
+
createWatcher: () => watcher,
|
|
5069
|
+
sleep: () => new Promise(() => { }),
|
|
5070
|
+
onShutdownSignal: (handler) => {
|
|
5071
|
+
triggerShutdown = handler;
|
|
5072
|
+
return () => { };
|
|
5073
|
+
},
|
|
5074
|
+
});
|
|
5075
|
+
await flushLoopMicrotasks();
|
|
5076
|
+
watcher.emit();
|
|
5077
|
+
await flushLoopMicrotasks();
|
|
5078
|
+
watcher.emit(paths[0][1], { paths: new Map(paths) });
|
|
5079
|
+
await flushLoopMicrotasks(30);
|
|
5080
|
+
expect(Object.keys(readJournal("indigo").files)).toHaveLength(paths.length);
|
|
5081
|
+
releaseBulk();
|
|
5082
|
+
await flushLoopMicrotasks();
|
|
5083
|
+
const journal = readJournal("indigo").files;
|
|
5084
|
+
expect(Object.keys(journal)).toHaveLength(paths.length * 2);
|
|
5085
|
+
for (let index = 0; index < paths.length; index++) {
|
|
5086
|
+
expect(journal[`bulk-${index}.md`]?.hash).toBe(`slow-${index}`);
|
|
5087
|
+
expect(journal[`realtime-${index}.md`]?.hash).toBe(`fast-${index}`);
|
|
5088
|
+
}
|
|
5089
|
+
triggerShutdown();
|
|
5090
|
+
await loop;
|
|
5091
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
5092
|
+
});
|
|
5093
|
+
it("fast lane: a bulk pass still completes after repeated small watcher batches", async () => {
|
|
5094
|
+
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-fairness-"));
|
|
5095
|
+
const watcher = makeBatchWatcherStub();
|
|
5096
|
+
let triggerShutdown = () => { };
|
|
5097
|
+
let releaseBulk = () => { };
|
|
5098
|
+
const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
|
|
5099
|
+
const completed = [];
|
|
5100
|
+
let fullPasses = 0;
|
|
5101
|
+
const runPass = vi.fn().mockImplementation(async (passArgv) => {
|
|
5102
|
+
if (passArgv.includes("--scope-path")) {
|
|
5103
|
+
completed.push("fast");
|
|
5104
|
+
return completedPassOutcome();
|
|
5105
|
+
}
|
|
5106
|
+
fullPasses += 1;
|
|
5107
|
+
if (fullPasses === 2) {
|
|
5108
|
+
await bulkGate;
|
|
5109
|
+
completed.push("bulk");
|
|
5110
|
+
}
|
|
5111
|
+
return completedPassOutcome();
|
|
5112
|
+
});
|
|
5113
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "push", "--hq-root", hqRoot], {
|
|
5114
|
+
runPass,
|
|
5115
|
+
clock: new FakeClock(),
|
|
5116
|
+
createWatcher: () => watcher,
|
|
5117
|
+
sleep: () => new Promise(() => { }),
|
|
5118
|
+
onShutdownSignal: (handler) => {
|
|
5119
|
+
triggerShutdown = handler;
|
|
5120
|
+
return () => { };
|
|
5121
|
+
},
|
|
5122
|
+
});
|
|
5123
|
+
await flushLoopMicrotasks();
|
|
5124
|
+
watcher.emit();
|
|
5125
|
+
await flushLoopMicrotasks();
|
|
5126
|
+
for (let index = 0; index < 5; index++) {
|
|
5127
|
+
const relativePath = `companies/indigo/realtime-${index}.md`;
|
|
5128
|
+
const absolutePath = path.join(hqRoot, relativePath);
|
|
5129
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
5130
|
+
fs.writeFileSync(absolutePath, "tiny");
|
|
5131
|
+
watcher.emit(relativePath, { paths: new Map([[absolutePath, relativePath]]) });
|
|
5132
|
+
}
|
|
5133
|
+
await flushLoopMicrotasks(60);
|
|
5134
|
+
expect(completed).toEqual(["fast", "fast", "fast", "fast", "fast"]);
|
|
5135
|
+
releaseBulk();
|
|
5136
|
+
await flushLoopMicrotasks();
|
|
5137
|
+
expect(completed).toContain("bulk");
|
|
5138
|
+
triggerShutdown();
|
|
5139
|
+
await loop;
|
|
5140
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
5141
|
+
});
|
|
5142
|
+
it("fast lane: sends only batches at or below the file and byte limits", () => {
|
|
5143
|
+
const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-limit-"));
|
|
5144
|
+
const small = path.join(hqRoot, "small.md");
|
|
5145
|
+
const boundary = path.join(hqRoot, "boundary.bin");
|
|
5146
|
+
fs.writeFileSync(small, "small");
|
|
5147
|
+
fs.writeFileSync(boundary, Buffer.alloc(DEFAULT_FAST_LANE_MAX_BYTES));
|
|
5148
|
+
expect(isFastLocalBatch([small])).toBe(true);
|
|
5149
|
+
expect(isFastLocalBatch([boundary])).toBe(true);
|
|
5150
|
+
fs.appendFileSync(boundary, "x");
|
|
5151
|
+
expect(isFastLocalBatch([boundary])).toBe(false);
|
|
5152
|
+
expect(isFastReceiverBatch(DEFAULT_FAST_LANE_MAX_FILES)).toBe(true);
|
|
5153
|
+
expect(isFastReceiverBatch(DEFAULT_FAST_LANE_MAX_FILES + 1)).toBe(false);
|
|
5154
|
+
const previousFiles = process.env[FAST_LANE_MAX_FILES_ENV];
|
|
5155
|
+
const previousBytes = process.env[FAST_LANE_MAX_BYTES_ENV];
|
|
5156
|
+
process.env[FAST_LANE_MAX_FILES_ENV] = "1";
|
|
5157
|
+
process.env[FAST_LANE_MAX_BYTES_ENV] = "4";
|
|
5158
|
+
try {
|
|
5159
|
+
expect(isFastLocalBatch([small])).toBe(false);
|
|
5160
|
+
expect(isFastReceiverBatch(2)).toBe(false);
|
|
5161
|
+
}
|
|
5162
|
+
finally {
|
|
5163
|
+
if (previousFiles === undefined)
|
|
5164
|
+
delete process.env[FAST_LANE_MAX_FILES_ENV];
|
|
5165
|
+
else
|
|
5166
|
+
process.env[FAST_LANE_MAX_FILES_ENV] = previousFiles;
|
|
5167
|
+
if (previousBytes === undefined)
|
|
5168
|
+
delete process.env[FAST_LANE_MAX_BYTES_ENV];
|
|
5169
|
+
else
|
|
5170
|
+
process.env[FAST_LANE_MAX_BYTES_ENV] = previousBytes;
|
|
5171
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
5172
|
+
}
|
|
5173
|
+
});
|
|
4595
5174
|
it("U07: drains the first watcher event after activation without guarded overlap", async () => {
|
|
4596
5175
|
const watcher = makeWatcherStub();
|
|
4597
5176
|
let triggerShutdown = () => { };
|
|
@@ -5147,6 +5726,8 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
5147
5726
|
"bare-wake": matched.includes("bare-wake"),
|
|
5148
5727
|
"batch-over-limit": matched.includes("batch-over-limit"),
|
|
5149
5728
|
"overflow-routes-unknown": matched.includes("overflow-routes-unknown"),
|
|
5729
|
+
"watcher-warmup-catch-up": matched.includes("watcher-warmup-catch-up"),
|
|
5730
|
+
"watcher-warmup-catch-up-failed": matched.includes("watcher-warmup-catch-up-failed"),
|
|
5150
5731
|
"scheduled-interval": matched.includes("scheduled-interval"),
|
|
5151
5732
|
});
|
|
5152
5733
|
function expectFullReconcileDecision(record, mode, reasons) {
|