@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
@@ -13,7 +13,7 @@ import * as path from "path";
13
13
  import { runRunner, runRunnerWithLoop, resolveDeletePolicy, resolveSkipPersonal, resolveSkipCompanies, selectObjectIOFactory, routeChangeToTarget, buildTargetedPushArgv, resolvePullScope, readPinnedPrefixes, SESSIONS_SCOPE_PREFIX, defaultCollectTelemetry, AUTH_REQUIRED_PASS_EXIT, } from "./sync-runner.js";
14
14
  import { PresignObjectIO, S3SdkObjectIO } from "../object-io.js";
15
15
  import { FakeClock, } from "../watcher.js";
16
- import { adaptivePollMs, journalEntriesForWatcherDelete, shouldQuarantineWatcherDeleteIntents, } from "./sync-runner-watch-loop.js";
16
+ import { adaptivePollMs, advanceMonotonicDeadline, journalEntriesForWatcherDelete, shouldQuarantineWatcherDeleteIntents, } from "./sync-runner-watch-loop.js";
17
17
  import { PERSONAL_VAULT_JOURNAL_SLUG, readJournal, writeJournal, } from "../journal.js";
18
18
  import { HQ_CLOUD_VERSION } from "../version.js";
19
19
  import { lockPathFor, OPERATION_LOCKED_EXIT } from "../operation-lock.js";
@@ -3383,6 +3383,21 @@ describe("adaptivePollMs (whole-box CPU backoff)", () => {
3383
3383
  expect(adaptivePollMs(4, 4, 30_000, 300_000)).toBe(300_000);
3384
3384
  });
3385
3385
  });
