@indigoai-us/hq-cloud 6.11.11 → 6.11.13

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 (220) hide show
  1. package/dist/bin/sync-runner-company.d.ts +35 -0
  2. package/dist/bin/sync-runner-company.d.ts.map +1 -0
  3. package/dist/bin/sync-runner-company.js +290 -0
  4. package/dist/bin/sync-runner-company.js.map +1 -0
  5. package/dist/bin/sync-runner-events.d.ts +12 -0
  6. package/dist/bin/sync-runner-events.d.ts.map +1 -0
  7. package/dist/bin/sync-runner-events.js +12 -0
  8. package/dist/bin/sync-runner-events.js.map +1 -0
  9. package/dist/bin/sync-runner-planning.d.ts +53 -0
  10. package/dist/bin/sync-runner-planning.d.ts.map +1 -0
  11. package/dist/bin/sync-runner-planning.js +59 -0
  12. package/dist/bin/sync-runner-planning.js.map +1 -0
  13. package/dist/bin/sync-runner-rollup.d.ts +24 -0
  14. package/dist/bin/sync-runner-rollup.d.ts.map +1 -0
  15. package/dist/bin/sync-runner-rollup.js +46 -0
  16. package/dist/bin/sync-runner-rollup.js.map +1 -0
  17. package/dist/bin/sync-runner-telemetry.d.ts +5 -0
  18. package/dist/bin/sync-runner-telemetry.d.ts.map +1 -0
  19. package/dist/bin/sync-runner-telemetry.js +5 -0
  20. package/dist/bin/sync-runner-telemetry.js.map +1 -0
  21. package/dist/bin/sync-runner-watch-loop.d.ts +17 -0
  22. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -0
  23. package/dist/bin/sync-runner-watch-loop.js +372 -0
  24. package/dist/bin/sync-runner-watch-loop.js.map +1 -0
  25. package/dist/bin/sync-runner-watch-routes.d.ts +25 -0
  26. package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -0
  27. package/dist/bin/sync-runner-watch-routes.js +74 -0
  28. package/dist/bin/sync-runner-watch-routes.js.map +1 -0
  29. package/dist/bin/sync-runner.d.ts +5 -54
  30. package/dist/bin/sync-runner.d.ts.map +1 -1
  31. package/dist/bin/sync-runner.js +76 -978
  32. package/dist/bin/sync-runner.js.map +1 -1
  33. package/dist/bin/sync-runner.test.js +265 -11
  34. package/dist/bin/sync-runner.test.js.map +1 -1
  35. package/dist/cli/reindex.d.ts.map +1 -1
  36. package/dist/cli/reindex.js +34 -17
  37. package/dist/cli/reindex.js.map +1 -1
  38. package/dist/cli/reindex.test.js +39 -5
  39. package/dist/cli/reindex.test.js.map +1 -1
  40. package/dist/cli/rescue-classify-ordering.test.js +75 -0
  41. package/dist/cli/rescue-classify-ordering.test.js.map +1 -1
  42. package/dist/cli/rescue-core.d.ts +45 -0
  43. package/dist/cli/rescue-core.d.ts.map +1 -1
  44. package/dist/cli/rescue-core.js +320 -170
  45. package/dist/cli/rescue-core.js.map +1 -1
  46. package/dist/cli/share.d.ts +2 -1
  47. package/dist/cli/share.d.ts.map +1 -1
  48. package/dist/cli/share.js +276 -660
  49. package/dist/cli/share.js.map +1 -1
  50. package/dist/cli/share.test.js +30 -0
  51. package/dist/cli/share.test.js.map +1 -1
  52. package/dist/cli/sync.d.ts +28 -1
  53. package/dist/cli/sync.d.ts.map +1 -1
  54. package/dist/cli/sync.js +541 -748
  55. package/dist/cli/sync.js.map +1 -1
  56. package/dist/cli/sync.test.js +382 -1
  57. package/dist/cli/sync.test.js.map +1 -1
  58. package/dist/cognito-auth.d.ts.map +1 -1
  59. package/dist/cognito-auth.js +55 -10
  60. package/dist/cognito-auth.js.map +1 -1
  61. package/dist/cognito-auth.test.js +61 -0
  62. package/dist/cognito-auth.test.js.map +1 -1
  63. package/dist/daemon-worker.d.ts +2 -2
  64. package/dist/daemon-worker.js +3 -3
  65. package/dist/daemon-worker.js.map +1 -1
  66. package/dist/index.d.ts +2 -1
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +1 -1
  69. package/dist/index.js.map +1 -1
  70. package/dist/journal.d.ts.map +1 -1
  71. package/dist/journal.js +93 -6
  72. package/dist/journal.js.map +1 -1
  73. package/dist/journal.test.js +59 -0
  74. package/dist/journal.test.js.map +1 -1
  75. package/dist/machine-auth.test.js +60 -2
  76. package/dist/machine-auth.test.js.map +1 -1
  77. package/dist/object-io.d.ts +37 -1
  78. package/dist/object-io.d.ts.map +1 -1
  79. package/dist/object-io.js +149 -30
  80. package/dist/object-io.js.map +1 -1
  81. package/dist/object-io.test.js +121 -0
  82. package/dist/object-io.test.js.map +1 -1
  83. package/dist/operation-lock.d.ts +8 -8
  84. package/dist/operation-lock.d.ts.map +1 -1
  85. package/dist/operation-lock.js +99 -32
  86. package/dist/operation-lock.js.map +1 -1
  87. package/dist/operation-lock.test.js +51 -4
  88. package/dist/operation-lock.test.js.map +1 -1
  89. package/dist/personal-vault.d.ts.map +1 -1
  90. package/dist/personal-vault.js +8 -2
  91. package/dist/personal-vault.js.map +1 -1
  92. package/dist/personal-vault.test.js +34 -0
  93. package/dist/personal-vault.test.js.map +1 -1
  94. package/dist/prefix-coalesce.d.ts +20 -9
  95. package/dist/prefix-coalesce.d.ts.map +1 -1
  96. package/dist/prefix-coalesce.js +124 -28
  97. package/dist/prefix-coalesce.js.map +1 -1
  98. package/dist/prefix-coalesce.test.js +57 -2
  99. package/dist/prefix-coalesce.test.js.map +1 -1
  100. package/dist/remote-pull.d.ts +8 -3
  101. package/dist/remote-pull.d.ts.map +1 -1
  102. package/dist/remote-pull.js +85 -16
  103. package/dist/remote-pull.js.map +1 -1
  104. package/dist/remote-pull.test.js +213 -2
  105. package/dist/remote-pull.test.js.map +1 -1
  106. package/dist/s3.d.ts +2 -0
  107. package/dist/s3.d.ts.map +1 -1
  108. package/dist/s3.js +197 -116
  109. package/dist/s3.js.map +1 -1
  110. package/dist/s3.test.js +109 -0
  111. package/dist/s3.test.js.map +1 -1
  112. package/dist/scope-shrink.d.ts +3 -2
  113. package/dist/scope-shrink.d.ts.map +1 -1
  114. package/dist/scope-shrink.js +1 -1
  115. package/dist/scope-shrink.js.map +1 -1
  116. package/dist/skill-telemetry.d.ts +1 -1
  117. package/dist/skill-telemetry.d.ts.map +1 -1
  118. package/dist/skill-telemetry.js +69 -9
  119. package/dist/skill-telemetry.js.map +1 -1
  120. package/dist/skill-telemetry.test.js +86 -0
  121. package/dist/skill-telemetry.test.js.map +1 -1
  122. package/dist/sync/event-sync.d.ts +6 -0
  123. package/dist/sync/event-sync.d.ts.map +1 -1
  124. package/dist/sync/event-sync.js +34 -1
  125. package/dist/sync/event-sync.js.map +1 -1
  126. package/dist/sync/event-sync.test.js +73 -0
  127. package/dist/sync/event-sync.test.js.map +1 -1
  128. package/dist/sync/metrics.d.ts +17 -1
  129. package/dist/sync/metrics.d.ts.map +1 -1
  130. package/dist/sync/metrics.js +32 -1
  131. package/dist/sync/metrics.js.map +1 -1
  132. package/dist/sync/metrics.test.js +74 -1
  133. package/dist/sync/metrics.test.js.map +1 -1
  134. package/dist/sync/pull-scope.d.ts.map +1 -1
  135. package/dist/sync/pull-scope.js +15 -7
  136. package/dist/sync/pull-scope.js.map +1 -1
  137. package/dist/sync/push-receiver.d.ts +12 -5
  138. package/dist/sync/push-receiver.d.ts.map +1 -1
  139. package/dist/sync/push-receiver.js +45 -17
  140. package/dist/sync/push-receiver.js.map +1 -1
  141. package/dist/sync/push-receiver.test.js +67 -1
  142. package/dist/sync/push-receiver.test.js.map +1 -1
  143. package/dist/sync-core.d.ts +27 -0
  144. package/dist/sync-core.d.ts.map +1 -0
  145. package/dist/sync-core.js +54 -0
  146. package/dist/sync-core.js.map +1 -0
  147. package/dist/telemetry.d.ts +1 -1
  148. package/dist/telemetry.d.ts.map +1 -1
  149. package/dist/telemetry.js +59 -6
  150. package/dist/telemetry.js.map +1 -1
  151. package/dist/telemetry.test.js +74 -0
  152. package/dist/telemetry.test.js.map +1 -1
  153. package/dist/types.d.ts +8 -0
  154. package/dist/types.d.ts.map +1 -1
  155. package/dist/vault-client.d.ts.map +1 -1
  156. package/dist/vault-client.js +284 -36
  157. package/dist/vault-client.js.map +1 -1
  158. package/dist/vault-client.test.js +59 -0
  159. package/dist/vault-client.test.js.map +1 -1
  160. package/dist/watcher.d.ts +38 -20
  161. package/dist/watcher.d.ts.map +1 -1
  162. package/dist/watcher.js +155 -143
  163. package/dist/watcher.js.map +1 -1
  164. package/dist/watcher.test.js +103 -0
  165. package/dist/watcher.test.js.map +1 -1
  166. package/package.json +1 -1
  167. package/src/bin/sync-runner-company.ts +350 -0
  168. package/src/bin/sync-runner-events.ts +25 -0
  169. package/src/bin/sync-runner-planning.ts +121 -0
  170. package/src/bin/sync-runner-rollup.ts +72 -0
  171. package/src/bin/sync-runner-telemetry.ts +8 -0
  172. package/src/bin/sync-runner-watch-loop.ts +443 -0
  173. package/src/bin/sync-runner-watch-routes.ts +86 -0
  174. package/src/bin/sync-runner.test.ts +298 -11
  175. package/src/bin/sync-runner.ts +99 -1054
  176. package/src/cli/reindex.test.ts +41 -3
  177. package/src/cli/reindex.ts +35 -19
  178. package/src/cli/rescue-classify-ordering.test.ts +81 -0
  179. package/src/cli/rescue-core.ts +400 -165
  180. package/src/cli/share.test.ts +38 -0
  181. package/src/cli/share.ts +420 -693
  182. package/src/cli/sync.test.ts +460 -1
  183. package/src/cli/sync.ts +788 -825
  184. package/src/cognito-auth.test.ts +77 -0
  185. package/src/cognito-auth.ts +73 -11
  186. package/src/daemon-worker.ts +3 -3
  187. package/src/index.ts +8 -0
  188. package/src/journal.test.ts +72 -0
  189. package/src/journal.ts +95 -8
  190. package/src/machine-auth.test.ts +64 -2
  191. package/src/object-io.test.ts +142 -0
  192. package/src/object-io.ts +183 -31
  193. package/src/operation-lock.test.ts +63 -4
  194. package/src/operation-lock.ts +99 -31
  195. package/src/personal-vault.test.ts +42 -0
  196. package/src/personal-vault.ts +8 -2
  197. package/src/prefix-coalesce.test.ts +71 -1
  198. package/src/prefix-coalesce.ts +155 -30
  199. package/src/remote-pull.test.ts +235 -1
  200. package/src/remote-pull.ts +106 -18
  201. package/src/s3.test.ts +126 -0
  202. package/src/s3.ts +237 -122
  203. package/src/scope-shrink.ts +6 -3
  204. package/src/skill-telemetry.test.ts +109 -0
  205. package/src/skill-telemetry.ts +82 -14
  206. package/src/sync/event-sync.test.ts +75 -0
  207. package/src/sync/event-sync.ts +54 -1
  208. package/src/sync/metrics.test.ts +81 -0
  209. package/src/sync/metrics.ts +59 -4
  210. package/src/sync/pull-scope.ts +23 -7
  211. package/src/sync/push-receiver.test.ts +73 -1
  212. package/src/sync/push-receiver.ts +56 -20
  213. package/src/sync-core.ts +58 -0
  214. package/src/telemetry.test.ts +85 -0
  215. package/src/telemetry.ts +69 -6
  216. package/src/types.ts +8 -0
  217. package/src/vault-client.test.ts +74 -0
  218. package/src/vault-client.ts +395 -43
  219. package/src/watcher.test.ts +117 -0
  220. package/src/watcher.ts +215 -174
