@indigoai-us/hq-cloud 6.14.49 → 6.15.0

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.
Files changed (126) hide show
  1. package/.github/workflows/unreleased-commits-nag.yml +256 -0
  2. package/dist/bin/sync-runner-watch-loop.d.ts +12 -0
  3. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  4. package/dist/bin/sync-runner-watch-loop.js +153 -25
  5. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  6. package/dist/bin/sync-runner-watch-routes.d.ts +17 -0
  7. package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
  8. package/dist/bin/sync-runner-watch-routes.js +11 -0
  9. package/dist/bin/sync-runner-watch-routes.js.map +1 -1
  10. package/dist/bin/sync-runner-watch-routes.test.d.ts +2 -0
  11. package/dist/bin/sync-runner-watch-routes.test.d.ts.map +1 -0
  12. package/dist/bin/sync-runner-watch-routes.test.js +51 -0
  13. package/dist/bin/sync-runner-watch-routes.test.js.map +1 -0
  14. package/dist/bin/sync-runner.d.ts +11 -12
  15. package/dist/bin/sync-runner.d.ts.map +1 -1
  16. package/dist/bin/sync-runner.js.map +1 -1
  17. package/dist/bin/sync-runner.test.js +173 -52
  18. package/dist/bin/sync-runner.test.js.map +1 -1
  19. package/dist/cli/reindex-knowledge.d.ts +7 -0
  20. package/dist/cli/reindex-knowledge.d.ts.map +1 -0
  21. package/dist/cli/reindex-knowledge.js +389 -0
  22. package/dist/cli/reindex-knowledge.js.map +1 -0
  23. package/dist/cli/reindex-knowledge.test.d.ts +12 -0
  24. package/dist/cli/reindex-knowledge.test.d.ts.map +1 -0
  25. package/dist/cli/reindex-knowledge.test.js +260 -0
  26. package/dist/cli/reindex-knowledge.test.js.map +1 -0
  27. package/dist/cli/reindex.d.ts.map +1 -1
  28. package/dist/cli/reindex.js +8 -0
  29. package/dist/cli/reindex.js.map +1 -1
  30. package/dist/cli/rescue-classify-ordering.test.js +8 -2
  31. package/dist/cli/rescue-classify-ordering.test.js.map +1 -1
  32. package/dist/cli/rescue-core.d.ts +85 -0
  33. package/dist/cli/rescue-core.d.ts.map +1 -1
  34. package/dist/cli/rescue-core.js +390 -1
  35. package/dist/cli/rescue-core.js.map +1 -1
  36. package/dist/cli/rescue-settings-reconcile.test.d.ts +2 -0
  37. package/dist/cli/rescue-settings-reconcile.test.d.ts.map +1 -0
  38. package/dist/cli/rescue-settings-reconcile.test.js +523 -0
  39. package/dist/cli/rescue-settings-reconcile.test.js.map +1 -0
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/journal.d.ts.map +1 -1
  45. package/dist/journal.js +81 -51
  46. package/dist/journal.js.map +1 -1
  47. package/dist/journal.test.js +11 -19
  48. package/dist/journal.test.js.map +1 -1
  49. package/dist/sync/event-sync.d.ts +8 -25
  50. package/dist/sync/event-sync.d.ts.map +1 -1
  51. package/dist/sync/event-sync.js +23 -48
  52. package/dist/sync/event-sync.js.map +1 -1
  53. package/dist/sync/event-sync.test.js +6 -32
  54. package/dist/sync/event-sync.test.js.map +1 -1
  55. package/dist/sync/feature-flags.d.ts +20 -117
  56. package/dist/sync/feature-flags.d.ts.map +1 -1
  57. package/dist/sync/feature-flags.js +24 -147
  58. package/dist/sync/feature-flags.js.map +1 -1
  59. package/dist/sync/feature-flags.test.js +15 -61
  60. package/dist/sync/feature-flags.test.js.map +1 -1
  61. package/dist/sync/index.d.ts +1 -1
  62. package/dist/sync/index.d.ts.map +1 -1
  63. package/dist/sync/index.js +1 -1
  64. package/dist/sync/index.js.map +1 -1
  65. package/dist/sync/lease-client.d.ts +67 -0
  66. package/dist/sync/lease-client.d.ts.map +1 -0
  67. package/dist/sync/lease-client.js +158 -0
  68. package/dist/sync/lease-client.js.map +1 -0
  69. package/dist/sync/lease-client.test.d.ts +2 -0
  70. package/dist/sync/lease-client.test.d.ts.map +1 -0
  71. package/dist/sync/lease-client.test.js +119 -0
  72. package/dist/sync/lease-client.test.js.map +1 -0
  73. package/dist/sync/push-receiver.d.ts +8 -19
  74. package/dist/sync/push-receiver.d.ts.map +1 -1
  75. package/dist/sync/push-receiver.js +6 -50
  76. package/dist/sync/push-receiver.js.map +1 -1
  77. package/dist/sync/push-receiver.test.js +6 -57
  78. package/dist/sync/push-receiver.test.js.map +1 -1
  79. package/dist/sync/realtime-rollout.d.ts +85 -0
  80. package/dist/sync/realtime-rollout.d.ts.map +1 -0
  81. package/dist/sync/realtime-rollout.js +206 -0
  82. package/dist/sync/realtime-rollout.js.map +1 -0
  83. package/dist/sync/realtime-rollout.test.d.ts +2 -0
  84. package/dist/sync/realtime-rollout.test.d.ts.map +1 -0
  85. package/dist/sync/realtime-rollout.test.js +71 -0
  86. package/dist/sync/realtime-rollout.test.js.map +1 -0
  87. package/dist/sync/state-store.d.ts +40 -0
  88. package/dist/sync/state-store.d.ts.map +1 -0
  89. package/dist/sync/state-store.js +680 -0
  90. package/dist/sync/state-store.js.map +1 -0
  91. package/dist/sync/state-store.test.d.ts +2 -0
  92. package/dist/sync/state-store.test.d.ts.map +1 -0
  93. package/dist/sync/state-store.test.js +159 -0
  94. package/dist/sync/state-store.test.js.map +1 -0
  95. package/dist/types.d.ts +12 -0
  96. package/dist/types.d.ts.map +1 -1
  97. package/package.json +1 -1
  98. package/src/bin/sync-runner-watch-loop.ts +176 -32
  99. package/src/bin/sync-runner-watch-routes.test.ts +71 -0
  100. package/src/bin/sync-runner-watch-routes.ts +27 -0
  101. package/src/bin/sync-runner.test.ts +188 -53
  102. package/src/bin/sync-runner.ts +11 -12
  103. package/src/cli/reindex-knowledge.test.ts +307 -0
  104. package/src/cli/reindex-knowledge.ts +450 -0
  105. package/src/cli/reindex.ts +9 -0
  106. package/src/cli/rescue-classify-ordering.test.ts +9 -3
  107. package/src/cli/rescue-core.ts +459 -1
  108. package/src/cli/rescue-settings-reconcile.test.ts +637 -0
  109. package/src/index.ts +2 -4
  110. package/src/journal.test.ts +19 -21
  111. package/src/journal.ts +87 -54
  112. package/src/sync/event-sync.test.ts +9 -44
  113. package/src/sync/event-sync.ts +26 -52
  114. package/src/sync/feature-flags.test.ts +22 -72
  115. package/src/sync/feature-flags.ts +32 -199
  116. package/src/sync/index.ts +2 -4
  117. package/src/sync/lease-client.test.ts +128 -0
  118. package/src/sync/lease-client.ts +207 -0
  119. package/src/sync/push-receiver.test.ts +6 -58
  120. package/src/sync/push-receiver.ts +9 -74
  121. package/src/sync/realtime-rollout.test.ts +86 -0
  122. package/src/sync/realtime-rollout.ts +262 -0
  123. package/src/sync/state-store.test.ts +194 -0
  124. package/src/sync/state-store.ts +727 -0
  125. package/src/types.ts +14 -0
  126. package/test/e2e/sync/transient-company-leg.test.ts +8 -4
