@mycadet/assistant 0.12.11 → 0.13.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 (176) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  3. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  5. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  6. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  7. package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  8. package/openapi.yaml +2 -708
  9. package/package.json +2 -3
  10. package/scripts/sync-web-search-catalog.ts +1 -2
  11. package/src/__tests__/cloud-profile-guard.test.ts +2 -2
  12. package/src/__tests__/config-schema.test.ts +6 -38
  13. package/src/__tests__/conversation-queue.test.ts +45 -6
  14. package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
  15. package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
  16. package/src/__tests__/persona-resolver.test.ts +0 -51
  17. package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
  18. package/src/__tests__/run-due-schedules.test.ts +0 -4
  19. package/src/__tests__/schedule-store.test.ts +0 -12
  20. package/src/__tests__/stt-stream-session.test.ts +4 -4
  21. package/src/__tests__/telephony-tts-guard.test.ts +2 -50
  22. package/src/__tests__/voice-config-update.test.ts +12 -167
  23. package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
  24. package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
  25. package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
  26. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
  27. package/src/calls/resolve-call-tts-provider.ts +6 -25
  28. package/src/calls/telephony-credential-preflight.ts +0 -10
  29. package/src/calls/telephony-tts-capability.ts +1 -17
  30. package/src/cli/commands/tts.ts +1 -2
  31. package/src/cli/index.help.ts +0 -2
  32. package/src/cli/program.ts +0 -2
  33. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
  34. package/src/config/schemas/__tests__/stt.test.ts +10 -14
  35. package/src/config/schemas/services.ts +5 -6
  36. package/src/config/schemas/stt.ts +1 -3
  37. package/src/config/schemas/tts.ts +1 -30
  38. package/src/daemon/daemon-readiness.ts +4 -29
  39. package/src/daemon/lifecycle.ts +0 -31
  40. package/src/daemon/providers-setup.ts +0 -8
  41. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  42. package/src/heartbeat/heartbeat-service.ts +1 -66
  43. package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
  44. package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
  45. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
  46. package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
  47. package/src/live-voice/live-voice-credential-preflight.ts +9 -33
  48. package/src/live-voice/live-voice-session.ts +10 -6
  49. package/src/live-voice/live-voice-tts.ts +1 -8
  50. package/src/plugins/defaults/memory/worker.ts +3 -3
  51. package/src/prompts/persona-resolver.ts +0 -35
  52. package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
  53. package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
  54. package/src/providers/search-provider-catalog.ts +7 -39
  55. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
  56. package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
  57. package/src/providers/speech-to-text/provider-catalog.ts +2 -73
  58. package/src/providers/speech-to-text/resolve.ts +3 -120
  59. package/src/runtime/http-router-types.ts +1 -3
  60. package/src/runtime/http-server.ts +1 -1
  61. package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
  62. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
  63. package/src/runtime/routes/index.ts +0 -4
  64. package/src/runtime/routes/live-voice-routes.test.ts +4 -11
  65. package/src/runtime/routes/live-voice-routes.ts +8 -11
  66. package/src/runtime/routes/secret-routes.ts +0 -6
  67. package/src/runtime/routes/tts-routes.ts +0 -38
  68. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
  69. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
  70. package/src/schedule/schedule-store.ts +0 -7
  71. package/src/schedule/scheduler.ts +0 -5
  72. package/src/schedule/worker.ts +1 -1
  73. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
  74. package/src/stt/daemon-batch-transcriber.ts +4 -38
  75. package/src/stt/types.ts +1 -9
  76. package/src/tools/network/__tests__/web-search.test.ts +3 -250
  77. package/src/tools/network/web-search.ts +4 -165
  78. package/src/tts/__tests__/provider-resolution.test.ts +0 -5
  79. package/src/tts/provider-catalog.ts +0 -4
  80. package/src/tts/types.ts +0 -1
  81. package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
  82. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
  83. package/src/__tests__/migration-export-http.test.ts +0 -557
  84. package/src/__tests__/migration-export-streaming.test.ts +0 -350
  85. package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
  86. package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
  87. package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
  88. package/src/__tests__/migration-import-from-path.test.ts +0 -324
  89. package/src/__tests__/migration-import-from-url.test.ts +0 -588
  90. package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
  91. package/src/__tests__/migration-jobs-status.test.ts +0 -139
  92. package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
  93. package/src/__tests__/migration-transport.test.ts +0 -1089
  94. package/src/__tests__/migration-validate-http.test.ts +0 -634
  95. package/src/__tests__/migration-wizard.test.ts +0 -1395
  96. package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
  97. package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
  98. package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
  99. package/src/__tests__/validation-results-screen.test.ts +0 -1130
  100. package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
  101. package/src/background-wake/background-wake-routes.test.ts +0 -906
  102. package/src/background-wake/cloud-client.test.ts +0 -328
  103. package/src/background-wake/cloud-client.ts +0 -196
  104. package/src/background-wake/next-wake.test.ts +0 -335
  105. package/src/background-wake/next-wake.ts +0 -188
  106. package/src/background-wake/publisher.ts +0 -93
  107. package/src/background-wake/wake-intent-hooks.test.ts +0 -234
  108. package/src/cli/commands/__tests__/backup.test.ts +0 -363
  109. package/src/cli/commands/backup.help.ts +0 -180
  110. package/src/cli/commands/backup.ts +0 -398
  111. package/src/cloud/cloud-speech.test.ts +0 -335
  112. package/src/cloud/cloud-speech.ts +0 -300
  113. package/src/config/cloud-speech-defaults.ts +0 -300
  114. package/src/config/sanitize-for-transfer.ts +0 -111
  115. package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
  116. package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
  117. package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
  118. package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
  119. package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
  120. package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
  121. package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
  122. package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
  123. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
  124. package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
  125. package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
  126. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
  127. package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
  128. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
  129. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
  130. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
  131. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
  132. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
  133. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
  134. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
  135. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
  136. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
  137. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
  138. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
  139. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
  140. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
  141. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
  142. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
  143. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
  144. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
  145. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
  146. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
  147. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
  148. package/src/runtime/migrations/gcs-signed-url.ts +0 -190
  149. package/src/runtime/migrations/job-registry.ts +0 -297
  150. package/src/runtime/migrations/migration-transport.ts +0 -612
  151. package/src/runtime/migrations/migration-wizard.ts +0 -747
  152. package/src/runtime/migrations/origin-mode.ts +0 -42
  153. package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
  154. package/src/runtime/migrations/staged-import-path.ts +0 -116
  155. package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
  156. package/src/runtime/migrations/validation-results-screen.ts +0 -469
  157. package/src/runtime/migrations/vbundle-builder.ts +0 -1382
  158. package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
  159. package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
  160. package/src/runtime/migrations/vbundle-importer.ts +0 -826
  161. package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
  162. package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
  163. package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
  164. package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
  165. package/src/runtime/migrations/vbundle-validator.ts +0 -724
  166. package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
  167. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
  168. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
  169. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
  170. package/src/runtime/routes/background-wake-routes.ts +0 -387
  171. package/src/runtime/routes/migration-routes.ts +0 -2584
  172. package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
  173. package/src/tools/network/cloud-search-proxy.ts +0 -211
  174. package/src/tts/__tests__/cadet-provider.test.ts +0 -253
  175. package/src/tts/providers/cadet-provider.ts +0 -244
  176. package/src/types/tar-stream.d.ts +0 -66