@@ -42,6 +42,7 @@ import type {
42
42
  PendingInviteByEmail,
43
43
  } from "../vault-client.js";
44
44
  import { VaultAuthError } from "../vault-client.js";
45
+ import type { PushEvent } from "../sync/push-event.js";
45
46
 
46
47
  // ---------------------------------------------------------------------------
47
48
  // Hermetic journal state — the runner now calls migratePersonalVaultJournal()
@@ -1008,7 +1009,7 @@ describe("per-company fanout", () => {
1008
1009
  ]);
1009
1010
  });
1010
1011
 
1011
- it("tags per-file error events with the company slug", async () => {
1012
+ it("F09: per-file transfer errors make the run partial and non-zero", async () => {
1012
1013
  const deps = makeDeps({
1013
1014
  createVaultClient: () =>
1014
1015
  makeVaultStub({
@@ -1027,7 +1028,7 @@ describe("per-company fanout", () => {
1027
1028
  });
1028
1029
 
1029
1030
  const code = await runRunner(["--companies"], deps);
1030
- expect(code).toBe(0);
1031
+ expect(code).toBe(2);
1031
1032
  // Per-file errors are routed to stderr (error-class events).
1032
1033
  const errs = deps.stderr
1033
1034
  .events()
@@ -1042,6 +1043,14 @@ describe("per-company fanout", () => {
1042
1043
  message: "access denied",
1043
1044
  },
1044
1045
  ]);
1046
+ const all = deps.stdout
1047
+ .events()
1048
+ .find(
1049
+ (e): e is Extract<RunnerEvent, { type: "all-complete" }> =>
1050
+ e.type === "all-complete",
1051
+ );
1052
+ expect(all).toBeDefined();
1053
+ expect(all?.partial).toBe(true);
1045
1054
  });
1046
1055
 
1047
1056
  it("emits complete event per company with the SyncResult spread", async () => {
@@ -3056,6 +3065,73 @@ describe("runRunnerWithLoop — event-push wiring", () => {
3056
3065
  await loop;
3057
3066
  });
3058
3067
 
3068
+ it("F05: guard-held watcher push is queued after the active poll pass", async () => {
3069
+ const watcher = makeWatcherStub();
3070
+ const clock = new FakeClock();
3071
+ let triggerShutdown = () => {};
3072
+
3073
+ let releasePoll: () => void = () => {};
3074
+ const pollGate = new Promise<void>((resolve) => {
3075
+ releasePoll = resolve;
3076
+ });
3077
+ let active = 0;
3078
+ let maxConcurrent = 0;
3079
+ let calls = 0;
3080
+ const runPass = vi.fn().mockImplementation(async () => {
3081
+ active++;
3082
+ maxConcurrent = Math.max(maxConcurrent, active);
3083
+ calls++;
3084
+ if (calls === 1) await pollGate;
3085
+ active--;
3086
+ return 0;
3087
+ });
3088
+
3089
+ const loop = runRunnerWithLoop(
3090
+ ["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"],
3091
+ {
3092
+ runPass,
3093
+ clock,
3094
+ createWatcher: () => watcher,
3095
+ sleep: () => new Promise<void>(() => {}),
3096
+ onShutdownSignal: (handler) => {
3097
+ triggerShutdown = handler;
3098
+ return () => {};
3099
+ },
3100
+ },
3101
+ );
3102
+
3103
+ await Promise.resolve();
3104
+ await Promise.resolve();
3105
+ expect(active).toBe(1);
3106
+
3107
+ watcher.emit("companies/indigo/queued.md");
3108
+ clock.advance(0);
3109
+ await Promise.resolve();
3110
+ await Promise.resolve();
3111
+ expect(maxConcurrent).toBe(1);
3112
+
3113
+ releasePoll();
3114
+ for (let i = 0; i < 8; i++) await Promise.resolve();
3115
+ clock.advance(0);
3116
+ for (let i = 0; i < 8; i++) await Promise.resolve();
3117
+
3118
+ triggerShutdown();
3119
+ await loop;
3120
+
3121
+ expect(maxConcurrent).toBe(1);
3122
+ const targetedCall = runPass.mock.calls.find((c) =>
3123
+ (c[0] as string[]).includes("--company"),
3124
+ );
3125
+ expect(targetedCall?.[0]).toEqual([
3126
+ "--company",
3127
+ "indigo",
3128
+ "--direction",
3129
+ "push",
3130
+ "--hq-root",
3131
+ "/tmp/hq",
3132
+ ]);
3133
+ });
3134
+
3059
3135
  it("poll-still-runs: the poll loop fires passes independent of the watcher", async () => {
3060
3136
  const watcher = makeWatcherStub();
3061
3137
  let triggerShutdown = () => {};
@@ -3813,7 +3889,7 @@ describe("readPinnedPrefixes", () => {
3813
3889
  });
3814
3890
 
3815
3891
  // ---------------------------------------------------------------------------
3816
- // Operation lock — one-shot sync takes it; the watch runner is exempt.
3892
+ // Operation lock — sync operations serialize per root.
3817
3893
  // ---------------------------------------------------------------------------
3818
3894
  describe("runRunnerWithLoop — operation lock", () => {
3819
3895
  const HQ = "/tmp/hq-oplock";
@@ -3931,14 +4007,29 @@ describe("runRunnerWithLoop — operation lock", () => {
3931
4007
  expect(out).toContain("reindex");
3932
4008
  }, 20_000);
3933
4009
 
3934
- it("the watch runner is EXEMPT runs despite a held lock and never takes it", async () => {
4010
+ it("F03: watch runner refuses when another operation holds the root lock", async () => {
3935
4011
  const lp = writeLiveHolder("sync");
3936
4012
  const watcher = makeWatcherStub();
3937
4013
  let triggerShutdown = () => {};
3938
4014
  const runPass = vi.fn().mockResolvedValue(0);
4015
+ const errs: string[] = [];
4016
+ const spy = vi
4017
+ .spyOn(process.stderr, "write")
4018
+ .mockImplementation((chunk: string | Uint8Array) => {
4019
+ errs.push(String(chunk));
4020
+ return true;
4021
+ });
3939
4022
 
3940
4023
  const loop = runRunnerWithLoop(
3941
- ["--companies", "--watch", "--event-push", "--hq-root", HQ],
4024
+ [
4025
+ "--companies",
4026
+ "--watch",
4027
+ "--event-push",
4028
+ "--hq-root",
4029
+ HQ,
4030
+ "--lock-timeout",
4031
+ "0",
4032
+ ],
3942
4033
  {
3943
4034
  runPass,
3944
4035
  clock: new FakeClock(),
@@ -3953,14 +4044,18 @@ describe("runRunnerWithLoop — operation lock", () => {
3953
4044
 
3954
4045
  await Promise.resolve();
3955
4046
  await Promise.resolve();
3956
- // It started and ran a pass even though the lock is held → not blocked.
3957
- expect(watcher.started).toBe(true);
3958
- expect(runPass).toHaveBeenCalled();
4047
+ if (watcher.started || runPass.mock.calls.length > 0) {
4048
+ triggerShutdown();
4049
+ }
4050
+ const code = await loop;
4051
+ spy.mockRestore();
3959
4052
 
3960
- triggerShutdown();
3961
- await loop;
4053
+ expect(code).toBe(OPERATION_LOCKED_EXIT);
4054
+ expect(watcher.started).toBe(false);
4055
+ expect(runPass).not.toHaveBeenCalled();
4056
+ expect(errs.join("")).toContain("sync");
3962
4057
 
3963
- // The pre-existing holder lock is untouched → the watcher never took it.
4058
+ // The pre-existing holder lock is untouched.
3964
4059
  const held = JSON.parse(fs.readFileSync(lp, "utf8"));
3965
4060
  expect(held.pid).toBe(1);
3966
4061
  expect(held.command).toBe("sync");
@@ -4147,6 +4242,198 @@ describe("runRunnerWithLoop — Phase 3 event-sync wiring (US-017/018/019)", ()
4147
4242
  await loop;
4148
4243
  });
4149
4244
 
4245
+ it("F04: mixed-route watcher batches publish only routes that were pushed", async () => {
4246
+ const publishBatch = vi.fn();
4247
+ const startEventSync = vi.fn().mockResolvedValue({
4248
+ publishBatch,
4249
+ receiver: { start: async () => {}, dispose: async () => {}, connected: true },
4250
+ ownDeviceId: "dev-test",
4251
+ dispose: vi.fn().mockResolvedValue(undefined),
4252
+ });
4253
+ const runPass = vi.fn().mockResolvedValue(0);
4254
+ const { loop, watcher, clock, shutdown } = runLoop({
4255
+ claims: ENROLLED_CLAIMS,
4256
+ startEventSync,
4257
+ runPass,
4258
+ });
4259
+ await microtasks();
4260
+ runPass.mockClear();
4261
+
4262
+ watcher.emit("companies/indigo/a.md", {
4263
+ paths: new Map([
4264
+ ["/tmp/hq/companies/indigo/a.md", "companies/indigo/a.md"],
4265
+ ["/tmp/hq/companies/acme/b.md", "companies/acme/b.md"],
4266
+ ["/tmp/hq/notes/personal.md", "notes/personal.md"],
4267
+ ]),
4268
+ });
4269
+ clock.advance(0);
4270
+ await microtasks();
4271
+
4272
+ const pushedRoutes = new Set<string>();
4273
+ let pushedEverything = false;
4274
+ for (const call of runPass.mock.calls) {
4275
+ const passArgv = call[0] as string[];
4276
+ const directionIdx = passArgv.indexOf("--direction");
4277
+ if (passArgv[directionIdx + 1] !== "push") continue;
4278
+ const companyIdx = passArgv.indexOf("--company");
4279
+ if (companyIdx >= 0) {
4280
+ pushedRoutes.add(`company:${passArgv[companyIdx + 1]}`);
4281
+ } else if (passArgv.includes("--companies")) {
4282
+ pushedEverything = true;
4283
+ }
4284
+ }
4285
+ const publishedRelPaths = publishBatch.mock.calls.flatMap((call) => {
4286
+ const batch = call[0] as { paths: Map<string, string> };
4287
+ return [...batch.paths.values()];
4288
+ });
4289
+ const publishedWithoutPush = publishedRelPaths.filter((relPath) => {
4290
+ if (pushedEverything) return false;
4291
+ const route = routeChangeToTarget(relPath);
4292
+ if (!route) return true;
4293
+ if (route.kind === "company") {
4294
+ return !pushedRoutes.has(`company:${route.slug}`);
4295
+ }
4296
+ return !pushedRoutes.has("personal");
4297
+ });
4298
+
4299
+ expect(publishedRelPaths).toContain("companies/indigo/a.md");
4300
+ expect(publishedWithoutPush).toEqual([]);
4301
+
4302
+ shutdown();
4303
+ await loop;
4304
+ });
4305
+
4306
+ it("R-F04F05: preserves queued watcher push targets across overlap", async () => {
4307
+ const publishBatch = vi.fn();
4308
+ const startEventSync = vi.fn().mockResolvedValue({
4309
+ publishBatch,
4310
+ receiver: { start: async () => {}, dispose: async () => {}, connected: true },
4311
+ ownDeviceId: "dev-test",
4312
+ dispose: vi.fn().mockResolvedValue(undefined),
4313
+ });
4314
+ let receiverSync:
4315
+ | ((ctx: {
4316
+ event: PushEvent;
4317
+ signal: AbortSignal;
4318
+ }) => Promise<void>)
4319
+ | null = null;
4320
+ const watcher = makeBatchWatcherStub();
4321
+ const clock = new FakeClock();
4322
+ let triggerShutdown = () => {};
4323
+ let releasePoll: () => void = () => {};
4324
+ const pollGate = new Promise<void>((resolve) => {
4325
+ releasePoll = resolve;
4326
+ });
4327
+ let passCount = 0;
4328
+ const runPass = vi.fn().mockImplementation(async () => {
4329
+ passCount += 1;
4330
+ if (passCount === 1) await pollGate;
4331
+ return 0;
4332
+ });
4333
+
4334
+ const loop = runRunnerWithLoop(
4335
+ ["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"],
4336
+ {
4337
+ runPass,
4338
+ clock,
4339
+ createWatcher: () => watcher,
4340
+ createReceiver: ({ syncFn }) => {
4341
+ receiverSync = syncFn;
4342
+ return {
4343
+ connected: true,
4344
+ start: async () => {},
4345
+ dispose: async () => {},
4346
+ };
4347
+ },
4348
+ sleep: () => new Promise<void>(() => {}),
4349
+ onShutdownSignal: (handler) => {
4350
+ triggerShutdown = handler;
4351
+ return () => {};
4352
+ },
4353
+ getIdTokenClaims: () => ENROLLED_CLAIMS as never,
4354
+ getAccessToken: async () => "jwt-test",
4355
+ startEventSync,
4356
+ },
4357
+ );
4358
+ await microtasks();
4359
+ expect(passCount).toBe(1);
4360
+ expect(receiverSync).toBeTruthy();
4361
+
4362
+ const indigoBatch = {
4363
+ paths: new Map([
4364
+ ["/tmp/hq/companies/indigo/a.md", "companies/indigo/a.md"],
4365
+ ]),
4366
+ };
4367
+ const betaBatch = {
4368
+ paths: new Map([
4369
+ ["/tmp/hq/companies/beta/b.md", "companies/beta/b.md"],
4370
+ ]),
4371
+ };
4372
+ watcher.emit("companies/indigo/a.md", indigoBatch);
4373
+ clock.advance(0);
4374
+ await microtasks();
4375
+ watcher.emit("companies/beta/b.md", betaBatch);
4376
+ clock.advance(0);
4377
+ await microtasks();
4378
+
4379
+ const remotePull = receiverSync!({
4380
+ event: {
4381
+ kind: "upsert",
4382
+ relativePath: "companies/acme/remote.md",
4383
+ contentHash: "sha256:remote",
4384
+ mtime: "2026-06-18T12:00:00.000Z",
4385
+ originDeviceId: "peer-device",
4386
+ originTenantId: "tenant-acme",
4387
+ sequenceNumber: 7,
4388
+ eventTimestamp: "2026-06-18T12:00:00.000Z",
4389
+ },
4390
+ signal: new AbortController().signal,
4391
+ });
4392
+ await microtasks();
4393
+
4394
+ releasePoll();
4395
+ await remotePull;
4396
+ await microtasks();
4397
+ clock.advance(0);
4398
+ await microtasks();
4399
+
4400
+ const passArgvs = runPass.mock.calls.map((call) => call[0] as string[]);
4401
+ expect(passArgvs).toEqual(
4402
+ expect.arrayContaining([
4403
+ ["--company", "indigo", "--direction", "push", "--hq-root", "/tmp/hq"],
4404
+ ["--company", "beta", "--direction", "push", "--hq-root", "/tmp/hq"],
4405
+ ["--company", "acme", "--direction", "pull", "--hq-root", "/tmp/hq"],
4406
+ ]),
4407
+ );
4408
+
4409
+ const pushedRoutes = new Set<string>();
4410
+ for (const passArgv of passArgvs) {
4411
+ const directionIdx = passArgv.indexOf("--direction");
4412
+ if (passArgv[directionIdx + 1] !== "push") continue;
4413
+ const companyIdx = passArgv.indexOf("--company");
4414
+ if (companyIdx >= 0) pushedRoutes.add(`company:${passArgv[companyIdx + 1]}`);
4415
+ }
4416
+ const publishedRelPaths = publishBatch.mock.calls.flatMap((call) => {
4417
+ const batch = call[0] as { paths: Map<string, string> };
4418
+ return [...batch.paths.values()];
4419
+ });
4420
+ const publishedWithoutPush = publishedRelPaths.filter((relPath) => {
4421
+ const route = routeChangeToTarget(relPath);
4422
+ return route?.kind !== "company" || !pushedRoutes.has(`company:${route.slug}`);
4423
+ });
4424
+
4425
+ expect(publishedRelPaths).toEqual(
4426
+ expect.arrayContaining([
4427
+ "companies/indigo/a.md",
4428
+ "companies/beta/b.md",
4429
+ ]),
4430
+ );
4431
+ expect(publishedWithoutPush).toEqual([]);
4432
+
4433
+ triggerShutdown();
4434
+ await loop;
4435
+ });
4436
+
4150
4437
  it("falls back to a synthesized single-path batch when the watcher emits a bare path", async () => {
4151
4438
  const publishBatch = vi.fn();
4152
4439
  const startEventSync = vi.fn().mockResolvedValue({