3386
+ describe("advanceMonotonicDeadline", () => {
3387
+ it("runs one immediate catch-up after missed slots, then resumes at the first future slot", () => {
3388
+ // A pass due at t=0 took through t=1,800 with a 600 ms cadence. It does
3389
+ // not replay the three missed slots; exactly one catch-up is immediate and
3390
+ // the following regular deadline is t=2,400.
3391
+ expect(advanceMonotonicDeadline(0, 1_800, 600)).toEqual({
3392
+ nextDueAt: 2_400,
3393
+ delayMs: 0,
3394
+ });
3395
+ expect(advanceMonotonicDeadline(2_400, 1_800, 600)).toEqual({
3396
+ nextDueAt: 3_000,
3397
+ delayMs: 1_200,
3398
+ });
3399
+ });
3400
+ });
3386
3401
  describe("runRunnerWithLoop — adaptive poll interval (no --poll-remote-ms)", () => {
3387
3402
  // Capture the ms handed to sleep on the FIRST poll, then hang so the loop
3388
3403
  // parks until shutdown (mirrors the never-resolving sleep the other loop
@@ -3397,55 +3412,78 @@ describe("runRunnerWithLoop — adaptive poll interval (no --poll-remote-ms)", (
3397
3412
  },
3398
3413
  };
3399
3414
  }
3400
- it("sleeps the floor interval when the box is idle", async () => {
3415
+ it("waits only the bounded remainder of the idle cadence slot", async () => {
3401
3416
  const { calls, sleep } = makeCapturingSleep();
3417
+ const monotonicNow = vi
3418
+ .fn()
3419
+ .mockReturnValueOnce(1_000)
3420
+ // A 20 ms pass consumes part of this 60 s slot. The scheduler must
3421
+ // retain the original deadline rather than slewing a new 60 s wait.
3422
+ .mockReturnValueOnce(1_020);
3402
3423
  let triggerShutdown = () => { };
3403
3424
  const loop = runRunnerWithLoop(["--companies", "--watch", "--hq-root", "/tmp/hq"], {
3404
3425
  runPass: vi.fn().mockResolvedValue(0),
3405
3426
  sleep,
3406
3427
  sampleLoadAvg: () => 0,
3428
+ monotonicNow,
3407
3429
  onShutdownSignal: (handler) => {
3408
3430
  triggerShutdown = handler;
3409
3431
  return () => { };
3410
3432
  },
3411
3433
  });
3412
3434
  await flushLoopMicrotasks();
3413
- expect(calls[0]).toBe(60_000);
3435
+ expect(calls[0]).toBe(59_980);
3436
+ expect(calls[0]).toBeGreaterThan(0);
3437
+ expect(calls[0]).toBeLessThanOrEqual(60_000);
3414
3438
  triggerShutdown();
3415
3439
  await loop;
3416
3440
  });
3417
- it("backs off to the ceiling interval when the box is fully loaded", async () => {
3441
+ it("waits only the bounded remainder of the saturated cadence slot", async () => {
3418
3442
  const { calls, sleep } = makeCapturingSleep();
3443
+ const monotonicNow = vi
3444
+ .fn()
3445
+ .mockReturnValueOnce(1_000)
3446
+ .mockReturnValueOnce(1_075);
3419
3447
  let triggerShutdown = () => { };
3420
3448
  const loop = runRunnerWithLoop(["--companies", "--watch", "--hq-root", "/tmp/hq"], {
3421
3449
  runPass: vi.fn().mockResolvedValue(0),
3422
3450
  sleep,
3423
3451
  sampleLoadAvg: () => 1_000, // far above any core count → clamps to ceil
3452
+ monotonicNow,
3424
3453
  onShutdownSignal: (handler) => {
3425
3454
  triggerShutdown = handler;
3426
3455
  return () => { };
3427
3456
  },
3428
3457
  });
3429
3458
  await flushLoopMicrotasks();
3430
- expect(calls[0]).toBe(600_000);
3459
+ expect(calls[0]).toBe(599_925);
3460
+ expect(calls[0]).toBeGreaterThan(0);
3461
+ expect(calls[0]).toBeLessThanOrEqual(600_000);
3431
3462
  triggerShutdown();
3432
3463
  await loop;
3433
3464
  });
3434
- it("an explicit --poll-remote-ms pins a fixed interval and ignores load", async () => {
3465
+ it("uses the bounded remainder of an explicit cadence slot and ignores load", async () => {
3435
3466
  const { calls, sleep } = makeCapturingSleep();
3436
3467
  const sampleLoadAvg = vi.fn(() => 1_000);
3468
+ const monotonicNow = vi
3469
+ .fn()
3470
+ .mockReturnValueOnce(1_000)
3471
+ .mockReturnValueOnce(1_123);
3437
3472
  let triggerShutdown = () => { };
3438
3473
  const loop = runRunnerWithLoop(["--companies", "--watch", "--poll-remote-ms", "123456", "--hq-root", "/tmp/hq"], {
3439
3474
  runPass: vi.fn().mockResolvedValue(0),
3440
3475
  sleep,
3441
3476
  sampleLoadAvg,
3477
+ monotonicNow,
3442
3478
  onShutdownSignal: (handler) => {
3443
3479
  triggerShutdown = handler;
3444
3480
  return () => { };
3445
3481
  },
3446
3482
  });
3447
3483
  await flushLoopMicrotasks();
3448
- expect(calls[0]).toBe(123456);
3484
+ expect(calls[0]).toBe(123_333);
3485
+ expect(calls[0]).toBeGreaterThan(0);
3486
+ expect(calls[0]).toBeLessThanOrEqual(123_456);
3449
3487
  expect(sampleLoadAvg).not.toHaveBeenCalled();
3450
3488
  triggerShutdown();
3451
3489
  await loop;
@@ -3476,35 +3514,45 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3476
3514
  expect(runPass).toHaveBeenCalled();
3477
3515
  expect(watcher.disposed).toBe(true);
3478
3516
  });
3479
- it("trigger-on-change: a changed signal accumulates without an immediate push", async () => {
3517
+ it("U07: watcher interrupts a fake 600 s wait and drains through the guarded scheduler", async () => {
3480
3518
  const watcher = makeWatcherStub();
3481
3519
  const clock = new FakeClock();
3482
3520
  let triggerShutdown = () => { };
3483
3521
  const runPass = vi.fn().mockResolvedValue(0);
3484
- const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3522
+ const sleepCalls = [];
3523
+ const loop = runRunnerWithLoop([
3524
+ "--companies",
3525
+ "--watch",
3526
+ "--event-push",
3527
+ "--poll-remote-ms",
3528
+ "600000",
3529
+ "--hq-root",
3530
+ "/tmp/hq",
3531
+ ], {
3485
3532
  runPass,
3486
3533
  clock,
3487
3534
  createWatcher: () => watcher,
3488
- sleep: () => new Promise(() => { }),
3535
+ monotonicNow: () => 0,
3536
+ sleep: (ms) => {
3537
+ sleepCalls.push(ms);
3538
+ return new Promise(() => { });
3539
+ },
3489
3540
  onShutdownSignal: (handler) => {
3490
3541
  triggerShutdown = handler;
3491
3542
  return () => { };
3492
3543
  },
3493
3544
  });
3494
- // Drain the initial poll pass.
3495
- await Promise.resolve();
3496
- await Promise.resolve();
3497
- runPass.mockClear();
3498
- // Fire a change for an indigo file; advance the (zero) driver window.
3545
+ await flushLoopMicrotasks();
3546
+ expect(sleepCalls).toEqual([600_000]);
3547
+ // The fake 600-second timer remains unresolved. The watcher wake must
3548
+ // nonetheless start the next pass immediately through runGuarded.
3499
3549
  watcher.emit("companies/indigo/board.json");
3500
- clock.advance(0);
3501
- await Promise.resolve();
3502
- await Promise.resolve();
3503
- expect(runPass).not.toHaveBeenCalled();
3550
+ await flushLoopMicrotasks();
3551
+ expect(runPass).toHaveBeenCalledTimes(2);
3504
3552
  triggerShutdown();
3505
3553
  await loop;
3506
3554
  });
3507
- it("bare signal (no path) accumulates without an immediate push", async () => {
3555
+ it("bare watcher signal interrupts the wait and schedules a full reconcile", async () => {
3508
3556
  const watcher = makeWatcherStub();
3509
3557
  const clock = new FakeClock();
3510
3558
  let triggerShutdown = () => { };
@@ -3524,9 +3572,8 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3524
3572
  runPass.mockClear();
3525
3573
  watcher.emit(); // no path — TreeWatcher's bare signal shape
3526
3574
  clock.advance(0);
3527
- await Promise.resolve();
3528
- await Promise.resolve();
3529
- expect(runPass).not.toHaveBeenCalled();
3575
+ await flushLoopMicrotasks();
3576
+ expect(runPass).toHaveBeenCalledTimes(1);
3530
3577
  triggerShutdown();
3531
3578
  await loop;
3532
3579
  });
@@ -3584,7 +3631,54 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3584
3631
  triggerShutdown();
3585
3632
  await loop;
3586
3633
  });
3587
- it("F05: guard-held watcher changes stay accumulated until a poll drain", async () => {
3634
+ it("U07: retains a watcher event raised before the next wait registers, without guarded overlap", async () => {
3635
+ const watcher = makeWatcherStub();
3636
+ let triggerShutdown = () => { };
3637
+ let releaseFirstPass = () => { };
3638
+ const firstPassGate = new Promise((resolve) => {
3639
+ releaseFirstPass = resolve;
3640
+ });
3641
+ const spans = [];
3642
+ let passes = 0;
3643
+ const runPass = vi.fn().mockImplementation(async () => {
3644
+ const span = { enteredAt: process.hrtime.bigint(), exitedAt: 0n };
3645
+ spans.push(span);
3646
+ passes += 1;
3647
+ if (passes === 1)
3648
+ await firstPassGate;
3649
+ span.exitedAt = process.hrtime.bigint();
3650
+ return 0;
3651
+ });
3652
+ const loop = runRunnerWithLoop([
3653
+ "--companies",
3654
+ "--watch",
3655
+ "--event-push",
3656
+ "--poll-remote-ms",
3657
+ "600000",
3658
+ "--hq-root",
3659
+ "/tmp/hq",
3660
+ ], {
3661
+ runPass,
3662
+ createWatcher: () => watcher,
3663
+ sleep: () => new Promise(() => { }),
3664
+ onShutdownSignal: (handler) => {
3665
+ triggerShutdown = handler;
3666
+ return () => { };
3667
+ },
3668
+ });
3669
+ await flushLoopMicrotasks();
3670
+ expect(spans).toHaveLength(1);
3671
+ // This event arrives while the initial guarded pass owns the scheduler,
3672
+ // before its next sleep/wait has been registered.
3673
+ watcher.emit("companies/indigo/pre-registration.md");
3674
+ releaseFirstPass();
3675
+ await flushLoopMicrotasks();
3676
+ expect(spans).toHaveLength(2);
3677
+ expect(spans[1].enteredAt >= spans[0].exitedAt).toBe(true);
3678
+ triggerShutdown();
3679
+ await loop;
3680
+ });
3681
+ it("F05: guard-held watcher changes stay accumulated, then drain immediately after the guarded pass", async () => {
3588
3682
  const watcher = makeWatcherStub();
3589
3683
  const clock = new FakeClock();
3590
3684
  let triggerShutdown = () => { };
@@ -3665,8 +3759,7 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3665
3759
  expect(passes).toBe(1);
3666
3760
  // Step the poll interval → a second poll pass runs (safety net cadence).
3667
3761
  poll.resolve?.();
3668
- await Promise.resolve();
3669
- await Promise.resolve();
3762
+ await flushLoopMicrotasks();
3670
3763
  expect(passes).toBeGreaterThanOrEqual(2);
3671
3764
  triggerShutdown();
3672
3765
  // Release any pending sleep so the loop can observe shutdown and exit.
@@ -3930,7 +4023,7 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
3930
4023
  shutdown: () => triggerShutdown(),
3931
4024
  };
3932
4025
  }
3933
- it("startup runs one full reconcile, then watcher edits wait for the poll drain", async () => {
4026
+ it("startup runs one full reconcile, then watcher edits interrupt the pending wait", async () => {
3934
4027
  const runPass = vi.fn().mockResolvedValue(0);
3935
4028
  const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
3936
4029
  await flushLoopMicrotasks();
@@ -3938,9 +4031,6 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
3938
4031
  runPass.mockClear();
3939
4032
  watcher.emit("companies/indigo/knowledge/a.md");
3940
4033
  await flushLoopMicrotasks();
3941
- expect(runPass).not.toHaveBeenCalled();
3942
- tick();
3943
- await flushLoopMicrotasks();
3944
4034
  expect(runPass.mock.calls.map((call) => call[0])).toEqual([
3945
4035
  [
3946
4036
  "--company",
@@ -3986,6 +4076,24 @@ describe("runRunnerWithLoop — accumulate-and-drain scoped push", () => {
3986
4076
  tick();
3987
4077
  await loop;
3988
4078
  });
4079
+ it("U08: --companies --skip-personal drops a personal watcher edit before it queues work", async () => {
4080
+ const runPass = vi.fn().mockResolvedValue(0);
4081
+ const { loop, watcher, tick, shutdown } = startDrainLoopWithArgv(runPass, [
4082
+ ...watchArgv,
4083
+ "--skip-personal",
4084
+ ]);
4085
+ await flushLoopMicrotasks();
4086
+ runPass.mockClear();
4087
+ watcher.emit("knowledge/private.md");
4088
+ await flushLoopMicrotasks();
4089
+ // The watcher event is rejected before the scheduler is interrupted:
4090
+ // no route-specific pass is queued and therefore no runner/network work
4091
+ // is reachable for a disabled personal scope.
4092
+ expect(runPass).not.toHaveBeenCalled();
4093
+ shutdown();
4094
+ tick();
4095
+ await loop;
4096
+ });
3989
4097
  it("drains an unlink path through the same scoped tombstone push path", async () => {
3990
4098
  const runPass = vi.fn().mockResolvedValue(0);
3991
4099
  const { loop, watcher, tick, shutdown } = startDrainLoop(runPass);
@@ -4989,7 +5097,7 @@ function makeBatchWatcherStub() {
4989
5097
  };
4990
5098
  }
4991
5099
  describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", () => {
4992
- const ENROLLED_CLAIMS = { email: "hassaan@getindigo.ai" };
5100
+ const EMAIL_CLAIMS = { email: "hassaan@getindigo.ai" };
4993
5101
  const UNENROLLED_CLAIMS = { email: "someone@getindigo.ai" };
4994
5102
  function runLoop(opts) {
4995
5103
  const watcher = opts.watcher ?? makeBatchWatcherStub();
@@ -5036,7 +5144,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5036
5144
  for (let i = 0; i < n; i++)
5037
5145
  await Promise.resolve();
5038
5146
  }
5039
- it("gate OFF (unenrolled email): the event-sync seam is never consulted", async () => {
5147
+ it("does not use an unenrolled email as a client-side denial", async () => {
5040
5148
  const startEventSync = vi.fn().mockResolvedValue(null);
5041
5149
  const { loop, shutdown } = runLoop({
5042
5150
  claims: UNENROLLED_CLAIMS,
@@ -5045,20 +5153,20 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5045
5153
  await microtasks();
5046
5154
  shutdown();
5047
5155
  await loop;
5048
- expect(startEventSync).not.toHaveBeenCalled();
5156
+ expect(startEventSync).toHaveBeenCalledTimes(1);
5049
5157
  });
5050
- it("gate OFF (no cached claims): the event-sync seam is never consulted", async () => {
5158
+ it("does not use missing cached claims as a client-side denial", async () => {
5051
5159
  const startEventSync = vi.fn().mockResolvedValue(null);
5052
5160
  const { loop, shutdown } = runLoop({ claims: null, startEventSync });
5053
5161
  await microtasks();
5054
5162
  shutdown();
5055
5163
  await loop;
5056
- expect(startEventSync).not.toHaveBeenCalled();
5164
+ expect(startEventSync).toHaveBeenCalledTimes(1);
5057
5165
  });
5058
- it("gate ON (enrolled email): wiring receives root, api url, auth + a sync bridge", async () => {
5166
+ it("asks the server for authority before wiring publish and receive", async () => {
5059
5167
  const startEventSync = vi.fn().mockResolvedValue(null);
5060
5168
  const { loop, shutdown } = runLoop({
5061
- claims: ENROLLED_CLAIMS,
5169
+ claims: EMAIL_CLAIMS,
5062
5170
  startEventSync,
5063
5171
  });
5064
5172
  await microtasks();
@@ -5081,9 +5189,16 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5081
5189
  ownDeviceId: "dev-test",
5082
5190
  dispose: vi.fn().mockResolvedValue(undefined),
5083
5191
  });
5084
- const runPass = vi.fn().mockResolvedValue(0);
5085
- const { loop, watcher, clock, tick, shutdown } = runLoop({
5086
- claims: ENROLLED_CLAIMS,
5192
+ let finishTargetedPush = () => { };
5193
+ const targetedPush = new Promise((resolve) => {
5194
+ finishTargetedPush = resolve;
5195
+ });
5196
+ const runPass = vi
5197
+ .fn()
5198
+ .mockResolvedValueOnce(0)
5199
+ .mockImplementation((passArgv) => passArgv.includes("--company") ? targetedPush : Promise.resolve(0));
5200
+ const { loop, watcher, clock, shutdown } = runLoop({
5201
+ claims: EMAIL_CLAIMS,
5087
5202
  startEventSync,
5088
5203
  runPass,
5089
5204
  });
@@ -5095,12 +5210,18 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5095
5210
  watcher.emit("companies/indigo/a.md", batch);
5096
5211
  clock.advance(0);
5097
5212
  await microtasks();
5098
- expect(runPass).not.toHaveBeenCalled();
5213
+ // U07 wakes the scheduler immediately. It may start the scoped push, but
5214
+ // it must not announce the batch until that push has succeeded.
5215
+ expect(runPass).toHaveBeenCalledWith(expect.arrayContaining([
5216
+ "--company",
5217
+ "indigo",
5218
+ "--direction",
5219
+ "push",
5220
+ ]));
5099
5221
  expect(publishBatch).not.toHaveBeenCalled();
5100
- tick();
5222
+ finishTargetedPush(0);
5101
5223
  await microtasks();
5102
5224
  // Scoped push drained and succeeded → batch published.
5103
- expect(runPass).toHaveBeenCalled();
5104
5225
  expect(publishBatch).toHaveBeenCalledTimes(1);
5105
5226
  expect([...publishBatch.mock.calls[0][0].paths.values()]).toEqual([
5106
5227
  "companies/indigo/a.md",
@@ -5122,7 +5243,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5122
5243
  .mockResolvedValueOnce(0)
5123
5244
  .mockResolvedValue(1);
5124
5245
  const { loop, watcher, clock, tick, shutdown } = runLoop({
5125
- claims: ENROLLED_CLAIMS,
5246
+ claims: EMAIL_CLAIMS,
5126
5247
  startEventSync,
5127
5248
  runPass,
5128
5249
  });
@@ -5153,7 +5274,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5153
5274
  });
5154
5275
  const runPass = vi.fn().mockResolvedValue(0);
5155
5276
  const { loop, watcher, clock, tick, shutdown } = runLoop({
5156
- claims: ENROLLED_CLAIMS,
5277
+ claims: EMAIL_CLAIMS,
5157
5278
  startEventSync,
5158
5279
  runPass,
5159
5280
  });
@@ -5205,7 +5326,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5205
5326
  })
5206
5327
  .mockResolvedValue(0);
5207
5328
  const { loop, watcher, tick, shutdown } = runLoop({
5208
- claims: ENROLLED_CLAIMS,
5329
+ claims: EMAIL_CLAIMS,
5209
5330
  startEventSync,
5210
5331
  runPass,
5211
5332
  });
@@ -5246,7 +5367,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5246
5367
  })
5247
5368
  .mockResolvedValue(0);
5248
5369
  const { loop, watcher, tick, shutdown } = runLoop({
5249
- claims: ENROLLED_CLAIMS,
5370
+ claims: EMAIL_CLAIMS,
5250
5371
  startEventSync,
5251
5372
  runPass,
5252
5373
  });
@@ -5275,7 +5396,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5275
5396
  });
5276
5397
  const runPass = vi.fn().mockResolvedValue(0);
5277
5398
  const { loop, watcher, clock, tick, shutdown } = runLoop({
5278
- claims: ENROLLED_CLAIMS,
5399
+ claims: EMAIL_CLAIMS,
5279
5400
  startEventSync,
5280
5401
  runPass,
5281
5402
  });
@@ -5382,7 +5503,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5382
5503
  triggerShutdown = handler;
5383
5504
  return () => { };
5384
5505
  },
5385
- getIdTokenClaims: () => ENROLLED_CLAIMS,
5506
+ getIdTokenClaims: () => EMAIL_CLAIMS,
5386
5507
  getAccessToken: async () => "jwt-test",
5387
5508
  startEventSync,
5388
5509
  });
@@ -5482,7 +5603,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5482
5603
  dispose: vi.fn().mockResolvedValue(undefined),
5483
5604
  });
5484
5605
  const { loop, watcher, clock, tick, shutdown } = runLoop({
5485
- claims: ENROLLED_CLAIMS,
5606
+ claims: EMAIL_CLAIMS,
5486
5607
  startEventSync,
5487
5608
  });
5488
5609
  await microtasks();
@@ -5506,7 +5627,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5506
5627
  dispose,
5507
5628
  });
5508
5629
  const { loop, shutdown } = runLoop({
5509
- claims: ENROLLED_CLAIMS,
5630
+ claims: EMAIL_CLAIMS,
5510
5631
  startEventSync,
5511
5632
  });
5512
5633
  await microtasks();
@@ -5520,7 +5641,7 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
5520
5641
  try {
5521
5642
  const startEventSync = vi.fn().mockResolvedValue(null);
5522
5643
  const { loop, shutdown } = runLoop({
5523
- claims: ENROLLED_CLAIMS,
5644
+ claims: EMAIL_CLAIMS,
5524
5645
  startEventSync,
5525
5646
  });
5526
5647
  await microtasks();