@@ -1,254 +0,0 @@
1
- /**
2
- * Tests for the GCS signed-URL validator.
3
- *
4
- * Covers:
5
- * - A valid V4-signed URL (`X-Goog-Signature=...`) is accepted and the
6
- * returned host/path match the input.
7
- * - A valid V2-signed URL (`Signature=...`) is accepted.
8
- * - `http://` is rejected with reason `scheme`.
9
- * - A non-GCS host is rejected with reason `host`.
10
- * - A GCS URL with no signature query param is rejected with reason
11
- * `missing_signature`; SigV4 (`X-Amz-Signature`) is accepted only for
12
- * hosts on a non-default allowlist, and unsigned URLs are rejected
13
- * everywhere.
14
- * - A GCS URL with a non-default explicit port (e.g. `:1234`, `:80`) is
15
- * rejected with reason `port`. A URL with `:443` is accepted because
16
- * WHATWG URL normalizes the default HTTPS port to an empty port
17
- * string.
18
- * - Path traversal (`/bucket/../foo`) is rejected with reason
19
- * `path_traversal`, even though the URL parser would normalize it.
20
- * - Encoded-slash traversal (`%2e%2e%2fother`), backslash-separator
21
- * traversal (`\..\secret`), uppercase percent encoding, and an
22
- * encoded dot-dot at the start of the path are all rejected.
23
- * - A malformed URL string is rejected with reason `invalid_url`.
24
- */
25
-
26
- import { describe, expect, test } from "bun:test";
27
-
28
- import { validateGcsSignedUrl } from "../gcs-signed-url.js";
29
-
30
- describe("validateGcsSignedUrl", () => {
31
- test("accepts a V4-signed URL", () => {
32
- const url =
33
- "https://storage.googleapis.com/my-bucket/path/to/object.tgz" +
34
- "?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
35
- "&X-Goog-Credential=service-account%40project.iam.gserviceaccount.com" +
36
- "&X-Goog-Date=20260420T000000Z" +
37
- "&X-Goog-Expires=3600" +
38
- "&X-Goog-SignedHeaders=host" +
39
- "&X-Goog-Signature=deadbeef";
40
-
41
- const result = validateGcsSignedUrl(url);
42
- expect(result.ok).toBe(true);
43
- if (result.ok) {
44
- expect(result.host).toBe("storage.googleapis.com");
45
- expect(result.path).toBe("/my-bucket/path/to/object.tgz");
46
- }
47
- });
48
-
49
- test("accepts a V2-signed URL", () => {
50
- const url =
51
- "https://storage.googleapis.com/my-bucket/object.tgz" +
52
- "?GoogleAccessId=service-account%40project.iam.gserviceaccount.com" +
53
- "&Expires=1700000000" +
54
- "&Signature=abc123";
55
-
56
- const result = validateGcsSignedUrl(url);
57
- expect(result.ok).toBe(true);
58
- if (result.ok) {
59
- expect(result.host).toBe("storage.googleapis.com");
60
- expect(result.path).toBe("/my-bucket/object.tgz");
61
- }
62
- });
63
-
64
- test("rejects http:// scheme", () => {
65
- const url =
66
- "http://storage.googleapis.com/my-bucket/object.tgz" +
67
- "?X-Goog-Signature=deadbeef";
68
-
69
- const result = validateGcsSignedUrl(url);
70
- expect(result).toEqual({ ok: false, reason: "scheme" });
71
- });
72
-
73
- test("accepts http:// for a host on a non-default allowlist", () => {
74
- // A non-default allowlist (e.g. local/minikube where the bundle is
75
- // served over http from host.docker.internal) relaxes the scheme check
76
- // to also accept http and skips the explicit-port check.
77
- const url =
78
- "http://host.docker.internal:8000/bucket/object.tgz" +
79
- "?X-Goog-Signature=deadbeef";
80
-
81
- const result = validateGcsSignedUrl(url, {
82
- allowedHosts: ["host.docker.internal"],
83
- });
84
- expect(result.ok).toBe(true);
85
- if (result.ok) {
86
- expect(result.host).toBe("host.docker.internal");
87
- expect(result.path).toBe("/bucket/object.tgz");
88
- }
89
- });
90
-
91
- test("still rejects a host absent from a non-default allowlist", () => {
92
- const url =
93
- "http://evil.internal/bucket/object.tgz" + "?X-Goog-Signature=deadbeef";
94
-
95
- const result = validateGcsSignedUrl(url, {
96
- allowedHosts: ["host.docker.internal"],
97
- });
98
- expect(result).toEqual({ ok: false, reason: "host" });
99
- });
100
-
101
- test("rejects a non-GCS host", () => {
102
- const url =
103
- "https://evil.com/my-bucket/object.tgz" + "?X-Goog-Signature=deadbeef";
104
-
105
- const result = validateGcsSignedUrl(url);
106
- expect(result).toEqual({ ok: false, reason: "host" });
107
- });
108
-
109
- test("rejects a GCS URL with no signature", () => {
110
- const url = "https://storage.googleapis.com/bucket/key";
111
-
112
- const result = validateGcsSignedUrl(url);
113
- expect(result).toEqual({ ok: false, reason: "missing_signature" });
114
- });
115
-
116
- test("accepts a SigV4-signed URL for a host on a non-default allowlist", () => {
117
- // The local/minikube platform serves SigV4 presigned URLs (SeaweedFS)
118
- // over plain http — X-Amz-Signature satisfies the signature-shape
119
- // requirement for explicitly allowlisted hosts.
120
- const url =
121
- "http://host.docker.internal:8000/bucket/object.tgz" +
122
- "?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=deadbeef";
123
-
124
- const result = validateGcsSignedUrl(url, {
125
- allowedHosts: ["host.docker.internal"],
126
- });
127
- expect(result.ok).toBe(true);
128
- if (result.ok) {
129
- expect(result.host).toBe("host.docker.internal");
130
- expect(result.path).toBe("/bucket/object.tgz");
131
- }
132
- });
133
-
134
- test("rejects an unsigned URL even on a non-default allowlist", () => {
135
- const url = "http://host.docker.internal:8000/bucket/object.tgz";
136
-
137
- const result = validateGcsSignedUrl(url, {
138
- allowedHosts: ["host.docker.internal"],
139
- });
140
- expect(result).toEqual({ ok: false, reason: "missing_signature" });
141
- });
142
-
143
- test("rejects X-Amz-Signature on the default GCS allowlist", () => {
144
- // SigV4 is only meaningful for allowlisted S3-compatible hosts; a GCS
145
- // URL signed with the wrong scheme's param is not a GCS presigned URL.
146
- const url =
147
- "https://storage.googleapis.com/bucket/key?X-Amz-Signature=deadbeef";
148
-
149
- const result = validateGcsSignedUrl(url);
150
- expect(result).toEqual({ ok: false, reason: "missing_signature" });
151
- });
152
-
153
- test("still rejects traversal in a SigV4-signed allowlisted URL", () => {
154
- const url =
155
- "http://host.docker.internal:8000/bucket/../secret" +
156
- "?X-Amz-Signature=deadbeef";
157
-
158
- const result = validateGcsSignedUrl(url, {
159
- allowedHosts: ["host.docker.internal"],
160
- });
161
- expect(result).toEqual({ ok: false, reason: "path_traversal" });
162
- });
163
-
164
- test("rejects a GCS URL with a non-default port (1234)", () => {
165
- const url =
166
- "https://storage.googleapis.com:1234/bucket/key" +
167
- "?X-Goog-Signature=deadbeef";
168
-
169
- const result = validateGcsSignedUrl(url);
170
- expect(result).toEqual({ ok: false, reason: "port" });
171
- });
172
-
173
- test("rejects a GCS URL with HTTP default port (:80)", () => {
174
- const url =
175
- "https://storage.googleapis.com:80/bucket/key" +
176
- "?X-Goog-Signature=deadbeef";
177
-
178
- const result = validateGcsSignedUrl(url);
179
- expect(result).toEqual({ ok: false, reason: "port" });
180
- });
181
-
182
- test("accepts a GCS URL with explicit :443 (normalized to empty port)", () => {
183
- // WHATWG URL normalizes `:443` to an empty port string for HTTPS,
184
- // so the port check (`parsed.port !== ""`) does not reject it.
185
- // A correctly-issued signed URL with an explicit default port is
186
- // therefore unaffected by the port guard.
187
- const url =
188
- "https://storage.googleapis.com:443/my-bucket/object.tgz" +
189
- "?X-Goog-Signature=deadbeef";
190
-
191
- // Sanity-check the WHATWG normalization assumption.
192
- expect(new URL(url).port).toBe("");
193
-
194
- const result = validateGcsSignedUrl(url);
195
- expect(result.ok).toBe(true);
196
- if (result.ok) {
197
- expect(result.host).toBe("storage.googleapis.com");
198
- expect(result.path).toBe("/my-bucket/object.tgz");
199
- }
200
- });
201
-
202
- test("rejects path traversal", () => {
203
- const url =
204
- "https://storage.googleapis.com/bucket/../foo" +
205
- "?X-Goog-Signature=deadbeef";
206
-
207
- const result = validateGcsSignedUrl(url);
208
- expect(result).toEqual({ ok: false, reason: "path_traversal" });
209
- });
210
-
211
- test("rejects path traversal with encoded slash (%2e%2e%2fother)", () => {
212
- const url =
213
- "https://storage.googleapis.com/bucket/%2e%2e%2fother" +
214
- "?X-Goog-Signature=deadbeef";
215
-
216
- const result = validateGcsSignedUrl(url);
217
- expect(result).toEqual({ ok: false, reason: "path_traversal" });
218
- });
219
-
220
- test("rejects path traversal with backslash separators", () => {
221
- // Note: the WHATWG URL parser normalizes `\` to `/` for special
222
- // schemes like `https:`, so `new URL()` would collapse this to
223
- // `/secret`. The raw-path guard must still reject it.
224
- const url =
225
- "https://storage.googleapis.com/bucket\\..\\secret" +
226
- "?X-Goog-Signature=deadbeef";
227
-
228
- const result = validateGcsSignedUrl(url);
229
- expect(result).toEqual({ ok: false, reason: "path_traversal" });
230
- });
231
-
232
- test("rejects path traversal with uppercase percent encoding", () => {
233
- const url =
234
- "https://storage.googleapis.com/bucket/%2E%2E/other" +
235
- "?X-Goog-Signature=deadbeef";
236
-
237
- const result = validateGcsSignedUrl(url);
238
- expect(result).toEqual({ ok: false, reason: "path_traversal" });
239
- });
240
-
241
- test("rejects encoded dot-dot at the start of the path", () => {
242
- const url =
243
- "https://storage.googleapis.com/%2e%2e/bucket/key" +
244
- "?X-Goog-Signature=deadbeef";
245
-
246
- const result = validateGcsSignedUrl(url);
247
- expect(result).toEqual({ ok: false, reason: "path_traversal" });
248
- });
249
-
250
- test("rejects a malformed URL", () => {
251
- const result = validateGcsSignedUrl("not a url at all");
252
- expect(result).toEqual({ ok: false, reason: "invalid_url" });
253
- });
254
- });
@@ -1,346 +0,0 @@
1
- /**
2
- * Tests for the in-memory migration job registry.
3
- *
4
- * Covered:
5
- * - Happy path: start -> poll pending -> poll running -> poll complete with result.
6
- * - Failure path: runner throws -> job ends in `failed` with mapped error.
7
- * - `kFetchBodyError` path: tagged thrown object maps to `fetch_failed` with
8
- * optional `upstreamStatus` preserved.
9
- * - Concurrent limit: second export while one is pending/running rejects with
10
- * `JobAlreadyInProgressError`; a job of the other type is allowed alongside.
11
- * - TTL sweep: completed job older than the TTL is evicted; job that is not
12
- * yet past TTL is retained.
13
- */
14
-
15
- import { describe, expect, test } from "bun:test";
16
-
17
- import {
18
- JobAlreadyInProgressError,
19
- type MigrationJob,
20
- MigrationJobRegistry,
21
- } from "../job-registry.js";
22
-
23
- const kFetchBodyError = Symbol.for("cadet.migrationImport.fetchBodyError");
24
-
25
- /** Spin the microtask queue N times so `queueMicrotask`-scheduled work runs. */
26
- async function flushMicrotasks(n = 4): Promise<void> {
27
- for (let i = 0; i < n; i++) {
28
- await Promise.resolve();
29
- }
30
- }
31
-
32
- describe("MigrationJobRegistry", () => {
33
- test("happy path: pending -> running -> complete with result", async () => {
34
- const registry = new MigrationJobRegistry();
35
-
36
- let release: (value: string) => void = () => {};
37
- const gate = new Promise<string>((resolve) => {
38
- release = resolve;
39
- });
40
-
41
- const job = registry.startJob("export", async (): Promise<string> => {
42
- return await gate;
43
- });
44
-
45
- // Returned synchronously in pending state.
46
- expect(job.status).toBe("pending");
47
- expect(job.id).toMatch(
48
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
49
- );
50
- expect(job.type).toBe("export");
51
- expect(job.createdAt).toBeGreaterThan(0);
52
- expect(job.startedAt).toBeUndefined();
53
- expect(job.completedAt).toBeUndefined();
54
-
55
- // Let the microtask run -> status flips to running with `startedAt` set.
56
- await flushMicrotasks();
57
- const running = registry.getJob(job.id);
58
- expect(running).not.toBeNull();
59
- expect(running!.status).toBe("running");
60
- expect(running!.startedAt).toBeGreaterThanOrEqual(running!.createdAt);
61
-
62
- // Resolve the runner and let it settle.
63
- release("the-result");
64
- await flushMicrotasks();
65
-
66
- const done = registry.getJob(job.id);
67
- expect(done).not.toBeNull();
68
- expect(done!.status).toBe("complete");
69
- expect(done!.result).toBe("the-result");
70
- expect(done!.completedAt).toBeGreaterThanOrEqual(done!.startedAt ?? 0);
71
- expect(done!.error).toBeUndefined();
72
-
73
- // `listJobs` sees it too.
74
- expect(registry.listJobs().map((j) => j.id)).toContain(job.id);
75
- });
76
-
77
- test("failure path: runner throws -> failed with mapped error", async () => {
78
- const registry = new MigrationJobRegistry();
79
- const job = registry.startJob("export", async () => {
80
- throw new Error("boom");
81
- });
82
-
83
- await flushMicrotasks();
84
-
85
- const done = registry.getJob(job.id);
86
- expect(done).not.toBeNull();
87
- expect(done!.status).toBe("failed");
88
- expect(done!.error).toEqual({ code: "unknown", message: "boom" });
89
- expect(done!.result).toBeUndefined();
90
- expect(done!.completedAt).toBeGreaterThanOrEqual(done!.startedAt ?? 0);
91
- });
92
-
93
- test("runner-provided `code` overrides the default `unknown`", async () => {
94
- const registry = new MigrationJobRegistry();
95
- const job = registry.startJob("import", async () => {
96
- const err = new Error("invalid manifest") as Error & { code: string };
97
- err.code = "invalid_manifest";
98
- throw err;
99
- });
100
-
101
- await flushMicrotasks();
102
-
103
- const done = registry.getJob(job.id);
104
- expect(done!.status).toBe("failed");
105
- expect(done!.error).toEqual({
106
- code: "invalid_manifest",
107
- message: "invalid manifest",
108
- });
109
- });
110
-
111
- test("kFetchBodyError path: tagged error maps to `fetch_failed`", async () => {
112
- const registry = new MigrationJobRegistry();
113
- const job = registry.startJob("import", async () => {
114
- const err = new Error("upstream hung up") as Error & {
115
- upstreamStatus?: number;
116
- };
117
- (err as unknown as Record<symbol, boolean>)[kFetchBodyError] = true;
118
- err.upstreamStatus = 502;
119
- throw err;
120
- });
121
-
122
- await flushMicrotasks();
123
-
124
- const done = registry.getJob(job.id);
125
- expect(done).not.toBeNull();
126
- expect(done!.status).toBe("failed");
127
- expect(done!.error).toEqual({
128
- code: "fetch_failed",
129
- message: "upstream hung up",
130
- upstreamStatus: 502,
131
- });
132
- });
133
-
134
- test("kFetchBodyError without upstreamStatus omits the field", async () => {
135
- const registry = new MigrationJobRegistry();
136
- const job = registry.startJob("import", async () => {
137
- const err = new Error("aborted") as Error;
138
- (err as unknown as Record<symbol, boolean>)[kFetchBodyError] = true;
139
- throw err;
140
- });
141
-
142
- await flushMicrotasks();
143
-
144
- const done = registry.getJob(job.id)!;
145
- expect(done.status).toBe("failed");
146
- expect(done.error).toEqual({
147
- code: "fetch_failed",
148
- message: "aborted",
149
- });
150
- expect(done.error?.upstreamStatus).toBeUndefined();
151
- });
152
-
153
- test("concurrent limit: second same-type job rejects; other type is allowed", async () => {
154
- const registry = new MigrationJobRegistry();
155
-
156
- let release: () => void = () => {};
157
- const gate = new Promise<void>((resolve) => {
158
- release = resolve;
159
- });
160
-
161
- // First export is pending (microtask hasn't run yet) — second export
162
- // immediately rejects on the still-pending first job.
163
- const first = registry.startJob("export", async () => {
164
- await gate;
165
- });
166
- expect(first.status).toBe("pending");
167
-
168
- expect(() => registry.startJob("export", async () => {})).toThrow(
169
- JobAlreadyInProgressError,
170
- );
171
-
172
- // Advance to running — a same-type attempt still rejects.
173
- await flushMicrotasks();
174
- expect(registry.getJob(first.id)!.status).toBe("running");
175
-
176
- let caught: unknown = null;
177
- try {
178
- registry.startJob("export", async () => {});
179
- } catch (err) {
180
- caught = err;
181
- }
182
- expect(caught).toBeInstanceOf(JobAlreadyInProgressError);
183
- expect((caught as JobAlreadyInProgressError).existingJobId).toBe(first.id);
184
-
185
- // A job of the *other* type is allowed in parallel.
186
- const importJob = registry.startJob("import", async () => "ok");
187
- expect(importJob.type).toBe("import");
188
- expect(importJob.id).not.toBe(first.id);
189
-
190
- // Settle both before leaving the test so no promises leak.
191
- release();
192
- await flushMicrotasks();
193
- expect(registry.getJob(first.id)!.status).toBe("complete");
194
- expect(registry.getJob(importJob.id)!.status).toBe("complete");
195
-
196
- // And now a new export is allowed since nothing is in flight.
197
- const third = registry.startJob("export", async () => "again");
198
- expect(third.status).toBe("pending");
199
- await flushMicrotasks();
200
- expect(registry.getJob(third.id)!.status).toBe("complete");
201
- });
202
-
203
- test("returned job snapshots are decoupled from internal registry state", async () => {
204
- const registry = new MigrationJobRegistry();
205
-
206
- let release: () => void = () => {};
207
- const gate = new Promise<void>((resolve) => {
208
- release = resolve;
209
- });
210
-
211
- const first = registry.startJob("export", async () => {
212
- await gate;
213
- });
214
-
215
- // The synchronous return of startJob must be a snapshot, not the
216
- // internal record. Mutating it (e.g. to simulate a caller stomping on
217
- // `status`) must not unblock the single-in-flight invariant while the
218
- // runner is still active.
219
- first.status = "complete";
220
- first.completedAt = Date.now();
221
-
222
- await flushMicrotasks();
223
- // Internal state is still running — the snapshot mutation did not leak.
224
- expect(registry.getJob(first.id)!.status).toBe("running");
225
-
226
- // Attempting a second same-type job must still reject.
227
- expect(() => registry.startJob("export", async () => {})).toThrow(
228
- JobAlreadyInProgressError,
229
- );
230
-
231
- // getJob snapshots are also decoupled: mutating the return value of
232
- // getJob does not leak into listJobs or subsequent getJob calls.
233
- const polled = registry.getJob(first.id)!;
234
- polled.status = "failed";
235
- expect(registry.getJob(first.id)!.status).toBe("running");
236
- expect(registry.listJobs().find((j) => j.id === first.id)!.status).toBe(
237
- "running",
238
- );
239
-
240
- // Settle.
241
- release();
242
- await flushMicrotasks();
243
- expect(registry.getJob(first.id)!.status).toBe("complete");
244
- });
245
-
246
- test("TTL sweep: completed jobs older than TTL are evicted", async () => {
247
- const registry = new MigrationJobRegistry();
248
- registry.completedJobTtlMs = 1_000; // 1s for test purposes
249
-
250
- const job = registry.startJob("export", async () => "done");
251
- await flushMicrotasks();
252
- const completed = registry.getJob(job.id)!;
253
- expect(completed.status).toBe("complete");
254
-
255
- // Before TTL: sweep is a no-op.
256
- const originalNow = Date.now;
257
- try {
258
- Date.now = () => (completed.completedAt ?? 0) + 500;
259
- registry.sweep();
260
- expect(registry.getJob(job.id)).not.toBeNull();
261
- expect(registry.listJobs().map((j) => j.id)).toContain(job.id);
262
-
263
- // Past TTL: sweep evicts the completed job.
264
- Date.now = () => (completed.completedAt ?? 0) + 2_000;
265
- registry.sweep();
266
- expect(registry.getJob(job.id)).toBeNull();
267
- expect(registry.listJobs().map((j) => j.id)).not.toContain(job.id);
268
- } finally {
269
- Date.now = originalNow;
270
- }
271
- });
272
-
273
- test("TTL sweep: failed jobs are also evicted past TTL", async () => {
274
- const registry = new MigrationJobRegistry();
275
- registry.completedJobTtlMs = 500;
276
-
277
- const job = registry.startJob("import", async () => {
278
- throw new Error("nope");
279
- });
280
- await flushMicrotasks();
281
- const failed = registry.getJob(job.id)!;
282
- expect(failed.status).toBe("failed");
283
-
284
- const originalNow = Date.now;
285
- try {
286
- Date.now = () => (failed.completedAt ?? 0) + 10_000;
287
- registry.sweep();
288
- expect(registry.getJob(job.id)).toBeNull();
289
- } finally {
290
- Date.now = originalNow;
291
- }
292
- });
293
-
294
- test("TTL sweep: running jobs are never evicted", async () => {
295
- const registry = new MigrationJobRegistry();
296
- registry.completedJobTtlMs = 1;
297
-
298
- let release: () => void = () => {};
299
- const gate = new Promise<void>((resolve) => {
300
- release = resolve;
301
- });
302
- const job = registry.startJob("export", async () => {
303
- await gate;
304
- });
305
- await flushMicrotasks();
306
- expect(registry.getJob(job.id)!.status).toBe("running");
307
-
308
- const originalNow = Date.now;
309
- try {
310
- Date.now = () => Number.MAX_SAFE_INTEGER;
311
- registry.sweep();
312
- expect(registry.getJob(job.id)).not.toBeNull();
313
- } finally {
314
- Date.now = originalNow;
315
- }
316
-
317
- // Cleanup: let the runner resolve.
318
- release();
319
- await flushMicrotasks();
320
- });
321
-
322
- test("getJob returns null for unknown ids", () => {
323
- const registry = new MigrationJobRegistry();
324
- expect(registry.getJob("nope")).toBeNull();
325
- });
326
-
327
- test("JobAlreadyInProgressError carries the existing job id", () => {
328
- const err = new JobAlreadyInProgressError("export", "abc-123");
329
- expect(err).toBeInstanceOf(Error);
330
- expect(err.name).toBe("JobAlreadyInProgressError");
331
- expect(err.existingJobId).toBe("abc-123");
332
- expect(err.message).toContain("export");
333
- expect(err.message).toContain("abc-123");
334
- });
335
-
336
- test("MigrationJob interface shape is preserved on success", async () => {
337
- const registry = new MigrationJobRegistry();
338
- const job = registry.startJob("export", async () => ({ bytes: 42 }));
339
- await flushMicrotasks();
340
- const done: MigrationJob = registry.getJob(job.id)!;
341
- expect(done.id).toBe(job.id);
342
- expect(done.type).toBe("export");
343
- expect(done.status).toBe("complete");
344
- expect(done.result).toEqual({ bytes: 42 });
345
- });
346
- });
@@ -1,62 +0,0 @@
1
- /**
2
- * Tests for `getOriginMode` — the vbundle manifest `origin.mode` derivation.
3
- *
4
- * Covers:
5
- * - `IS_CLOUD=true` → "managed" (wins over IS_CONTAINERIZED).
6
- * - `IS_CONTAINERIZED=true` (no IS_CLOUD) → "self-hosted-remote".
7
- * - Neither → "self-hosted-local".
8
- * - Regression: a logged-in local assistant holding cloud-proxy
9
- * credentials (platform URL + assistant_api_key) must NOT be classified
10
- * "managed" — every logged-in local has those credentials, and a
11
- * "managed" stamp trips the importer's `secrets_redacted must be true
12
- * when origin.mode is 'managed'` refine, breaking local→platform
13
- * teleport. The helper keys on the IS_CLOUD deployment env only —
14
- * nothing here consults the credential store, so the regression case is
15
- * simply "no env flags set".
16
- */
17
-
18
- import { afterEach, describe, expect, test } from "bun:test";
19
-
20
- import { getOriginMode } from "../origin-mode.js";
21
-
22
- const ORIGINAL_IS_CLOUD = process.env.IS_CLOUD;
23
- const ORIGINAL_IS_CONTAINERIZED = process.env.IS_CONTAINERIZED;
24
-
25
- function restore(name: string, value: string | undefined): void {
26
- if (value === undefined) {
27
- delete process.env[name];
28
- } else {
29
- process.env[name] = value;
30
- }
31
- }
32
-
33
- afterEach(() => {
34
- restore("IS_CLOUD", ORIGINAL_IS_CLOUD);
35
- restore("IS_CONTAINERIZED", ORIGINAL_IS_CONTAINERIZED);
36
- });
37
-
38
- describe("getOriginMode", () => {
39
- test("IS_CLOUD=true → managed", async () => {
40
- process.env.IS_CLOUD = "true";
41
- delete process.env.IS_CONTAINERIZED;
42
- expect(await getOriginMode()).toBe("managed");
43
- });
44
-
45
- test("IS_CLOUD wins over IS_CONTAINERIZED", async () => {
46
- process.env.IS_CLOUD = "1";
47
- process.env.IS_CONTAINERIZED = "true";
48
- expect(await getOriginMode()).toBe("managed");
49
- });
50
-
51
- test("IS_CONTAINERIZED=true without IS_CLOUD → self-hosted-remote", async () => {
52
- delete process.env.IS_CLOUD;
53
- process.env.IS_CONTAINERIZED = "true";
54
- expect(await getOriginMode()).toBe("self-hosted-remote");
55
- });
56
-
57
- test("neither flag → self-hosted-local (incl. logged-in locals with cloud-proxy creds)", async () => {
58
- delete process.env.IS_CLOUD;
59
- delete process.env.IS_CONTAINERIZED;
60
- expect(await getOriginMode()).toBe("self-hosted-local");
61
- });
62
- });