@indigoai-us/hq-cloud 6.15.0 → 6.15.2

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 (329) hide show
  1. package/dist/bin/sync-mutation.d.ts +16 -0
  2. package/dist/bin/sync-mutation.d.ts.map +1 -0
  3. package/dist/bin/sync-mutation.js +60 -0
  4. package/dist/bin/sync-mutation.js.map +1 -0
  5. package/dist/bin/sync-mutation.test.d.ts +2 -0
  6. package/dist/bin/sync-mutation.test.d.ts.map +1 -0
  7. package/dist/bin/sync-mutation.test.js +165 -0
  8. package/dist/bin/sync-mutation.test.js.map +1 -0
  9. package/dist/bin/sync-runner-company.d.ts +8 -0
  10. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  11. package/dist/bin/sync-runner-company.js +16 -0
  12. package/dist/bin/sync-runner-company.js.map +1 -1
  13. package/dist/bin/sync-runner-company.test.d.ts +2 -0
  14. package/dist/bin/sync-runner-company.test.d.ts.map +1 -0
  15. package/dist/bin/sync-runner-company.test.js +36 -0
  16. package/dist/bin/sync-runner-company.test.js.map +1 -0
  17. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  18. package/dist/bin/sync-runner-watch-loop.js +98 -8
  19. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  20. package/dist/bin/sync-runner.d.ts +17 -0
  21. package/dist/bin/sync-runner.d.ts.map +1 -1
  22. package/dist/bin/sync-runner.js.map +1 -1
  23. package/dist/bin/sync-runner.test.js +109 -0
  24. package/dist/bin/sync-runner.test.js.map +1 -1
  25. package/dist/cli/conflict-recovery.test.d.ts +2 -0
  26. package/dist/cli/conflict-recovery.test.d.ts.map +1 -0
  27. package/dist/cli/conflict-recovery.test.js +201 -0
  28. package/dist/cli/conflict-recovery.test.js.map +1 -0
  29. package/dist/cli/conflict.d.ts +60 -0
  30. package/dist/cli/conflict.d.ts.map +1 -1
  31. package/dist/cli/conflict.js +333 -0
  32. package/dist/cli/conflict.js.map +1 -1
  33. package/dist/cli/sync.d.ts +35 -0
  34. package/dist/cli/sync.d.ts.map +1 -1
  35. package/dist/cli/sync.js +100 -0
  36. package/dist/cli/sync.js.map +1 -1
  37. package/dist/cli/sync.test.js +85 -1
  38. package/dist/cli/sync.test.js.map +1 -1
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/skill-telemetry.d.ts +6 -0
  44. package/dist/skill-telemetry.d.ts.map +1 -1
  45. package/dist/skill-telemetry.js +14 -2
  46. package/dist/skill-telemetry.js.map +1 -1
  47. package/dist/skill-telemetry.test.js +79 -0
  48. package/dist/skill-telemetry.test.js.map +1 -1
  49. package/dist/sync/candidate-uploader.d.ts +88 -0
  50. package/dist/sync/candidate-uploader.d.ts.map +1 -0
  51. package/dist/sync/candidate-uploader.js +212 -0
  52. package/dist/sync/candidate-uploader.js.map +1 -0
  53. package/dist/sync/candidate-uploader.test.d.ts +2 -0
  54. package/dist/sync/candidate-uploader.test.d.ts.map +1 -0
  55. package/dist/sync/candidate-uploader.test.js +132 -0
  56. package/dist/sync/candidate-uploader.test.js.map +1 -0
  57. package/dist/sync/delta-client.d.ts +73 -0
  58. package/dist/sync/delta-client.d.ts.map +1 -0
  59. package/dist/sync/delta-client.js +201 -0
  60. package/dist/sync/delta-client.js.map +1 -0
  61. package/dist/sync/delta-client.test.d.ts +2 -0
  62. package/dist/sync/delta-client.test.d.ts.map +1 -0
  63. package/dist/sync/delta-client.test.js +97 -0
  64. package/dist/sync/delta-client.test.js.map +1 -0
  65. package/dist/sync/durable-apply.d.ts +76 -0
  66. package/dist/sync/durable-apply.d.ts.map +1 -0
  67. package/dist/sync/durable-apply.js +530 -0
  68. package/dist/sync/durable-apply.js.map +1 -0
  69. package/dist/sync/durable-apply.test.d.ts +2 -0
  70. package/dist/sync/durable-apply.test.d.ts.map +1 -0
  71. package/dist/sync/durable-apply.test.js +180 -0
  72. package/dist/sync/durable-apply.test.js.map +1 -0
  73. package/dist/sync/event-sync.d.ts +33 -1
  74. package/dist/sync/event-sync.d.ts.map +1 -1
  75. package/dist/sync/event-sync.js +149 -1
  76. package/dist/sync/event-sync.js.map +1 -1
  77. package/dist/sync/event-sync.test.js +142 -1
  78. package/dist/sync/event-sync.test.js.map +1 -1
  79. package/dist/sync/index.d.ts +2 -0
  80. package/dist/sync/index.d.ts.map +1 -1
  81. package/dist/sync/index.js +1 -0
  82. package/dist/sync/index.js.map +1 -1
  83. package/dist/sync/multipart-uploader.d.ts +99 -0
  84. package/dist/sync/multipart-uploader.d.ts.map +1 -0
  85. package/dist/sync/multipart-uploader.js +447 -0
  86. package/dist/sync/multipart-uploader.js.map +1 -0
  87. package/dist/sync/multipart-uploader.test.d.ts +2 -0
  88. package/dist/sync/multipart-uploader.test.d.ts.map +1 -0
  89. package/dist/sync/multipart-uploader.test.js +119 -0
  90. package/dist/sync/multipart-uploader.test.js.map +1 -0
  91. package/dist/sync/mutation-client.d.ts +85 -0
  92. package/dist/sync/mutation-client.d.ts.map +1 -0
  93. package/dist/sync/mutation-client.js +245 -0
  94. package/dist/sync/mutation-client.js.map +1 -0
  95. package/dist/sync/mutation-client.test.d.ts +2 -0
  96. package/dist/sync/mutation-client.test.d.ts.map +1 -0
  97. package/dist/sync/mutation-client.test.js +51 -0
  98. package/dist/sync/mutation-client.test.js.map +1 -0
  99. package/dist/sync/push-receiver.d.ts +45 -0
  100. package/dist/sync/push-receiver.d.ts.map +1 -1
  101. package/dist/sync/push-receiver.js +101 -0
  102. package/dist/sync/push-receiver.js.map +1 -1
  103. package/dist/sync/push-receiver.test.js +54 -2
  104. package/dist/sync/push-receiver.test.js.map +1 -1
  105. package/dist/sync/scope-inventory-client.d.ts +69 -0
  106. package/dist/sync/scope-inventory-client.d.ts.map +1 -0
  107. package/dist/sync/scope-inventory-client.js +210 -0
  108. package/dist/sync/scope-inventory-client.js.map +1 -0
  109. package/dist/sync/scope-inventory-client.test.d.ts +2 -0
  110. package/dist/sync/scope-inventory-client.test.d.ts.map +1 -0
  111. package/dist/sync/scope-inventory-client.test.js +94 -0
  112. package/dist/sync/scope-inventory-client.test.js.map +1 -0
  113. package/dist/sync/snapshot-client.d.ts +98 -0
  114. package/dist/sync/snapshot-client.d.ts.map +1 -0
  115. package/dist/sync/snapshot-client.js +402 -0
  116. package/dist/sync/snapshot-client.js.map +1 -0
  117. package/dist/sync/snapshot-client.test.d.ts +2 -0
  118. package/dist/sync/snapshot-client.test.d.ts.map +1 -0
  119. package/dist/sync/snapshot-client.test.js +169 -0
  120. package/dist/sync/snapshot-client.test.js.map +1 -0
  121. package/dist/sync/uploader-finalization.d.ts +97 -0
  122. package/dist/sync/uploader-finalization.d.ts.map +1 -0
  123. package/dist/sync/uploader-finalization.js +273 -0
  124. package/dist/sync/uploader-finalization.js.map +1 -0
  125. package/dist/sync/uploader-finalization.test.d.ts +2 -0
  126. package/dist/sync/uploader-finalization.test.d.ts.map +1 -0
  127. package/dist/sync/uploader-finalization.test.js +92 -0
  128. package/dist/sync/uploader-finalization.test.js.map +1 -0
  129. package/dist/telemetry.d.ts +11 -1
  130. package/dist/telemetry.d.ts.map +1 -1
  131. package/dist/telemetry.js +21 -2
  132. package/dist/telemetry.js.map +1 -1
  133. package/dist/telemetry.test.js +80 -0
  134. package/dist/telemetry.test.js.map +1 -1
  135. package/package.json +6 -1
  136. package/.claude/policies/hq-cloud-esm-cannot-spy-fs-builtins.md +0 -30
  137. package/.claude/policies/hq-cloud-strip-types-no-parameter-properties.md +0 -22
  138. package/.github/workflows/ci.yml +0 -84
  139. package/.github/workflows/publish.yml +0 -56
  140. package/.github/workflows/unreleased-commits-nag.yml +0 -256
  141. package/eslint.config.js +0 -67
  142. package/pnpm-workspace.yaml +0 -2
  143. package/scripts/presign-transport-e2e.mjs +0 -250
  144. package/scripts/vault-rebaseline.sh +0 -323
  145. package/scripts/vault-rescue.sh +0 -332
  146. package/src/active-company.test.ts +0 -188
  147. package/src/active-company.ts +0 -168
  148. package/src/agent-codex-instructions.test.ts +0 -332
  149. package/src/agent-codex-instructions.ts +0 -309
  150. package/src/auth.ts +0 -146
  151. package/src/backup-prune.test.ts +0 -98
  152. package/src/backup-prune.ts +0 -182
  153. package/src/bin/backup-prune-runner.ts +0 -33
  154. package/src/bin/rescue-runner.ts +0 -25
  155. package/src/bin/sync-runner-company.ts +0 -695
  156. package/src/bin/sync-runner-events.test.ts +0 -143
  157. package/src/bin/sync-runner-events.ts +0 -55
  158. package/src/bin/sync-runner-planning.test.ts +0 -311
  159. package/src/bin/sync-runner-planning.ts +0 -258
  160. package/src/bin/sync-runner-rollup.test.ts +0 -37
  161. package/src/bin/sync-runner-rollup.ts +0 -97
  162. package/src/bin/sync-runner-telemetry.ts +0 -15
  163. package/src/bin/sync-runner-watch-loop.ts +0 -1235
  164. package/src/bin/sync-runner-watch-routes.test.ts +0 -71
  165. package/src/bin/sync-runner-watch-routes.ts +0 -184
  166. package/src/bin/sync-runner.test.ts +0 -8767
  167. package/src/bin/sync-runner.ts +0 -2190
  168. package/src/cli/accept.ts +0 -124
  169. package/src/cli/conflict.ts +0 -119
  170. package/src/cli/doctor.test.ts +0 -581
  171. package/src/cli/doctor.ts +0 -642
  172. package/src/cli/index.ts +0 -49
  173. package/src/cli/invite.test.ts +0 -250
  174. package/src/cli/invite.ts +0 -214
  175. package/src/cli/promote.ts +0 -157
  176. package/src/cli/reindex-knowledge.test.ts +0 -307
  177. package/src/cli/reindex-knowledge.ts +0 -450
  178. package/src/cli/reindex.test.ts +0 -957
  179. package/src/cli/reindex.ts +0 -979
  180. package/src/cli/rescue-classify-ordering.test.ts +0 -548
  181. package/src/cli/rescue-clone-diagnostics.test.ts +0 -120
  182. package/src/cli/rescue-core.ts +0 -3011
  183. package/src/cli/rescue-drift-reconcile.test.ts +0 -179
  184. package/src/cli/rescue-drop-dir-symlink.test.ts +0 -224
  185. package/src/cli/rescue-exec-bit-preserve.test.ts +0 -187
  186. package/src/cli/rescue-hq-root-guard.test.ts +0 -232
  187. package/src/cli/rescue-journal-reconcile.test.ts +0 -215
  188. package/src/cli/rescue-mtime-preserve.test.ts +0 -203
  189. package/src/cli/rescue-settings-reconcile.test.ts +0 -637
  190. package/src/cli/rescue-snapshot.test.ts +0 -57
  191. package/src/cli/rescue-snapshot.ts +0 -51
  192. package/src/cli/rescue.reindex.test.ts +0 -63
  193. package/src/cli/rescue.test.ts +0 -131
  194. package/src/cli/rescue.ts +0 -182
  195. package/src/cli/share.test.ts +0 -7843
  196. package/src/cli/share.ts +0 -3663
  197. package/src/cli/sync-scope.test.ts +0 -652
  198. package/src/cli/sync.test.ts +0 -5207
  199. package/src/cli/sync.ts +0 -3470
  200. package/src/cli/tombstones.ts +0 -106
  201. package/src/cli/watch-event-push-conflict.test.ts +0 -234
  202. package/src/client-info.test.ts +0 -214
  203. package/src/client-info.ts +0 -121
  204. package/src/cognito-auth.test.ts +0 -712
  205. package/src/cognito-auth.ts +0 -1422
  206. package/src/company-resolver.test.ts +0 -618
  207. package/src/company-resolver.ts +0 -521
  208. package/src/context.test.ts +0 -583
  209. package/src/context.ts +0 -378
  210. package/src/daemon-worker.ts +0 -26
  211. package/src/daemon.ts +0 -99
  212. package/src/entity-resolver.test.ts +0 -315
  213. package/src/entity-resolver.ts +0 -180
  214. package/src/ignore.test.ts +0 -466
  215. package/src/ignore.ts +0 -469
  216. package/src/index.ts +0 -439
  217. package/src/journal.test.ts +0 -968
  218. package/src/journal.ts +0 -765
  219. package/src/lib/cloud-authoritative.test.ts +0 -45
  220. package/src/lib/cloud-authoritative.ts +0 -59
  221. package/src/lib/conflict-file.ts +0 -86
  222. package/src/lib/conflict-index.ts +0 -289
  223. package/src/lib/conflict.test.ts +0 -348
  224. package/src/lib/describe-error.test.ts +0 -100
  225. package/src/lib/describe-error.ts +0 -58
  226. package/src/lib/exit-codes.ts +0 -24
  227. package/src/lib/machine-id.test.ts +0 -231
  228. package/src/lib/machine-id.ts +0 -175
  229. package/src/lib/net-errors.test.ts +0 -65
  230. package/src/lib/net-errors.ts +0 -86
  231. package/src/lib/readlink-safe.test.ts +0 -43
  232. package/src/lib/readlink-safe.ts +0 -29
  233. package/src/local-path-codec.test.ts +0 -138
  234. package/src/local-path-codec.ts +0 -161
  235. package/src/machine-auth.test.ts +0 -1323
  236. package/src/manifest-reconcile.test.ts +0 -1123
  237. package/src/manifest-reconcile.ts +0 -518
  238. package/src/object-io.test.ts +0 -1221
  239. package/src/object-io.ts +0 -1306
  240. package/src/operation-lock.test.ts +0 -484
  241. package/src/operation-lock.ts +0 -680
  242. package/src/outcome-telemetry.test.ts +0 -498
  243. package/src/outcome-telemetry.ts +0 -639
  244. package/src/personal-vault-exclusions.test.ts +0 -308
  245. package/src/personal-vault-exclusions.ts +0 -354
  246. package/src/personal-vault.test.ts +0 -756
  247. package/src/personal-vault.ts +0 -496
  248. package/src/prefix-coalesce.test.ts +0 -240
  249. package/src/prefix-coalesce.ts +0 -273
  250. package/src/public-surface.test.ts +0 -117
  251. package/src/qmd-reindex.test.ts +0 -877
  252. package/src/qmd-reindex.ts +0 -842
  253. package/src/read-only-state-dir.test.ts +0 -188
  254. package/src/remote-pull.test.ts +0 -1130
  255. package/src/remote-pull.ts +0 -618
  256. package/src/s3.symlink-materialize.test.ts +0 -492
  257. package/src/s3.test.ts +0 -1789
  258. package/src/s3.ts +0 -1532
  259. package/src/schemas/signal-types.test.ts +0 -82
  260. package/src/schemas/signal-types.ts +0 -38
  261. package/src/schemas/source-channels.test.ts +0 -82
  262. package/src/schemas/source-channels.ts +0 -53
  263. package/src/scope-shrink.test.ts +0 -633
  264. package/src/scope-shrink.ts +0 -481
  265. package/src/signals/get.test.ts +0 -310
  266. package/src/signals/get.ts +0 -75
  267. package/src/signals/internals.ts +0 -195
  268. package/src/signals/list.test.ts +0 -420
  269. package/src/signals/list.ts +0 -79
  270. package/src/signals/parse.ts +0 -8
  271. package/src/signals/types.ts +0 -91
  272. package/src/skill-telemetry.test.ts +0 -1825
  273. package/src/skill-telemetry.ts +0 -1439
  274. package/src/sources/get.test.ts +0 -293
  275. package/src/sources/get.ts +0 -66
  276. package/src/sources/internals.ts +0 -198
  277. package/src/sources/list.test.ts +0 -402
  278. package/src/sources/list.ts +0 -84
  279. package/src/sources/parse.ts +0 -43
  280. package/src/sources/types.ts +0 -84
  281. package/src/sync/event-sync.test.ts +0 -594
  282. package/src/sync/event-sync.ts +0 -545
  283. package/src/sync/feature-flags.test.ts +0 -378
  284. package/src/sync/feature-flags.ts +0 -62
  285. package/src/sync/index.ts +0 -76
  286. package/src/sync/lease-client.test.ts +0 -128
  287. package/src/sync/lease-client.ts +0 -207
  288. package/src/sync/logger.test.ts +0 -242
  289. package/src/sync/logger.ts +0 -79
  290. package/src/sync/metrics.test.ts +0 -462
  291. package/src/sync/metrics.ts +0 -213
  292. package/src/sync/pull-scope.ts +0 -265
  293. package/src/sync/push-event.test.ts +0 -266
  294. package/src/sync/push-event.ts +0 -224
  295. package/src/sync/push-receiver.test.ts +0 -566
  296. package/src/sync/push-receiver.ts +0 -1048
  297. package/src/sync/push-transport.ts +0 -231
  298. package/src/sync/realtime-rollout.test.ts +0 -86
  299. package/src/sync/realtime-rollout.ts +0 -262
  300. package/src/sync/state-store.test.ts +0 -194
  301. package/src/sync/state-store.ts +0 -727
  302. package/src/sync-core.ts +0 -58
  303. package/src/sync-progress.test.ts +0 -94
  304. package/src/sync-progress.ts +0 -140
  305. package/src/telemetry-events.test.ts +0 -88
  306. package/src/telemetry-events.ts +0 -205
  307. package/src/telemetry.test.ts +0 -1280
  308. package/src/telemetry.ts +0 -1109
  309. package/src/types.ts +0 -314
  310. package/src/vault-client.test.ts +0 -1380
  311. package/src/vault-client.ts +0 -1694
  312. package/src/version.ts +0 -24
  313. package/src/watch-roots.test.ts +0 -278
  314. package/src/watch-roots.ts +0 -162
  315. package/src/watcher-event-gate.test.ts +0 -212
  316. package/src/watcher.test.ts +0 -1079
  317. package/src/watcher.ts +0 -1741
  318. package/test/e2e/sync/cross-tenant-isolation.test.ts +0 -630
  319. package/test/e2e/sync/skill-telemetry-oversized-transcript.test.ts +0 -124
  320. package/test/e2e/sync/transient-company-leg.test.ts +0 -384
  321. package/test/e2e/sync/windows-unreadable-link-leg.test.ts +0 -191
  322. package/test/e2e/watcher-real-chokidar.test.ts +0 -165
  323. package/test/e2e/watcher-recursive-backend.test.ts +0 -181
  324. package/test/e2e/watcher-scoped-coverage.test.ts +0 -381
  325. package/test/invite-flow.integration.test.ts +0 -244
  326. package/test/joiner-manifest-reconcile.integration.test.ts +0 -322
  327. package/test/share-sync.integration.test.ts +0 -213
  328. package/tsconfig.json +0 -19
  329. package/vitest.config.ts +0 -22
