@indigoai-us/hq-cloud 6.15.47 → 6.15.49
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 +15 -4
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +69 -9
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.test.js +306 -16
- package/dist/bin/sync-runner.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, journalEntriesForWatcherDelete, 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_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";
|
|
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";
|
|
@@ -3709,6 +3709,23 @@ describe("advanceMonotonicDeadline", () => {
|
|
|
3709
3709
|
});
|
|
3710
3710
|
});
|
|
3711
3711
|
});
|
|
3712
|
+
describe("watch-loop reconcile environment resolvers", () => {
|
|
3713
|
+
it("uses the shipped 10,000 event-batch default while allowing smaller and larger overrides", () => {
|
|
3714
|
+
expect(DEFAULT_EVENT_BATCH_LIMIT).toBe(10_000);
|
|
3715
|
+
expect(resolveEventBatchLimit({})).toBe(10_000);
|
|
3716
|
+
expect(resolveEventBatchLimit({ [EVENT_BATCH_LIMIT_ENV]: "5" })).toBe(5);
|
|
3717
|
+
expect(resolveEventBatchLimit({ [EVENT_BATCH_LIMIT_ENV]: "50000" })).toBe(50_000);
|
|
3718
|
+
});
|
|
3719
|
+
it("uses the legacy tick cadence for an unset or invalid full-reconcile interval", () => {
|
|
3720
|
+
expect(resolveFullReconcileMs({})).toBe(DEFAULT_FULL_RECONCILE_MS);
|
|
3721
|
+
for (const raw of ["", "0", "0.5", "-1", "NaN", "not-a-number", "Infinity"]) {
|
|
3722
|
+
expect(resolveFullReconcileMs({ [FULL_RECONCILE_MS_ENV]: raw })).toBe(DEFAULT_FULL_RECONCILE_MS);
|
|
3723
|
+
}
|
|
3724
|
+
});
|
|
3725
|
+
it("accepts a positive millisecond full-reconcile interval", () => {
|
|
3726
|
+
expect(resolveFullReconcileMs({ [FULL_RECONCILE_MS_ENV]: "10800000" })).toBe(10_800_000);
|
|
3727
|
+
});
|
|
3728
|
+
});
|
|
3712
3729
|
describe("runRunnerWithLoop — adaptive poll interval (no --poll-remote-ms)", () => {
|
|
3713
3730
|
// Capture the ms handed to sleep on the FIRST poll, then hang so the loop
|
|
3714
3731
|
// parks until shutdown (mirrors the never-resolving sleep the other loop
|
|
@@ -3836,6 +3853,56 @@ describe("runRunnerWithLoop — adaptive poll interval (no --poll-remote-ms)", (
|
|
|
3836
3853
|
});
|
|
3837
3854
|
});
|
|
3838
3855
|
describe("runRunnerWithLoop — event-push wiring", () => {
|
|
3856
|
+
it("keeps a healthy watcher through repeated poll cycles and the periodic full reconcile", async () => {
|
|
3857
|
+
const sleep = makeSteppableSleep();
|
|
3858
|
+
const watchers = [];
|
|
3859
|
+
let disposeCalls = 0;
|
|
3860
|
+
const createWatcher = vi.fn(() => {
|
|
3861
|
+
const watcher = makeWatcherStub();
|
|
3862
|
+
watcher.isWatching = () => true;
|
|
3863
|
+
watcher.watchedPathCount = () => 16_609;
|
|
3864
|
+
const dispose = watcher.dispose.bind(watcher);
|
|
3865
|
+
watcher.dispose = () => {
|
|
3866
|
+
disposeCalls += 1;
|
|
3867
|
+
dispose();
|
|
3868
|
+
};
|
|
3869
|
+
watchers.push(watcher);
|
|
3870
|
+
return watcher;
|
|
3871
|
+
});
|
|
3872
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
3873
|
+
let triggerShutdown = () => { };
|
|
3874
|
+
const loop = runRunnerWithLoop([
|
|
3875
|
+
"--companies",
|
|
3876
|
+
"--watch",
|
|
3877
|
+
"--event-push",
|
|
3878
|
+
"--poll-remote-ms",
|
|
3879
|
+
"60000",
|
|
3880
|
+
"--hq-root",
|
|
3881
|
+
"/tmp/hq",
|
|
3882
|
+
], {
|
|
3883
|
+
runPass,
|
|
3884
|
+
createWatcher,
|
|
3885
|
+
sleep: sleep.sleep,
|
|
3886
|
+
onShutdownSignal: (handler) => {
|
|
3887
|
+
triggerShutdown = handler;
|
|
3888
|
+
return () => { };
|
|
3889
|
+
},
|
|
3890
|
+
});
|
|
3891
|
+
await flushLoopMicrotasks();
|
|
3892
|
+
for (let i = 0; i < 9; i++) {
|
|
3893
|
+
sleep.tick();
|
|
3894
|
+
await flushLoopMicrotasks();
|
|
3895
|
+
}
|
|
3896
|
+
// Tick 1 and tick 10 are full reconciles; all ten completed poll cycles
|
|
3897
|
+
// must leave the healthy watcher in place.
|
|
3898
|
+
expect(runPass).toHaveBeenCalledTimes(10);
|
|
3899
|
+
expect(createWatcher).toHaveBeenCalledTimes(1);
|
|
3900
|
+
expect(watchers).toHaveLength(1);
|
|
3901
|
+
expect(disposeCalls).toBe(0);
|
|
3902
|
+
triggerShutdown();
|
|
3903
|
+
await loop;
|
|
3904
|
+
expect(disposeCalls).toBe(1);
|
|
3905
|
+
});
|
|
3839
3906
|
it("activates the watcher after a successful numeric --companies pass", async () => {
|
|
3840
3907
|
const watcher = makeWatcherStub();
|
|
3841
3908
|
let triggerShutdown = () => { };
|
|
@@ -4045,6 +4112,8 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
4045
4112
|
expect(stderrWrite.mock.calls.map(([chunk]) => String(chunk)).join("")).toContain("event-push watcher degraded; stopped; continuing cadence-only: initial_discovery_timeout");
|
|
4046
4113
|
await vi.advanceTimersByTimeAsync(WATCHER_REDISCOVERY_RETRY_MS);
|
|
4047
4114
|
expect(attempts).toBe(2);
|
|
4115
|
+
expect(stderrWrite.mock.calls.map(([chunk]) => String(chunk)).join("")).toContain("watcher-replacement reason=initial_discovery_timeout " +
|
|
4116
|
+
"previous-watcher-id=1 next-watcher-id=2");
|
|
4048
4117
|
await vi.advanceTimersByTimeAsync(WATCHER_REDISCOVERY_RETRY_MS * 2 - 1);
|
|
4049
4118
|
expect(attempts).toBe(2);
|
|
4050
4119
|
await vi.advanceTimersByTimeAsync(1);
|
|
@@ -4068,6 +4137,74 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
4068
4137
|
vi.useRealTimers();
|
|
4069
4138
|
}
|
|
4070
4139
|
});
|
|
4140
|
+
it("re-arms exponential backoff when replacement watcher discovery throws", async () => {
|
|
4141
|
+
vi.useFakeTimers();
|
|
4142
|
+
const stderrWrite = vi
|
|
4143
|
+
.spyOn(process.stderr, "write")
|
|
4144
|
+
.mockImplementation(() => true);
|
|
4145
|
+
let attempts = 0;
|
|
4146
|
+
let triggerShutdown = () => { };
|
|
4147
|
+
const createWatcher = vi.fn((options) => {
|
|
4148
|
+
attempts += 1;
|
|
4149
|
+
if (attempts > 1 && attempts < 7) {
|
|
4150
|
+
throw new Error("transient replacement watcher failure");
|
|
4151
|
+
}
|
|
4152
|
+
const watcher = makeWatcherStub();
|
|
4153
|
+
watcher.isWatching = () => attempts === 7;
|
|
4154
|
+
watcher.watchedPathCount = () => 9_220;
|
|
4155
|
+
watcher.watchMode = () => "chokidar";
|
|
4156
|
+
if (attempts === 1) {
|
|
4157
|
+
watcher.start = () => {
|
|
4158
|
+
watcher.started = true;
|
|
4159
|
+
options.onDegraded?.({
|
|
4160
|
+
reason: "initial_discovery_timeout",
|
|
4161
|
+
watchedPaths: 9_220,
|
|
4162
|
+
maxWatchedPaths: 500_000,
|
|
4163
|
+
action: "stopped",
|
|
4164
|
+
});
|
|
4165
|
+
};
|
|
4166
|
+
}
|
|
4167
|
+
return watcher;
|
|
4168
|
+
});
|
|
4169
|
+
const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
4170
|
+
runPass: vi.fn().mockResolvedValue(completedPassOutcome()),
|
|
4171
|
+
createWatcher,
|
|
4172
|
+
idleHeartbeatIntervalMs: WATCHER_REDISCOVERY_RETRY_MS,
|
|
4173
|
+
sleep: () => new Promise(() => { }),
|
|
4174
|
+
onShutdownSignal: (handler) => {
|
|
4175
|
+
triggerShutdown = handler;
|
|
4176
|
+
return () => { };
|
|
4177
|
+
},
|
|
4178
|
+
});
|
|
4179
|
+
try {
|
|
4180
|
+
await flushLoopMicrotasks();
|
|
4181
|
+
expect(attempts).toBe(1);
|
|
4182
|
+
await vi.advanceTimersByTimeAsync(WATCHER_REDISCOVERY_RETRY_MS);
|
|
4183
|
+
expect(attempts).toBe(2);
|
|
4184
|
+
await vi.advanceTimersByTimeAsync(WATCHER_REDISCOVERY_RETRY_MS * 2 - 1);
|
|
4185
|
+
expect(attempts).toBe(2);
|
|
4186
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
4187
|
+
expect(attempts).toBe(3);
|
|
4188
|
+
await vi.advanceTimersByTimeAsync(WATCHER_REDISCOVERY_RETRY_MS * 4);
|
|
4189
|
+
expect(attempts).toBe(4);
|
|
4190
|
+
await vi.advanceTimersByTimeAsync(WATCHER_REDISCOVERY_RETRY_MS * 8);
|
|
4191
|
+
expect(attempts).toBe(5);
|
|
4192
|
+
await vi.advanceTimersByTimeAsync(WATCHER_REDISCOVERY_RETRY_MAX_MS);
|
|
4193
|
+
expect(attempts).toBe(6);
|
|
4194
|
+
// Repeated replacement failures cap at one hour rather than retrying at
|
|
4195
|
+
// a fixed short interval. The next attempt recovers event push.
|
|
4196
|
+
await vi.advanceTimersByTimeAsync(WATCHER_REDISCOVERY_RETRY_MAX_MS);
|
|
4197
|
+
expect(attempts).toBe(7);
|
|
4198
|
+
await vi.advanceTimersByTimeAsync(WATCHER_REDISCOVERY_RETRY_MAX_MS);
|
|
4199
|
+
expect(createWatcher).toHaveBeenCalledTimes(7);
|
|
4200
|
+
}
|
|
4201
|
+
finally {
|
|
4202
|
+
triggerShutdown();
|
|
4203
|
+
await loop;
|
|
4204
|
+
stderrWrite.mockRestore();
|
|
4205
|
+
vi.useRealTimers();
|
|
4206
|
+
}
|
|
4207
|
+
});
|
|
4071
4208
|
it("reports current watcher liveness after startup", async () => {
|
|
4072
4209
|
vi.useFakeTimers();
|
|
4073
4210
|
const watcher = makeWatcherStub();
|
|
@@ -5325,6 +5462,125 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
5325
5462
|
sleep.tick();
|
|
5326
5463
|
await loop;
|
|
5327
5464
|
});
|
|
5465
|
+
it("keeps the legacy every-tenth-tick full-reconcile cadence when the interval env is unset", async () => {
|
|
5466
|
+
const saved = process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
5467
|
+
delete process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
5468
|
+
try {
|
|
5469
|
+
const watcher = makeBatchWatcherStub();
|
|
5470
|
+
const sleep = makeSteppableSleep();
|
|
5471
|
+
let triggerShutdown = () => { };
|
|
5472
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5473
|
+
const loop = runRunnerWithLoop(watchArgv, {
|
|
5474
|
+
runPass,
|
|
5475
|
+
clock: new FakeClock(),
|
|
5476
|
+
createWatcher: () => watcher,
|
|
5477
|
+
sleep: sleep.sleep,
|
|
5478
|
+
onShutdownSignal: (handler) => {
|
|
5479
|
+
triggerShutdown = handler;
|
|
5480
|
+
return () => { };
|
|
5481
|
+
},
|
|
5482
|
+
});
|
|
5483
|
+
await flushLoopMicrotasks();
|
|
5484
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
5485
|
+
runPass.mockClear();
|
|
5486
|
+
for (let tick = 0; tick < 8; tick++) {
|
|
5487
|
+
sleep.tick();
|
|
5488
|
+
await flushLoopMicrotasks();
|
|
5489
|
+
}
|
|
5490
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual(Array.from({ length: 8 }, () => fullPullArgv));
|
|
5491
|
+
runPass.mockClear();
|
|
5492
|
+
sleep.tick();
|
|
5493
|
+
await flushLoopMicrotasks();
|
|
5494
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
5495
|
+
triggerShutdown();
|
|
5496
|
+
await loop;
|
|
5497
|
+
}
|
|
5498
|
+
finally {
|
|
5499
|
+
if (saved === undefined)
|
|
5500
|
+
delete process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
5501
|
+
else
|
|
5502
|
+
process.env.HQ_SYNC_FULL_RECONCILE_MS = saved;
|
|
5503
|
+
}
|
|
5504
|
+
});
|
|
5505
|
+
it("uses the configured elapsed-time interval instead of the tick count", async () => {
|
|
5506
|
+
const saved = process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
5507
|
+
process.env.HQ_SYNC_FULL_RECONCILE_MS = "1000";
|
|
5508
|
+
try {
|
|
5509
|
+
const clockState = { now: 0 };
|
|
5510
|
+
const watcher = makeBatchWatcherStub();
|
|
5511
|
+
const sleep = makeSteppableSleep();
|
|
5512
|
+
let triggerShutdown = () => { };
|
|
5513
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5514
|
+
const loop = runRunnerWithLoop(watchArgv, {
|
|
5515
|
+
runPass,
|
|
5516
|
+
clock: new FakeClock(),
|
|
5517
|
+
monotonicNow: () => clockState.now,
|
|
5518
|
+
createWatcher: () => watcher,
|
|
5519
|
+
sleep: sleep.sleep,
|
|
5520
|
+
onShutdownSignal: (handler) => {
|
|
5521
|
+
triggerShutdown = handler;
|
|
5522
|
+
return () => { };
|
|
5523
|
+
},
|
|
5524
|
+
});
|
|
5525
|
+
await flushLoopMicrotasks();
|
|
5526
|
+
// Startup always forces the first full reconcile, and starts the interval.
|
|
5527
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
5528
|
+
runPass.mockClear();
|
|
5529
|
+
clockState.now = 999;
|
|
5530
|
+
sleep.tick();
|
|
5531
|
+
await flushLoopMicrotasks();
|
|
5532
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullPullArgv]);
|
|
5533
|
+
runPass.mockClear();
|
|
5534
|
+
clockState.now = 1_000;
|
|
5535
|
+
sleep.tick();
|
|
5536
|
+
await flushLoopMicrotasks();
|
|
5537
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
5538
|
+
triggerShutdown();
|
|
5539
|
+
await loop;
|
|
5540
|
+
}
|
|
5541
|
+
finally {
|
|
5542
|
+
if (saved === undefined)
|
|
5543
|
+
delete process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
5544
|
+
else
|
|
5545
|
+
process.env.HQ_SYNC_FULL_RECONCILE_MS = saved;
|
|
5546
|
+
}
|
|
5547
|
+
});
|
|
5548
|
+
it("does not let a full-reconcile interval suppress event-push activation", async () => {
|
|
5549
|
+
const saved = process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
5550
|
+
process.env.HQ_SYNC_FULL_RECONCILE_MS = "10800000";
|
|
5551
|
+
try {
|
|
5552
|
+
const watcher = makeBatchWatcherStub();
|
|
5553
|
+
const sleep = makeSteppableSleep();
|
|
5554
|
+
let triggerShutdown = () => { };
|
|
5555
|
+
// A numeric partial result does not activate event-push surfaces, so the
|
|
5556
|
+
// next tick must force another full pass even though three hours have not elapsed.
|
|
5557
|
+
const runPass = vi.fn().mockResolvedValue(PARTIAL_SYNC_EXIT);
|
|
5558
|
+
const loop = runRunnerWithLoop(watchArgv, {
|
|
5559
|
+
runPass,
|
|
5560
|
+
clock: new FakeClock(),
|
|
5561
|
+
monotonicNow: () => 0,
|
|
5562
|
+
createWatcher: () => watcher,
|
|
5563
|
+
sleep: sleep.sleep,
|
|
5564
|
+
onShutdownSignal: (handler) => {
|
|
5565
|
+
triggerShutdown = handler;
|
|
5566
|
+
return () => { };
|
|
5567
|
+
},
|
|
5568
|
+
});
|
|
5569
|
+
await flushLoopMicrotasks();
|
|
5570
|
+
runPass.mockClear();
|
|
5571
|
+
sleep.tick();
|
|
5572
|
+
await flushLoopMicrotasks();
|
|
5573
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
5574
|
+
triggerShutdown();
|
|
5575
|
+
await loop;
|
|
5576
|
+
}
|
|
5577
|
+
finally {
|
|
5578
|
+
if (saved === undefined)
|
|
5579
|
+
delete process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
5580
|
+
else
|
|
5581
|
+
process.env.HQ_SYNC_FULL_RECONCILE_MS = saved;
|
|
5582
|
+
}
|
|
5583
|
+
});
|
|
5328
5584
|
it("overflow with known dropped-route hints fans out per touched route instead of a full reconcile", async () => {
|
|
5329
5585
|
const watcher = makeBatchWatcherStub();
|
|
5330
5586
|
const sleep = makeSteppableSleep();
|
|
@@ -5410,7 +5666,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
5410
5666
|
process.env.HQ_SYNC_EVENT_BATCH_LIMIT = saved;
|
|
5411
5667
|
}
|
|
5412
5668
|
});
|
|
5413
|
-
it("a batch under the default
|
|
5669
|
+
it("a 5,000-path batch under the shipped default stays on the targeted path", async () => {
|
|
5414
5670
|
const watcher = makeBatchWatcherStub();
|
|
5415
5671
|
const sleep = makeSteppableSleep();
|
|
5416
5672
|
let triggerShutdown = () => { };
|
|
@@ -5428,33 +5684,67 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
|
|
|
5428
5684
|
await flushLoopMicrotasks();
|
|
5429
5685
|
runPass.mockClear();
|
|
5430
5686
|
watcher.emit("companies/indigo/a.md", {
|
|
5431
|
-
paths: new Map([
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
]),
|
|
5687
|
+
paths: new Map(Array.from({ length: 5_000 }, (_, index) => [
|
|
5688
|
+
`/tmp/hq/companies/indigo/${index}.md`,
|
|
5689
|
+
`companies/indigo/${index}.md`,
|
|
5690
|
+
])),
|
|
5436
5691
|
});
|
|
5437
5692
|
sleep.tick();
|
|
5438
5693
|
await flushLoopMicrotasks();
|
|
5439
5694
|
const calls = runPass.mock.calls.map((call) => call[0]);
|
|
5440
5695
|
expect(calls).not.toContainEqual(fullArgv);
|
|
5441
|
-
expect(calls
|
|
5696
|
+
expect(calls).toHaveLength(2);
|
|
5697
|
+
expect(calls[0]?.slice(0, 4)).toEqual([
|
|
5442
5698
|
"--company",
|
|
5443
5699
|
"indigo",
|
|
5444
5700
|
"--direction",
|
|
5445
5701
|
"push",
|
|
5446
|
-
"--scope-path",
|
|
5447
|
-
"a.md",
|
|
5448
|
-
"--scope-path",
|
|
5449
|
-
"b.md",
|
|
5450
|
-
"--scope-path",
|
|
5451
|
-
"c.md",
|
|
5452
|
-
"--hq-root",
|
|
5453
|
-
"/tmp/hq",
|
|
5454
5702
|
]);
|
|
5703
|
+
expect(calls[0]?.filter((arg) => arg === "--scope-path")).toHaveLength(5_000);
|
|
5704
|
+
expect(calls[1]?.filter((arg) => arg === "--scope-path")).toHaveLength(5_000);
|
|
5455
5705
|
triggerShutdown();
|
|
5456
5706
|
await loop;
|
|
5457
5707
|
});
|
|
5708
|
+
it("a batch above the shipped 10,000-path default forces a full reconcile before the interval", async () => {
|
|
5709
|
+
const saved = process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
5710
|
+
process.env.HQ_SYNC_FULL_RECONCILE_MS = "10800000";
|
|
5711
|
+
try {
|
|
5712
|
+
const watcher = makeBatchWatcherStub();
|
|
5713
|
+
const sleep = makeSteppableSleep();
|
|
5714
|
+
let triggerShutdown = () => { };
|
|
5715
|
+
const runPass = vi.fn().mockResolvedValue(completedPassOutcome());
|
|
5716
|
+
const loop = runRunnerWithLoop(watchArgv, {
|
|
5717
|
+
runPass,
|
|
5718
|
+
clock: new FakeClock(),
|
|
5719
|
+
monotonicNow: () => 0,
|
|
5720
|
+
createWatcher: () => watcher,
|
|
5721
|
+
sleep: sleep.sleep,
|
|
5722
|
+
onShutdownSignal: (handler) => {
|
|
5723
|
+
triggerShutdown = handler;
|
|
5724
|
+
return () => { };
|
|
5725
|
+
},
|
|
5726
|
+
});
|
|
5727
|
+
await flushLoopMicrotasks();
|
|
5728
|
+
runPass.mockClear();
|
|
5729
|
+
watcher.emit("companies/indigo/a.md", {
|
|
5730
|
+
paths: new Map(Array.from({ length: 10_001 }, (_, index) => [
|
|
5731
|
+
`/tmp/hq/companies/indigo/${index}.md`,
|
|
5732
|
+
`companies/indigo/${index}.md`,
|
|
5733
|
+
])),
|
|
5734
|
+
});
|
|
5735
|
+
sleep.tick();
|
|
5736
|
+
await flushLoopMicrotasks();
|
|
5737
|
+
expect(runPass.mock.calls.map((call) => call[0])).toEqual([fullArgv]);
|
|
5738
|
+
triggerShutdown();
|
|
5739
|
+
await loop;
|
|
5740
|
+
}
|
|
5741
|
+
finally {
|
|
5742
|
+
if (saved === undefined)
|
|
5743
|
+
delete process.env.HQ_SYNC_FULL_RECONCILE_MS;
|
|
5744
|
+
else
|
|
5745
|
+
process.env.HQ_SYNC_FULL_RECONCILE_MS = saved;
|
|
5746
|
+
}
|
|
5747
|
+
});
|
|
5458
5748
|
it("a throwing targeted pass falls back to one full reconcile instead of dropping the change", async () => {
|
|
5459
5749
|
const runPass = vi.fn(async (argv) => {
|
|
5460
5750
|
if (argv.includes("pull") && argv.includes("--scope-path")) {
|