@@ -42,6 +42,7 @@ import {
42
42
  } from "../watcher.js";
43
43
  import {
44
44
  adaptivePollMs,
45
+ advanceMonotonicDeadline,
45
46
  journalEntriesForWatcherDelete,
46
47
  shouldQuarantineWatcherDeleteIntents,
47
48
  } from "./sync-runner-watch-loop.js";
@@ -4205,6 +4206,22 @@ describe("adaptivePollMs (whole-box CPU backoff)", () => {
4205
4206
  });
4206
4207
  });
4207
4208
 
4209
+ describe("advanceMonotonicDeadline", () => {
4210
+ it("runs one immediate catch-up after missed slots, then resumes at the first future slot", () => {
4211
+ // A pass due at t=0 took through t=1,800 with a 600 ms cadence. It does
4212
+ // not replay the three missed slots; exactly one catch-up is immediate and
4213
+ // the following regular deadline is t=2,400.
4214
+ expect(advanceMonotonicDeadline(0, 1_800, 600)).toEqual({
4215
+ nextDueAt: 2_400,
4216
+ delayMs: 0,
4217
+ });
4218
+ expect(advanceMonotonicDeadline(2_400, 1_800, 600)).toEqual({
4219
+ nextDueAt: 3_000,
4220
+ delayMs: 1_200,
4221
+ });
4222
+ });
4223
+ });
4224
+
4208
4225
  describe("runRunnerWithLoop — adaptive poll interval (no --poll-remote-ms)", () => {
4209
4226
  // Capture the ms handed to sleep on the FIRST poll, then hang so the loop
4210
4227
  // parks until shutdown (mirrors the never-resolving sleep the other loop
@@ -4220,8 +4237,14 @@ describe("runRunnerWithLoop — adaptive poll interval (no --poll-remote-ms)", (
4220
4237
  };
4221
4238
  }
4222
4239
 
4223
- it("sleeps the floor interval when the box is idle", async () => {
4240
+ it("waits only the bounded remainder of the idle cadence slot", async () => {
4224
4241
  const { calls, sleep } = makeCapturingSleep();
4242
+ const monotonicNow = vi
4243
+ .fn<() => number>()
4244
+ .mockReturnValueOnce(1_000)
4245
+ // A 20 ms pass consumes part of this 60 s slot. The scheduler must
4246
+ // retain the original deadline rather than slewing a new 60 s wait.
4247
+ .mockReturnValueOnce(1_020);
4225
4248
  let triggerShutdown = () => {};
4226
4249
  const loop = runRunnerWithLoop(
4227
4250
  ["--companies", "--watch", "--hq-root", "/tmp/hq"],
@@ -4229,6 +4252,7 @@ describe("runRunnerWithLoop — adaptive poll interval (no --poll-remote-ms)", (
4229
4252
  runPass: vi.fn().mockResolvedValue(0),
4230
4253
  sleep,
4231
4254
  sampleLoadAvg: () => 0,
4255
+ monotonicNow,
4232
4256
  onShutdownSignal: (handler) => {
4233
4257
  triggerShutdown = handler;
4234
4258
  return () => {};
@@ -4236,13 +4260,19 @@ describe("runRunnerWithLoop — adaptive poll interval (no --poll-remote-ms)", (
4236
4260
  },
4237
4261
  );
4238
4262
  await flushLoopMicrotasks();
4239
- expect(calls[0]).toBe(60_000);
4263
+ expect(calls[0]).toBe(59_980);
4264
+ expect(calls[0]).toBeGreaterThan(0);
4265
+ expect(calls[0]).toBeLessThanOrEqual(60_000);
4240
4266
  triggerShutdown();
4241
4267
  await loop;
4242
4268
  });
4243
4269
 
4244
- it("backs off to the ceiling interval when the box is fully loaded", async () => {
4270
+ it("waits only the bounded remainder of the saturated cadence slot", async () => {
4245
4271
  const { calls, sleep } = makeCapturingSleep();
4272
+ const monotonicNow = vi
4273
+ .fn<() => number>()
4274
+ .mockReturnValueOnce(1_000)
4275
+ .mockReturnValueOnce(1_075);
4246
4276
  let triggerShutdown = () => {};
4247
4277
  const loop = runRunnerWithLoop(
4248
4278
  ["--companies", "--watch", "--hq-root", "/tmp/hq"],
@@ -4250,6 +4280,7 @@ describe("runRunnerWithLoop — adaptive poll interval (no --poll-remote-ms)", (
4250
4280
  runPass: vi.fn().mockResolvedValue(0),
4251
4281
  sleep,
4252
4282
  sampleLoadAvg: () => 1_000, // far above any core count → clamps to ceil
4283
+ monotonicNow,
4253
4284
  onShutdownSignal: (handler) => {
4254
4285
  triggerShutdown = handler;
4255
4286
  return () => {};
@@ -4257,14 +4288,20 @@ describe("runRunnerWithLoop — adaptive poll interval (no --poll-remote-ms)", (
4257
4288
  },
4258
4289
  );
4259
4290
  await flushLoopMicrotasks();
4260
- expect(calls[0]).toBe(600_000);
4291
+ expect(calls[0]).toBe(599_925);
4292
+ expect(calls[0]).toBeGreaterThan(0);
4293
+ expect(calls[0]).toBeLessThanOrEqual(600_000);
4261
4294
  triggerShutdown();
4262
4295
  await loop;
4263
4296
  });
4264
4297
 
4265
- it("an explicit --poll-remote-ms pins a fixed interval and ignores load", async () => {
4298
+ it("uses the bounded remainder of an explicit cadence slot and ignores load", async () => {
4266
4299
  const { calls, sleep } = makeCapturingSleep();
4267
4300
  const sampleLoadAvg = vi.fn(() => 1_000);
4301
+ const monotonicNow = vi
4302
+ .fn<() => number>()
4303
+ .mockReturnValueOnce(1_000)
4304
+ .mockReturnValueOnce(1_123);
4268
4305
  let triggerShutdown = () => {};
4269
4306
  const loop = runRunnerWithLoop(
4270
4307
  ["--companies", "--watch", "--poll-remote-ms", "123456", "--hq-root", "/tmp/hq"],
@@ -4272,6 +4309,7 @@ describe("runRunnerWithLoop — adaptive poll interval (no --poll-remote-ms)", (
4272
4309
  runPass: vi.fn().mockResolvedValue(0),
4273
4310
  sleep,
4274
4311
  sampleLoadAvg,
4312
+ monotonicNow,
4275
4313
  onShutdownSignal: (handler) => {
4276
4314
  triggerShutdown = handler;
4277
4315
  return () => {};
@@ -4279,7 +4317,9 @@ describe("runRunnerWithLoop — adaptive poll interval (no --poll-remote-ms)", (
4279
4317
  },
4280
4318
  );
4281
4319
  await flushLoopMicrotasks();
4282
- expect(calls[0]).toBe(123456);
4320
+ expect(calls[0]).toBe(123_333);
4321
+ expect(calls[0]).toBeGreaterThan(0);
4322
+ expect(calls[0]).toBeLessThanOrEqual(123_456);
4283
4323
  expect(sampleLoadAvg).not.toHaveBeenCalled();
4284
4324
  triggerShutdown();
4285
4325
  await loop;
@@ -4317,19 +4357,32 @@ describe("runRunnerWithLoop — event-push wiring", () => {
4317
4357
  expect(watcher.disposed).toBe(true);
4318
4358
  });
4319
4359
 
4320
- it("trigger-on-change: a changed signal accumulates without an immediate push", async () => {
4360
+ it("U07: watcher interrupts a fake 600 s wait and drains through the guarded scheduler", async () => {
4321
4361
  const watcher = makeWatcherStub();
4322
4362
  const clock = new FakeClock();
4323
4363
  let triggerShutdown = () => {};
4324
4364
  const runPass = vi.fn().mockResolvedValue(0);
4365
+ const sleepCalls: number[] = [];
4325
4366
 
4326
4367
  const loop = runRunnerWithLoop(
4327
- ["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"],
4368
+ [
4369
+ "--companies",
4370
+ "--watch",
4371
+ "--event-push",
4372
+ "--poll-remote-ms",
4373
+ "600000",
4374
+ "--hq-root",
4375
+ "/tmp/hq",
4376
+ ],
4328
4377
  {
4329
4378
  runPass,
4330
4379
  clock,
4331
4380
  createWatcher: () => watcher,
4332
- sleep: () => new Promise<void>(() => {}),
4381
+ monotonicNow: () => 0,
4382
+ sleep: (ms) => {
4383
+ sleepCalls.push(ms);
4384
+ return new Promise<void>(() => {});
4385
+ },
4333
4386
  onShutdownSignal: (handler) => {
4334
4387
  triggerShutdown = handler;
4335
4388
  return () => {};
@@ -4337,24 +4390,20 @@ describe("runRunnerWithLoop — event-push wiring", () => {
4337
4390
  },
4338
4391
  );
4339
4392
 
4340
- // Drain the initial poll pass.
4341
- await Promise.resolve();
4342
- await Promise.resolve();
4343
- runPass.mockClear();
4393
+ await flushLoopMicrotasks();
4394
+ expect(sleepCalls).toEqual([600_000]);
4344
4395
 
4345
- // Fire a change for an indigo file; advance the (zero) driver window.
4396
+ // The fake 600-second timer remains unresolved. The watcher wake must
4397
+ // nonetheless start the next pass immediately through runGuarded.
4346
4398
  watcher.emit("companies/indigo/board.json");
4347
- clock.advance(0);
4348
- await Promise.resolve();
4349
- await Promise.resolve();
4350
-
4351
- expect(runPass).not.toHaveBeenCalled();
4399
+ await flushLoopMicrotasks();
4400
+ expect(runPass).toHaveBeenCalledTimes(2);
4352
4401
 
4353
4402
  triggerShutdown();
4354
4403
  await loop;
4355
4404
  });
4356
4405
 
4357
- it("bare signal (no path) accumulates without an immediate push", async () => {
4406
+ it("bare watcher signal interrupts the wait and schedules a full reconcile", async () => {
4358
4407
  const watcher = makeWatcherStub();
4359
4408
  const clock = new FakeClock();
4360
4409
  let triggerShutdown = () => {};
@@ -4380,10 +4429,9 @@ describe("runRunnerWithLoop — event-push wiring", () => {
4380
4429
 
4381
4430
  watcher.emit(); // no path — TreeWatcher's bare signal shape
4382
4431
  clock.advance(0);
4383
- await Promise.resolve();
4384
- await Promise.resolve();
4432
+ await flushLoopMicrotasks();
4385
4433
 
4386
- expect(runPass).not.toHaveBeenCalled();
4434
+ expect(runPass).toHaveBeenCalledTimes(1);
4387
4435
 
4388
4436
  triggerShutdown();
4389
4437
  await loop;
@@ -4453,7 +4501,61 @@ describe("runRunnerWithLoop — event-push wiring", () => {
4453
4501
  await loop;
4454
4502
  });
4455
4503
 
4456
- it("F05: guard-held watcher changes stay accumulated until a poll drain", async () => {
4504
+ it("U07: retains a watcher event raised before the next wait registers, without guarded overlap", async () => {
4505
+ const watcher = makeWatcherStub();
4506
+ let triggerShutdown = () => {};
4507
+ let releaseFirstPass: () => void = () => {};
4508
+ const firstPassGate = new Promise<void>((resolve) => {
4509
+ releaseFirstPass = resolve;
4510
+ });
4511
+ const spans: Array<{ enteredAt: bigint; exitedAt: bigint }> = [];
4512
+ let passes = 0;
4513
+ const runPass = vi.fn().mockImplementation(async () => {
4514
+ const span = { enteredAt: process.hrtime.bigint(), exitedAt: 0n };
4515
+ spans.push(span);
4516
+ passes += 1;
4517
+ if (passes === 1) await firstPassGate;
4518
+ span.exitedAt = process.hrtime.bigint();
4519
+ return 0;
4520
+ });
4521
+
4522
+ const loop = runRunnerWithLoop(
4523
+ [
4524
+ "--companies",
4525
+ "--watch",
4526
+ "--event-push",
4527
+ "--poll-remote-ms",
4528
+ "600000",
4529
+ "--hq-root",
4530
+ "/tmp/hq",
4531
+ ],
4532
+ {
4533
+ runPass,
4534
+ createWatcher: () => watcher,
4535
+ sleep: () => new Promise<void>(() => {}),
4536
+ onShutdownSignal: (handler) => {
4537
+ triggerShutdown = handler;
4538
+ return () => {};
4539
+ },
4540
+ },
4541
+ );
4542
+
4543
+ await flushLoopMicrotasks();
4544
+ expect(spans).toHaveLength(1);
4545
+ // This event arrives while the initial guarded pass owns the scheduler,
4546
+ // before its next sleep/wait has been registered.
4547
+ watcher.emit("companies/indigo/pre-registration.md");
4548
+ releaseFirstPass();
4549
+ await flushLoopMicrotasks();
4550
+
4551
+ expect(spans).toHaveLength(2);
4552
+ expect(spans[1]!.enteredAt >= spans[0]!.exitedAt).toBe(true);
4553
+
4554
+ triggerShutdown();
4555
+ await loop;
4556
+ });
4557
+
4558
+ it("F05: guard-held watcher changes stay accumulated, then drain immediately after the guarded pass", async () => {
4457
4559
  const watcher = makeWatcherStub();
4458
4560
  const clock = new FakeClock();
4459
4561
  let triggerShutdown = () => {};
@@ -4551,8 +4653,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
4551
4653
 
4552
4654
  // Step the poll interval → a second poll pass runs (safety net cadence).
4553
4655
  poll.resolve?.();
4554
- await Promise.resolve();
4555
- await Promise.resolve();
4656
+ await flushLoopMicrotasks();
4556
4657
  expect(passes).toBeGreaterThanOrEqual(2);
4557
4658
 
4558
4659
  triggerShutdown();
@@ -4876,7 +4977,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4876
4977
  };
4877
4978
  }
4878
4979
 
4879
- it("startup runs one full reconcile, then watcher edits wait for the poll drain", async () => {
4980
+ it("startup runs one full reconcile, then watcher edits interrupt the pending wait", async () => {
4880
4981
  const runPass = vi.fn().mockResolvedValue(0);
4881
4982
  const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
4882
4983
  await flushLoopMicrotasks();
@@ -4886,10 +4987,6 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4886
4987
 
4887
4988
  watcher.emit("companies/indigo/knowledge/a.md");
4888
4989
  await flushLoopMicrotasks();
4889
- expect(runPass).not.toHaveBeenCalled();
4890
-
4891
- tick();
4892
- await flushLoopMicrotasks();
4893
4990
 
4894
4991
  expect(runPass.mock.calls.map((call) => call[0])).toEqual([
4895
4992
  [
@@ -4942,6 +5039,28 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
4942
5039
  await loop;
4943
5040
  });
4944
5041
 
5042
+ it("U08: --companies --skip-personal drops a personal watcher edit before it queues work", async () => {
5043
+ const runPass = vi.fn().mockResolvedValue(0);
5044
+ const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
5045
+ ...watchArgv,
5046
+ "--skip-personal",
5047
+ ]);
5048
+ await flushLoopMicrotasks();
5049
+ runPass.mockClear();
5050
+
5051
+ watcher.emit("knowledge/private.md");
5052
+ await flushLoopMicrotasks();
5053
+
5054
+ // The watcher event is rejected before the scheduler is interrupted:
5055
+ // no route-specific pass is queued and therefore no runner/network work
5056
+ // is reachable for a disabled personal scope.
5057
+ expect(runPass).not.toHaveBeenCalled();
5058
+
5059
+ shutdown();
5060
+ tick();
5061
+ await loop;
5062
+ });
5063
+
4945
5064
  it("drains an unlink path through the same scoped tombstone push path", async () => {
4946
5065
  const runPass = vi.fn().mockResolvedValue(0);
4947
5066
  const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
@@ -6215,7 +6334,7 @@ function makeBatchWatcherStub(): BatchWatcherStub {
6215
6334
  }
6216
6335
 
6217
6336
  describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", () => {
6218
- const ENROLLED_CLAIMS = { email: "hassaan@getindigo.ai" };
6337
+ const EMAIL_CLAIMS = { email: "hassaan@getindigo.ai" };
6219
6338
  const UNENROLLED_CLAIMS = { email: "someone@getindigo.ai" };
6220
6339
 
6221
6340
  function runLoop(opts: {
@@ -6271,7 +6390,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
6271
6390
  for (let i = 0; i < n; i++) await Promise.resolve();
6272
6391
  }
6273
6392
 
6274
- it("gate OFF (unenrolled email): the event-sync seam is never consulted", async () => {
6393
+ it("does not use an unenrolled email as a client-side denial", async () => {
6275
6394
  const startEventSync = vi.fn().mockResolvedValue(null);
6276
6395
  const { loop, shutdown } = runLoop({
6277
6396
  claims: UNENROLLED_CLAIMS,
@@ -6280,22 +6399,22 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
6280
6399
  await microtasks();
6281
6400
  shutdown();
6282
6401
  await loop;
6283
- expect(startEventSync).not.toHaveBeenCalled();
6402
+ expect(startEventSync).toHaveBeenCalledTimes(1);
6284
6403
  });
6285
6404
 
6286
- it("gate OFF (no cached claims): the event-sync seam is never consulted", async () => {
6405
+ it("does not use missing cached claims as a client-side denial", async () => {
6287
6406
  const startEventSync = vi.fn().mockResolvedValue(null);
6288
6407
  const { loop, shutdown } = runLoop({ claims: null, startEventSync });
6289
6408
  await microtasks();
6290
6409
  shutdown();
6291
6410
  await loop;
6292
- expect(startEventSync).not.toHaveBeenCalled();
6411
+ expect(startEventSync).toHaveBeenCalledTimes(1);
6293
6412
  });
6294
6413
 
6295
- it("gate ON (enrolled email): wiring receives root, api url, auth + a sync bridge", async () => {
6414
+ it("asks the server for authority before wiring publish and receive", async () => {
6296
6415
  const startEventSync = vi.fn().mockResolvedValue(null);
6297
6416
  const { loop, shutdown } = runLoop({
6298
- claims: ENROLLED_CLAIMS,
6417
+ claims: EMAIL_CLAIMS,
6299
6418
  startEventSync,
6300
6419
  });
6301
6420
  await microtasks();
@@ -6319,9 +6438,18 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
6319
6438
  ownDeviceId: "dev-test",
6320
6439
  dispose: vi.fn().mockResolvedValue(undefined),
6321
6440
  });
6322
- const runPass = vi.fn().mockResolvedValue(0);
6323
- const { loop, watcher, clock, tick, shutdown } = runLoop({
6324
- claims: ENROLLED_CLAIMS,
6441
+ let finishTargetedPush: (outcome: number) => void = () => {};
6442
+ const targetedPush = new Promise<number>((resolve) => {
6443
+ finishTargetedPush = resolve;
6444
+ });
6445
+ const runPass = vi
6446
+ .fn()
6447
+ .mockResolvedValueOnce(0)
6448
+ .mockImplementation((passArgv: string[]) =>
6449
+ passArgv.includes("--company") ? targetedPush : Promise.resolve(0),
6450
+ );
6451
+ const { loop, watcher, clock, shutdown } = runLoop({
6452
+ claims: EMAIL_CLAIMS,
6325
6453
  startEventSync,
6326
6454
  runPass,
6327
6455
  });
@@ -6334,14 +6462,21 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
6334
6462
  watcher.emit("companies/indigo/a.md", batch);
6335
6463
  clock.advance(0);
6336
6464
  await microtasks();
6337
- expect(runPass).not.toHaveBeenCalled();
6465
+
6466
+ // U07 wakes the scheduler immediately. It may start the scoped push, but
6467
+ // it must not announce the batch until that push has succeeded.
6468
+ expect(runPass).toHaveBeenCalledWith(expect.arrayContaining([
6469
+ "--company",
6470
+ "indigo",
6471
+ "--direction",
6472
+ "push",
6473
+ ]));
6338
6474
  expect(publishBatch).not.toHaveBeenCalled();
6339
6475
 
6340
- tick();
6476
+ finishTargetedPush(0);
6341
6477
  await microtasks();
6342
6478
 
6343
6479
  // Scoped push drained and succeeded → batch published.
6344
- expect(runPass).toHaveBeenCalled();
6345
6480
  expect(publishBatch).toHaveBeenCalledTimes(1);
6346
6481
  expect([...publishBatch.mock.calls[0][0].paths.values()]).toEqual([
6347
6482
  "companies/indigo/a.md",
@@ -6365,7 +6500,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
6365
6500
  .mockResolvedValueOnce(0)
6366
6501
  .mockResolvedValue(1);
6367
6502
  const { loop, watcher, clock, tick, shutdown } = runLoop({
6368
- claims: ENROLLED_CLAIMS,
6503
+ claims: EMAIL_CLAIMS,
6369
6504
  startEventSync,
6370
6505
  runPass,
6371
6506
  });
@@ -6402,7 +6537,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
6402
6537
  });
6403
6538
  const runPass = vi.fn().mockResolvedValue(0);
6404
6539
  const { loop, watcher, clock, tick, shutdown } = runLoop({
6405
- claims: ENROLLED_CLAIMS,
6540
+ claims: EMAIL_CLAIMS,
6406
6541
  startEventSync,
6407
6542
  runPass,
6408
6543
  });
@@ -6459,7 +6594,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
6459
6594
  })
6460
6595
  .mockResolvedValue(0);
6461
6596
  const { loop, watcher, tick, shutdown } = runLoop({
6462
- claims: ENROLLED_CLAIMS,
6597
+ claims: EMAIL_CLAIMS,
6463
6598
  startEventSync,
6464
6599
  runPass,
6465
6600
  });
@@ -6502,7 +6637,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
6502
6637
  })
6503
6638
  .mockResolvedValue(0);
6504
6639
  const { loop, watcher, tick, shutdown } = runLoop({
6505
- claims: ENROLLED_CLAIMS,
6640
+ claims: EMAIL_CLAIMS,
6506
6641
  startEventSync,
6507
6642
  runPass,
6508
6643
  });
@@ -6533,7 +6668,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
6533
6668
  });
6534
6669
  const runPass = vi.fn().mockResolvedValue(0);
6535
6670
  const { loop, watcher, clock, tick, shutdown } = runLoop({
6536
- claims: ENROLLED_CLAIMS,
6671
+ claims: EMAIL_CLAIMS,
6537
6672
  startEventSync,
6538
6673
  runPass,
6539
6674
  });
@@ -6646,7 +6781,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
6646
6781
  triggerShutdown = handler;
6647
6782
  return () => {};
6648
6783
  },
6649
- getIdTokenClaims: () => ENROLLED_CLAIMS as never,
6784
+ getIdTokenClaims: () => EMAIL_CLAIMS as never,
6650
6785
  getAccessToken: async () => "jwt-test",
6651
6786
  startEventSync,
6652
6787
  },
@@ -6757,7 +6892,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
6757
6892
  dispose: vi.fn().mockResolvedValue(undefined),
6758
6893
  });
6759
6894
  const { loop, watcher, clock, tick, shutdown } = runLoop({
6760
- claims: ENROLLED_CLAIMS,
6895
+ claims: EMAIL_CLAIMS,
6761
6896
  startEventSync,
6762
6897
  });
6763
6898
  await microtasks();
@@ -6787,7 +6922,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
6787
6922
  dispose,
6788
6923
  });
6789
6924
  const { loop, shutdown } = runLoop({
6790
- claims: ENROLLED_CLAIMS,
6925
+ claims: EMAIL_CLAIMS,
6791
6926
  startEventSync,
6792
6927
  });
6793
6928
  await microtasks();
@@ -6802,7 +6937,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
6802
6937
  try {
6803
6938
  const startEventSync = vi.fn().mockResolvedValue(null);
6804
6939
  const { loop, shutdown } = runLoop({
6805
- claims: ENROLLED_CLAIMS,
6940
+ claims: EMAIL_CLAIMS,
6806
6941
  startEventSync,
6807
6942
  });
6808
6943
  await microtasks();
@@ -2010,21 +2010,14 @@ const isDirectInvocation = (() => {
2010
2010
  }
2011
2011
  })();
2012
2012
 
2013
- /**
2014
- * Auto-sync (Beta) watch loop. Re-runs the one-shot runner every
2015
- * `pollRemoteMs` until the process is killed (SIGTERM from the menubar's
2016
- * stop_daemon command) or until a pass returns a non-zero exit code (hard
2017
- * error worth surfacing to the operator). `setup-needed` and `auth-error`
2018
- * exit 0 today and so will retry — acceptable noise for the beta; deal with
2019
- * it via a richer return shape if it shows up in Sentry.
2020
- */
2021
2013
  /**
2022
2014
  * Test/event-driven seam (US-001).
2023
2015
  *
2024
- * `runRunnerWithLoop` performs an unbounded poll loop in production. To make
2025
- * the loop deterministically testable (US-003 wires the event-driven watcher
2026
- * into this same loop), the inter-pass sleep is injectable via `deps.sleep`.
2027
- * The default uses the host timer and preserves exact production behavior.
2016
+ * `runRunnerWithLoop` performs an unbounded, monotonic-cadence loop in
2017
+ * production. The initial pass is immediate; subsequent deadlines advance
2018
+ * from their prior deadline rather than pass completion. Watcher and receiver
2019
+ * signals interrupt the wait but still use the same guarded pass queue. To
2020
+ * test it deterministically, both waiting and monotonic time are injectable.
2028
2021
  *
2029
2022
  * A test (or US-003's wiring) injects a fake sleep that resolves immediately
2030
2023
  * and/or coordinates with the {@link WatchPushDriver} seam in `../watcher.js`,
@@ -2033,6 +2026,12 @@ const isDirectInvocation = (() => {
2033
2026
  export interface RunnerLoopDeps {
2034
2027
  /** Sleep `ms` between passes. Default: host setTimeout. */
2035
2028
  sleep?: (ms: number) => Promise<void>;
2029
+ /**
2030
+ * Monotonic milliseconds source for cadence deadlines. Defaults to
2031
+ * `performance.now()`; tests inject it to prove missed-deadline behavior
2032
+ * without depending on wall-clock time.
2033
+ */
2034
+ monotonicNow?: () => number;
2036
2035
  /**
2037
2036
  * 1-minute whole-box load average source for the adaptive remote-poll
2038
2037
  * backoff (used only when `--poll-remote-ms` is omitted). Defaults to