@@ -1,566 +0,0 @@
1
- /**
2
- * US-009 — push-receiver unit tests.
3
- *
4
- * Covers the five required behaviors from the PRD AC, against the SQS-backed
5
- * {@link SqsPushReceiver} (mocked SQS client — NO real AWS) and the
6
- * {@link InMemoryPushReceiver} (in-process fanout analogue):
7
- *
8
- * 1. subscribe lifecycle — start opens the poll loop / subscription, dispose
9
- * stops it; double-start is a no-op; flag-OFF stays dormant.
10
- * 2. dedupe — an event with sequenceNumber <= the highest seen
11
- * for its path is skipped; per-path counters are independent.
12
- * 3. reconnect-replay — a transient receiveMessage failure backs off and
13
- * resumes; SQS retention redelivers the buffered events; dedupe absorbs
14
- * redelivery (in-memory: disconnect buffers, reconnect drains).
15
- * 4. flag gating — dormant when the per-tenant flag is OFF; no queue
16
- * polled, connected stays false.
17
- * 5. dispose drain — dispose aborts + awaits the in-flight syncFn, then
18
- * disconnects; the loop terminates with no leaked timers.
19
- *
20
- * The live per-client SQS queue is NOT provisioned yet (server provisioning
21
- * Lambda is an unbuilt follow-up — see references.md). Every test injects a
22
- * fake SQS client / in-memory fanout.
23
- */
24
-
25
- import { describe, expect, it } from "vitest";
26
-
27
- import { encodePushEvent, type PushEvent } from "./push-event.js";
28
- import {
29
- InMemoryFanout,
30
- InMemoryPushReceiver,
31
- NoopPushReceiver,
32
- SqsPushReceiver,
33
- createPushReceiver,
34
- type SqsClientLike,
35
- type SqsMessageLike,
36
- type SyncEngineFn,
37
- } from "./push-receiver.js";
38
-
39
- // ─── Fixtures ────────────────────────────────────────────────────────────────
40
-
41
- const TENANT = "tenant-indigo";
42
- const QUEUE_URL = "https://sqs.us-east-1.amazonaws.com/123456789012/sync-push-indigo-deviceA";
43
-
44
- function makeEvent(overrides: Partial<PushEvent> = {}): PushEvent {
45
- return {
46
- relativePath: "companies/indigo/notes.md",
47
- kind: "upsert",
48
- contentHash:
49
- "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
50
- mtime: "2026-05-21T12:34:56.000Z",
51
- originDeviceId: "device-A",
52
- originTenantId: TENANT,
53
- sequenceNumber: 1,
54
- eventTimestamp: "2026-05-21T12:34:56.500Z",
55
- ...overrides,
56
- };
57
- }
58
-
59
- function msg(event: PushEvent, receiptHandle = `rh-${event.sequenceNumber}`): SqsMessageLike {
60
- return {
61
- Body: encodePushEvent(event),
62
- ReceiptHandle: receiptHandle,
63
- MessageId: `m-${event.relativePath}-${event.sequenceNumber}`,
64
- };
65
- }
66
-
67
- /**
68
- * A scriptable fake SQS client. `enqueue` adds batches that successive
69
- * `receiveMessage` calls drain (one batch per call); once drained, it blocks
70
- * on the abort signal (modeling an idle long-poll) so dispose ends it cleanly.
71
- * `failNext(n)` makes the next `n` receiveMessage calls reject (transient).
72
- */
73
- class FakeSqs implements SqsClientLike {
74
- private batches: SqsMessageLike[][] = [];
75
- private failCount = 0;
76
- readonly deleted: string[] = [];
77
- receiveCalls = 0;
78
-
79
- enqueue(messages: SqsMessageLike[]): void {
80
- this.batches.push(messages);
81
- }
82
- failNext(n: number): void {
83
- this.failCount += n;
84
- }
85
-
86
- async receiveMessage(args: {
87
- queueUrl: string;
88
- maxMessages: number;
89
- waitTimeSeconds: number;
90
- signal: AbortSignal;
91
- }): Promise<{ messages: SqsMessageLike[] }> {
92
- this.receiveCalls += 1;
93
- if (this.failCount > 0) {
94
- this.failCount -= 1;
95
- throw new Error("simulated SQS receive failure");
96
- }
97
- const next = this.batches.shift();
98
- if (next && next.length > 0) {
99
- return { messages: next };
100
- }
101
- // Idle long-poll: resolve empty on the next tick (or promptly on abort).
102
- return new Promise((resolve) => {
103
- if (args.signal.aborted) return resolve({ messages: [] });
104
- const t = setTimeout(() => resolve({ messages: [] }), 5);
105
- (t as { unref?: () => void }).unref?.();
106
- args.signal.addEventListener(
107
- "abort",
108
- () => {
109
- clearTimeout(t);
110
- resolve({ messages: [] });
111
- },
112
- { once: true },
113
- );
114
- });
115
- }
116
-
117
- async deleteMessage(args: { queueUrl: string; receiptHandle: string }): Promise<void> {
118
- this.deleted.push(args.receiptHandle);
119
- }
120
- }
121
-
122
- /** Wait until `predicate()` is true or `timeoutMs` elapses. */
123
- async function waitFor(predicate: () => boolean, timeoutMs = 1000): Promise<void> {
124
- const start = Date.now();
125
- while (!predicate()) {
126
- if (Date.now() - start > timeoutMs) {
127
- throw new Error("waitFor: timed out");
128
- }
129
- await new Promise((r) => setTimeout(r, 5));
130
- }
131
- }
132
-
133
- // fast, abortable sleep so backoff doesn't slow the suite
134
- const fastSleep = (ms: number, signal: AbortSignal): Promise<void> =>
135
- new Promise((resolve) => {
136
- if (signal.aborted) return resolve();
137
- const t = setTimeout(resolve, Math.min(ms, 5));
138
- signal.addEventListener("abort", () => { clearTimeout(t); resolve(); }, { once: true });
139
- });
140
-
141
- // ─── SqsPushReceiver ─────────────────────────────────────────────────────────
142
-
143
- describe("US-009: SqsPushReceiver — subscribe lifecycle", () => {
144
- it("start opens the poll loop and processes a received event via syncFn, then deletes it", async () => {
145
- const sqs = new FakeSqs();
146
- const seen: string[] = [];
147
- const syncFn: SyncEngineFn = async (ctx) => {
148
- seen.push(ctx.event.relativePath);
149
- };
150
- sqs.enqueue([msg(makeEvent({ sequenceNumber: 1 }))]);
151
-
152
- const receiver = new SqsPushReceiver({
153
- tenantId: TENANT,
154
- queueUrl: QUEUE_URL,
155
- sqs,
156
- syncFn,
157
- sleep: fastSleep,
158
- });
159
-
160
- await receiver.start();
161
- expect(receiver.connected).toBe(true);
162
- await waitFor(() => receiver.processedCount === 1);
163
- expect(seen).toEqual(["companies/indigo/notes.md"]);
164
- expect(sqs.deleted).toContain("rh-1");
165
-
166
- await receiver.dispose();
167
- expect(receiver.connected).toBe(false);
168
- });
169
-
170
- it("double-start is a no-op (single poll loop)", async () => {
171
- const sqs = new FakeSqs();
172
- const receiver = new SqsPushReceiver({
173
- tenantId: TENANT,
174
- queueUrl: QUEUE_URL,
175
- sqs,
176
- syncFn: async () => undefined,
177
- sleep: fastSleep,
178
- });
179
- await receiver.start();
180
- await receiver.start();
181
- // Let a couple poll iterations happen; a duplicated loop would roughly
182
- // double the receive call rate. We just assert it stays connected + clean.
183
- await new Promise((r) => setTimeout(r, 30));
184
- expect(receiver.connected).toBe(true);
185
- await receiver.dispose();
186
- });
187
-
188
- it("subscribes ONLY to its own tenant's queue (cross-tenant isolation boundary)", async () => {
189
- const sqs = new FakeSqs();
190
- const urls: string[] = [];
191
- const spy: SqsClientLike = {
192
- receiveMessage: (a) => {
193
- urls.push(a.queueUrl);
194
- return sqs.receiveMessage(a);
195
- },
196
- deleteMessage: (a) => sqs.deleteMessage(a),
197
- };
198
- const receiver = new SqsPushReceiver({
199
- tenantId: TENANT,
200
- queueUrl: QUEUE_URL,
201
- sqs: spy,
202
- syncFn: async () => undefined,
203
- sleep: fastSleep,
204
- });
205
- await receiver.start();
206
- await waitFor(() => urls.length >= 1);
207
- await receiver.dispose();
208
- expect(new Set(urls)).toEqual(new Set([QUEUE_URL]));
209
- });
210
-
211
- it("constructor rejects empty tenantId / queueUrl", () => {
212
- expect(
213
- () =>
214
- new SqsPushReceiver({
215
- tenantId: "",
216
- queueUrl: QUEUE_URL,
217
- sqs: new FakeSqs(),
218
- syncFn: async () => undefined,
219
- }),
220
- ).toThrow(/tenantId is required/);
221
- expect(
222
- () =>
223
- new SqsPushReceiver({
224
- tenantId: TENANT,
225
- queueUrl: "",
226
- sqs: new FakeSqs(),
227
- syncFn: async () => undefined,
228
- }),
229
- ).toThrow(/queueUrl is required/);
230
- });
231
- });
232
-
233
- describe("US-009: SqsPushReceiver — dedupe", () => {
234
- it("skips an event whose sequenceNumber <= the highest seen for that path", async () => {
235
- const sqs = new FakeSqs();
236
- const processed: number[] = [];
237
- const syncFn: SyncEngineFn = async (ctx) => {
238
- processed.push(ctx.event.sequenceNumber);
239
- };
240
- // seq 1, then a redelivery of 1, then 2 — same path.
241
- sqs.enqueue([msg(makeEvent({ sequenceNumber: 1 }), "rh-a")]);
242
- sqs.enqueue([msg(makeEvent({ sequenceNumber: 1 }), "rh-b")]);
243
- sqs.enqueue([msg(makeEvent({ sequenceNumber: 2 }), "rh-c")]);
244
-
245
- const receiver = new SqsPushReceiver({
246
- tenantId: TENANT,
247
- queueUrl: QUEUE_URL,
248
- sqs,
249
- syncFn,
250
- sleep: fastSleep,
251
- });
252
- await receiver.start();
253
- await waitFor(() => receiver.processedCount === 2);
254
- await receiver.dispose();
255
-
256
- expect(processed).toEqual([1, 2]);
257
- expect(receiver.dedupedCount).toBe(1);
258
- // The deduped redelivery is still deleted (we don't need it again).
259
- expect(sqs.deleted).toContain("rh-b");
260
- });
261
-
262
- it("dedupe is per-path — same sequence number on different paths both process", async () => {
263
- const sqs = new FakeSqs();
264
- const processed: string[] = [];
265
- const syncFn: SyncEngineFn = async (ctx) => {
266
- processed.push(ctx.event.relativePath);
267
- };
268
- sqs.enqueue([
269
- msg(makeEvent({ relativePath: "companies/indigo/a.md", sequenceNumber: 5 }), "rh-1"),
270
- msg(makeEvent({ relativePath: "companies/indigo/b.md", sequenceNumber: 5 }), "rh-2"),
271
- ]);
272
- const receiver = new SqsPushReceiver({
273
- tenantId: TENANT,
274
- queueUrl: QUEUE_URL,
275
- sqs,
276
- syncFn,
277
- sleep: fastSleep,
278
- });
279
- await receiver.start();
280
- await waitFor(() => receiver.processedCount === 2);
281
- await receiver.dispose();
282
- expect(new Set(processed)).toEqual(
283
- new Set(["companies/indigo/a.md", "companies/indigo/b.md"]),
284
- );
285
- expect(receiver.dedupedCount).toBe(0);
286
- });
287
-
288
- it("drops an undecodable message at the wire boundary without crashing the loop", async () => {
289
- const sqs = new FakeSqs();
290
- sqs.enqueue([{ Body: "{not json", ReceiptHandle: "rh-bad", MessageId: "m-bad" }]);
291
- sqs.enqueue([msg(makeEvent({ sequenceNumber: 1 }), "rh-good")]);
292
- const receiver = new SqsPushReceiver({
293
- tenantId: TENANT,
294
- queueUrl: QUEUE_URL,
295
- sqs,
296
- syncFn: async () => undefined,
297
- sleep: fastSleep,
298
- });
299
- await receiver.start();
300
- await waitFor(() => receiver.processedCount === 1);
301
- await receiver.dispose();
302
- expect(receiver.decodeFailureCount).toBe(1);
303
- expect(sqs.deleted).toContain("rh-bad"); // poison message deleted
304
- expect(sqs.deleted).toContain("rh-good");
305
- });
306
- });
307
-
308
- describe("US-009: SqsPushReceiver — reconnect / catch-up replay", () => {
309
- it("recovers from transient receiveMessage failures (backoff + resume) and replays the queue", async () => {
310
- const sqs = new FakeSqs();
311
- sqs.failNext(2); // two transient disconnects before the message lands
312
- sqs.enqueue([msg(makeEvent({ sequenceNumber: 1 }), "rh-1")]);
313
- const receiver = new SqsPushReceiver({
314
- tenantId: TENANT,
315
- queueUrl: QUEUE_URL,
316
- sqs,
317
- syncFn: async () => undefined,
318
- sleep: fastSleep,
319
- reconnect: { initialMs: 1, maxMs: 4, jitter: false },
320
- });
321
- await receiver.start();
322
- await waitFor(() => receiver.processedCount === 1);
323
- await receiver.dispose();
324
- expect(receiver.receiveErrorCount).toBe(2);
325
- expect(sqs.deleted).toContain("rh-1");
326
- });
327
-
328
- it("a syncFn throw does not crash the loop and the next event still processes", async () => {
329
- const sqs = new FakeSqs();
330
- let calls = 0;
331
- const syncFn: SyncEngineFn = async () => {
332
- calls += 1;
333
- if (calls === 1) throw new Error("boom");
334
- };
335
- sqs.enqueue([msg(makeEvent({ relativePath: "companies/indigo/x.md", sequenceNumber: 1 }), "rh-1")]);
336
- sqs.enqueue([msg(makeEvent({ relativePath: "companies/indigo/y.md", sequenceNumber: 1 }), "rh-2")]);
337
- const receiver = new SqsPushReceiver({
338
- tenantId: TENANT,
339
- queueUrl: QUEUE_URL,
340
- sqs,
341
- syncFn,
342
- sleep: fastSleep,
343
- });
344
- await receiver.start();
345
- await waitFor(() => calls === 2);
346
- await receiver.dispose();
347
- // First threw (processedCount not incremented), second succeeded.
348
- expect(receiver.processedCount).toBe(1);
349
- // F10: a failed targeted pull is left undeleted for SQS redelivery; only the
350
- // successful message (rh-2) is acknowledged. The thrown one (rh-1) stays on
351
- // the queue so the visibility-timeout redelivery can retry it.
352
- expect(sqs.deleted).toContain("rh-2");
353
- expect(sqs.deleted).not.toContain("rh-1");
354
- });
355
-
356
- it("F10: leaves failed targeted pull messages undeleted so redelivery retries", async () => {
357
- const sqs = new FakeSqs();
358
- let calls = 0;
359
- const syncFn: SyncEngineFn = async () => {
360
- calls += 1;
361
- if (calls === 1) throw new Error("targeted pull failed");
362
- };
363
- const event = makeEvent({
364
- relativePath: "companies/indigo/retry-after-failure.md",
365
- sequenceNumber: 7,
366
- });
367
- sqs.enqueue([msg(event, "rh-first-attempt")]);
368
- sqs.enqueue([msg(event, "rh-redelivery")]);
369
-
370
- const receiver = new SqsPushReceiver({
371
- tenantId: TENANT,
372
- queueUrl: QUEUE_URL,
373
- sqs,
374
- syncFn,
375
- sleep: fastSleep,
376
- });
377
- await receiver.start();
378
- await waitFor(() => sqs.deleted.includes("rh-redelivery"));
379
- await receiver.dispose();
380
-
381
- expect(calls).toBe(2);
382
- expect(receiver.processedCount).toBe(1);
383
- expect(receiver.dedupedCount).toBe(0);
384
- expect(sqs.deleted).not.toContain("rh-first-attempt");
385
- expect(sqs.deleted).toContain("rh-redelivery");
386
- });
387
-
388
- it("F29: bounds dedupe state by evicting least-recently-used paths", async () => {
389
- const sqs = new FakeSqs();
390
- const processed: string[] = [];
391
- const syncFn: SyncEngineFn = async (ctx) => {
392
- processed.push(ctx.event.relativePath);
393
- };
394
-
395
- sqs.enqueue([msg(makeEvent({ relativePath: "companies/indigo/a.md", sequenceNumber: 1 }), "rh-a1")]);
396
- sqs.enqueue([msg(makeEvent({ relativePath: "companies/indigo/b.md", sequenceNumber: 1 }), "rh-b1")]);
397
- sqs.enqueue([msg(makeEvent({ relativePath: "companies/indigo/c.md", sequenceNumber: 1 }), "rh-c1")]);
398
- sqs.enqueue([msg(makeEvent({ relativePath: "companies/indigo/a.md", sequenceNumber: 1 }), "rh-a-redelivered")]);
399
-
400
- const receiver = new SqsPushReceiver({
401
- tenantId: TENANT,
402
- queueUrl: QUEUE_URL,
403
- sqs,
404
- syncFn,
405
- sleep: fastSleep,
406
- dedupeMaxPaths: 2,
407
- });
408
- await receiver.start();
409
- await waitFor(() => receiver.processedCount === 4);
410
- await receiver.dispose();
411
-
412
- expect(processed).toEqual([
413
- "companies/indigo/a.md",
414
- "companies/indigo/b.md",
415
- "companies/indigo/c.md",
416
- "companies/indigo/a.md",
417
- ]);
418
- expect(receiver.dedupedCount).toBe(0);
419
- expect(sqs.deleted).toContain("rh-a-redelivered");
420
- });
421
- });
422
-
423
- describe("U15: SqsPushReceiver — server-granted handoff", () => {
424
- it("starts from supplied server queue material with no tenant client gate", async () => {
425
- const sqs = new FakeSqs();
426
- sqs.enqueue([msg(makeEvent({ sequenceNumber: 1 }))]);
427
- const receiver = new SqsPushReceiver({
428
- tenantId: TENANT,
429
- queueUrl: QUEUE_URL,
430
- sqs,
431
- syncFn: async () => undefined,
432
- sleep: fastSleep,
433
- });
434
- await receiver.start();
435
- await waitFor(() => receiver.processedCount === 1);
436
- expect(receiver.connected).toBe(true);
437
- await receiver.dispose();
438
- });
439
-
440
- });
441
-
442
- describe("US-009: SqsPushReceiver — dispose drain", () => {
443
- it("aborts and drains the in-flight syncFn, then disconnects", async () => {
444
- const sqs = new FakeSqs();
445
- let aborted = false;
446
- let resolveSync: (() => void) | null = null;
447
- const syncFn: SyncEngineFn = (ctx) =>
448
- new Promise<void>((resolve) => {
449
- resolveSync = resolve;
450
- ctx.signal.addEventListener("abort", () => {
451
- aborted = true;
452
- resolve();
453
- });
454
- });
455
- sqs.enqueue([msg(makeEvent({ sequenceNumber: 1 }))]);
456
- const receiver = new SqsPushReceiver({
457
- tenantId: TENANT,
458
- queueUrl: QUEUE_URL,
459
- sqs,
460
- syncFn,
461
- sleep: fastSleep,
462
- disposeDrainMs: 200,
463
- });
464
- await receiver.start();
465
- await waitFor(() => resolveSync !== null);
466
- await receiver.dispose();
467
- expect(aborted).toBe(true);
468
- expect(receiver.connected).toBe(false);
469
- });
470
-
471
- it("dispose is idempotent", async () => {
472
- const sqs = new FakeSqs();
473
- const receiver = new SqsPushReceiver({
474
- tenantId: TENANT,
475
- queueUrl: QUEUE_URL,
476
- sqs,
477
- syncFn: async () => undefined,
478
- sleep: fastSleep,
479
- });
480
- await receiver.start();
481
- await Promise.all([receiver.dispose(), receiver.dispose()]);
482
- expect(receiver.connected).toBe(false);
483
- });
484
- });
485
-
486
- // ─── InMemoryPushReceiver (transport analogue) ───────────────────────────────
487
-
488
- describe("US-009: InMemoryPushReceiver — disconnect buffering + reconnect drain", () => {
489
- it("buffers events during disconnect and drains them on reconnect, deduping replays", async () => {
490
- const fanout = new InMemoryFanout();
491
- const processed: number[] = [];
492
- const receiver = new InMemoryPushReceiver({
493
- tenantId: TENANT,
494
- fanout,
495
- syncFn: async (ctx) => {
496
- processed.push(ctx.event.sequenceNumber);
497
- },
498
- });
499
- await receiver.start();
500
-
501
- fanout.publish(encodePushEvent(makeEvent({ sequenceNumber: 1 })));
502
- await waitFor(() => receiver.processedCount === 1);
503
-
504
- receiver.simulateDisconnect();
505
- expect(receiver.connected).toBe(false);
506
- // Published while disconnected — buffered, not processed.
507
- fanout.publish(encodePushEvent(makeEvent({ sequenceNumber: 2 })));
508
- fanout.publish(encodePushEvent(makeEvent({ sequenceNumber: 1 }))); // stale replay
509
- expect(receiver.bufferedCount).toBe(2);
510
-
511
- receiver.simulateReconnect();
512
- await waitFor(() => receiver.processedCount === 2);
513
- await receiver.dispose();
514
-
515
- // seq 2 processed; the stale seq-1 replay deduped.
516
- expect(processed).toEqual([1, 2]);
517
- expect(receiver.dedupedCount).toBe(1);
518
- });
519
-
520
- it("does not derive activation from a tenant flag", async () => {
521
- const fanout = new InMemoryFanout();
522
- let calls = 0;
523
- const receiver = new InMemoryPushReceiver({
524
- tenantId: TENANT,
525
- fanout,
526
- syncFn: async () => {
527
- calls += 1;
528
- },
529
- });
530
- await receiver.start();
531
- fanout.publish(encodePushEvent(makeEvent()));
532
- await new Promise((r) => setTimeout(r, 20));
533
- await waitFor(() => calls === 1);
534
- expect(receiver.connected).toBe(true);
535
- expect(calls).toBe(1);
536
- await receiver.dispose();
537
- });
538
- });
539
-
540
- // ─── Factory + Noop ──────────────────────────────────────────────────────────
541
-
542
- describe("US-009: createPushReceiver factory", () => {
543
- it("returns NoopPushReceiver for kind:noop", () => {
544
- const r = createPushReceiver({ kind: "noop" });
545
- expect(r).toBeInstanceOf(NoopPushReceiver);
546
- });
547
-
548
- it("returns SqsPushReceiver when a queue + sqs client are supplied", () => {
549
- const r = createPushReceiver({
550
- tenantId: TENANT,
551
- queueUrl: QUEUE_URL,
552
- sqs: new FakeSqs(),
553
- syncFn: async () => undefined,
554
- });
555
- expect(r).toBeInstanceOf(SqsPushReceiver);
556
- });
557
-
558
- it("NoopPushReceiver flips connected on start/dispose with no work", async () => {
559
- const r = new NoopPushReceiver();
560
- expect(r.connected).toBe(false);
561
- await r.start();
562
- expect(r.connected).toBe(true);
563
- await r.dispose();
564
- expect(r.connected).toBe(false);
565
- });
566
- });