@mycadet/assistant 0.12.10 → 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 (180) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  3. package/node_modules/@mycadet/ces-client/src/http-credentials.ts +18 -1
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  5. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  6. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  7. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  8. package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  9. package/openapi.yaml +2 -708
  10. package/package.json +2 -3
  11. package/scripts/sync-web-search-catalog.ts +1 -2
  12. package/src/__tests__/cloud-profile-guard.test.ts +41 -5
  13. package/src/__tests__/config-schema.test.ts +6 -38
  14. package/src/__tests__/conversation-queue.test.ts +45 -6
  15. package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
  16. package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
  17. package/src/__tests__/persona-resolver.test.ts +0 -51
  18. package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
  19. package/src/__tests__/run-due-schedules.test.ts +0 -4
  20. package/src/__tests__/schedule-store.test.ts +0 -12
  21. package/src/__tests__/stt-stream-session.test.ts +4 -4
  22. package/src/__tests__/telephony-tts-guard.test.ts +2 -50
  23. package/src/__tests__/voice-config-update.test.ts +12 -167
  24. package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
  25. package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
  26. package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
  27. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
  28. package/src/calls/resolve-call-tts-provider.ts +6 -25
  29. package/src/calls/telephony-credential-preflight.ts +0 -10
  30. package/src/calls/telephony-tts-capability.ts +1 -17
  31. package/src/cli/commands/tts.ts +1 -2
  32. package/src/cli/index.help.ts +0 -2
  33. package/src/cli/program.ts +0 -2
  34. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
  35. package/src/config/schemas/__tests__/stt.test.ts +10 -14
  36. package/src/config/schemas/llm.ts +3 -5
  37. package/src/config/schemas/services.ts +5 -6
  38. package/src/config/schemas/stt.ts +1 -3
  39. package/src/config/schemas/tts.ts +1 -30
  40. package/src/daemon/daemon-readiness.ts +4 -29
  41. package/src/daemon/lifecycle.ts +0 -31
  42. package/src/daemon/providers-setup.ts +0 -8
  43. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  44. package/src/heartbeat/heartbeat-service.ts +1 -66
  45. package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
  46. package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
  47. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
  48. package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
  49. package/src/live-voice/live-voice-credential-preflight.ts +9 -33
  50. package/src/live-voice/live-voice-session.ts +10 -6
  51. package/src/live-voice/live-voice-tts.ts +1 -8
  52. package/src/plugins/defaults/memory/worker.ts +3 -3
  53. package/src/prompts/persona-resolver.ts +0 -35
  54. package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
  55. package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
  56. package/src/providers/search-provider-catalog.ts +7 -39
  57. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
  58. package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
  59. package/src/providers/speech-to-text/provider-catalog.ts +2 -73
  60. package/src/providers/speech-to-text/resolve.ts +3 -120
  61. package/src/runtime/http-router-types.ts +1 -3
  62. package/src/runtime/http-server.ts +1 -1
  63. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +13 -11
  64. package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
  65. package/src/runtime/routes/conversation-query-routes.ts +0 -96
  66. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
  67. package/src/runtime/routes/index.ts +0 -4
  68. package/src/runtime/routes/live-voice-routes.test.ts +4 -11
  69. package/src/runtime/routes/live-voice-routes.ts +8 -11
  70. package/src/runtime/routes/secret-routes.ts +0 -6
  71. package/src/runtime/routes/tts-routes.ts +0 -38
  72. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
  73. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
  74. package/src/schedule/schedule-store.ts +0 -7
  75. package/src/schedule/scheduler.ts +0 -5
  76. package/src/schedule/worker.ts +1 -1
  77. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
  78. package/src/stt/daemon-batch-transcriber.ts +4 -38
  79. package/src/stt/types.ts +1 -9
  80. package/src/tools/network/__tests__/web-search.test.ts +3 -250
  81. package/src/tools/network/web-search.ts +4 -165
  82. package/src/tts/__tests__/provider-resolution.test.ts +0 -5
  83. package/src/tts/provider-catalog.ts +0 -4
  84. package/src/tts/types.ts +0 -1
  85. package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
  86. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
  87. package/src/__tests__/migration-export-http.test.ts +0 -557
  88. package/src/__tests__/migration-export-streaming.test.ts +0 -350
  89. package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
  90. package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
  91. package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
  92. package/src/__tests__/migration-import-from-path.test.ts +0 -324
  93. package/src/__tests__/migration-import-from-url.test.ts +0 -588
  94. package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
  95. package/src/__tests__/migration-jobs-status.test.ts +0 -139
  96. package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
  97. package/src/__tests__/migration-transport.test.ts +0 -1089
  98. package/src/__tests__/migration-validate-http.test.ts +0 -634
  99. package/src/__tests__/migration-wizard.test.ts +0 -1395
  100. package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
  101. package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
  102. package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
  103. package/src/__tests__/validation-results-screen.test.ts +0 -1130
  104. package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
  105. package/src/background-wake/background-wake-routes.test.ts +0 -906
  106. package/src/background-wake/cloud-client.test.ts +0 -328
  107. package/src/background-wake/cloud-client.ts +0 -196
  108. package/src/background-wake/next-wake.test.ts +0 -335
  109. package/src/background-wake/next-wake.ts +0 -188
  110. package/src/background-wake/publisher.ts +0 -93
  111. package/src/background-wake/wake-intent-hooks.test.ts +0 -234
  112. package/src/cli/commands/__tests__/backup.test.ts +0 -363
  113. package/src/cli/commands/backup.help.ts +0 -180
  114. package/src/cli/commands/backup.ts +0 -398
  115. package/src/cloud/cloud-speech.test.ts +0 -335
  116. package/src/cloud/cloud-speech.ts +0 -300
  117. package/src/config/cloud-speech-defaults.ts +0 -300
  118. package/src/config/sanitize-for-transfer.ts +0 -111
  119. package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
  120. package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
  121. package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
  122. package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
  123. package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
  124. package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
  125. package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
  126. package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
  127. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
  128. package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
  129. package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
  130. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
  131. package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
  132. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
  133. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
  134. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
  135. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
  136. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
  137. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
  138. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
  139. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
  140. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
  141. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
  142. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
  143. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
  144. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
  145. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
  146. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
  147. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
  148. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
  149. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
  150. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
  151. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
  152. package/src/runtime/migrations/gcs-signed-url.ts +0 -190
  153. package/src/runtime/migrations/job-registry.ts +0 -297
  154. package/src/runtime/migrations/migration-transport.ts +0 -612
  155. package/src/runtime/migrations/migration-wizard.ts +0 -747
  156. package/src/runtime/migrations/origin-mode.ts +0 -42
  157. package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
  158. package/src/runtime/migrations/staged-import-path.ts +0 -116
  159. package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
  160. package/src/runtime/migrations/validation-results-screen.ts +0 -469
  161. package/src/runtime/migrations/vbundle-builder.ts +0 -1382
  162. package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
  163. package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
  164. package/src/runtime/migrations/vbundle-importer.ts +0 -826
  165. package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
  166. package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
  167. package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
  168. package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
  169. package/src/runtime/migrations/vbundle-validator.ts +0 -724
  170. package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
  171. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
  172. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
  173. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
  174. package/src/runtime/routes/background-wake-routes.ts +0 -387
  175. package/src/runtime/routes/migration-routes.ts +0 -2584
  176. package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
  177. package/src/tools/network/cloud-search-proxy.ts +0 -211
  178. package/src/tts/__tests__/cadet-provider.test.ts +0 -253
  179. package/src/tts/providers/cadet-provider.ts +0 -244
  180. package/src/types/tar-stream.d.ts +0 -66
@@ -1,534 +0,0 @@
1
- /**
2
- * Integration tests for POST /v1/migrations/import-from-gcs.
3
- *
4
- * The endpoint kicks off an async import job that fetches a .vbundle from
5
- * a signed GCS URL and streams it through the importer. The handler returns
6
- * 202 with a `job_id`; the test then polls the in-process
7
- * `migrationJobs` registry directly (no separate status endpoint yet — PR 4
8
- * adds that) to assert the job progresses through `pending`/`running` to a
9
- * terminal `complete` or `failed` state with the expected result / error
10
- * mapping.
11
- *
12
- * Covered cases:
13
- * - Happy path: 202 → poll → `complete` with `result.report.summary`.
14
- * - Upstream fetch failure (HTTP 500): job ends `failed` with
15
- * `error.code === "fetch_failed"` and `error.upstreamStatus === 500`.
16
- * - Validation failure (corrupt bundle): job ends `failed` with
17
- * `error.code === "validation_failed"`.
18
- * - Concurrency: second import while the first is in flight → 409 with
19
- * `error.code === "import_in_progress"`.
20
- */
21
-
22
- import {
23
- createReadStream,
24
- existsSync,
25
- mkdirSync,
26
- mkdtempSync,
27
- realpathSync,
28
- rmSync,
29
- writeFileSync,
30
- } from "node:fs";
31
- import { createServer, type Server } from "node:http";
32
- import { tmpdir } from "node:os";
33
- import { join } from "node:path";
34
- import {
35
- afterAll,
36
- afterEach,
37
- beforeAll,
38
- beforeEach,
39
- describe,
40
- expect,
41
- test,
42
- } from "bun:test";
43
-
44
- // ---------------------------------------------------------------------------
45
- // Test isolation: per-file workspace root.
46
- //
47
- // The streaming importer renames the workspace dir itself, so each test
48
- // needs a fresh tmp dir that the importer can swap into place.
49
- // ---------------------------------------------------------------------------
50
-
51
- const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
52
-
53
- function freshWorkspaceRoot(): string {
54
- const parent = realpathSync(
55
- mkdtempSync(join(tmpdir(), "migration-import-from-gcs-")),
56
- );
57
- const workspaceDir = join(parent, "workspace");
58
- mkdirSync(workspaceDir, { recursive: true });
59
- return workspaceDir;
60
- }
61
-
62
- function setWorkspaceDir(dir: string): void {
63
- process.env.CADET_WORKSPACE_DIR = dir;
64
- }
65
-
66
- process.env.DISABLE_HTTP_AUTH = "true";
67
-
68
- // ---------------------------------------------------------------------------
69
- // Imports (after mocks so module-level code picks up the stubs)
70
- // ---------------------------------------------------------------------------
71
-
72
- import { defaultV1Options } from "../runtime/migrations/__tests__/v1-test-helpers.js";
73
- import {
74
- type MigrationJob,
75
- migrationJobs,
76
- } from "../runtime/migrations/job-registry.js";
77
- import { buildVBundle } from "../runtime/migrations/vbundle-builder.js";
78
- import {
79
- _setUrlImportValidatorOptionsForTests,
80
- handleMigrationImportFromGcs,
81
- } from "../runtime/routes/migration-routes.js";
82
- import { callHandler } from "./helpers/call-route-handler.js";
83
- // ---------------------------------------------------------------------------
84
- // Local http fixture server
85
- // ---------------------------------------------------------------------------
86
-
87
- interface FixtureServer {
88
- server: Server;
89
- port: number;
90
- close: () => Promise<void>;
91
- }
92
-
93
- async function startFixtureServer(
94
- handler: (
95
- req: import("node:http").IncomingMessage,
96
- res: import("node:http").ServerResponse,
97
- ) => void,
98
- ): Promise<FixtureServer> {
99
- const server = createServer(handler);
100
- // Track every connection so `close()` can forcibly tear them down. Tests
101
- // that intentionally hang the response body (to simulate an in-flight
102
- // upstream fetch) would otherwise deadlock `server.close()`, which waits
103
- // for clients to disconnect on their own.
104
- const sockets = new Set<import("node:net").Socket>();
105
- server.on("connection", (sock) => {
106
- sockets.add(sock);
107
- sock.on("close", () => sockets.delete(sock));
108
- });
109
- await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
110
- const address = server.address();
111
- if (!address || typeof address === "string") {
112
- throw new Error("fixture server bound to unexpected address");
113
- }
114
- const port = address.port;
115
- return {
116
- server,
117
- port,
118
- close: async () => {
119
- for (const sock of sockets) {
120
- try {
121
- sock.destroy();
122
- } catch {
123
- /* best effort */
124
- }
125
- }
126
- await new Promise<void>((resolve) => server.close(() => resolve()));
127
- },
128
- };
129
- }
130
-
131
- function makeFakeSignedUrl(port: number): string {
132
- // `validateGcsSignedUrl` requires a signature query param; pin a dummy.
133
- return `http://127.0.0.1:${port}/bundle?X-Goog-Signature=fake`;
134
- }
135
-
136
- // ---------------------------------------------------------------------------
137
- // Fixture helpers
138
- // ---------------------------------------------------------------------------
139
-
140
- function makeSmallValidBundlePath(parent: string): string {
141
- const { archive } = buildVBundle({
142
- files: [
143
- {
144
- path: "workspace/data/db/assistant.db",
145
- data: new TextEncoder().encode("SQLite format 3\0"),
146
- },
147
- {
148
- path: "workspace/config.json",
149
- data: new TextEncoder().encode(
150
- JSON.stringify({ provider: "anthropic", model: "test-model" }),
151
- ),
152
- },
153
- ],
154
- ...defaultV1Options(),
155
- });
156
- const bundlePath = join(parent, "fixture-small.vbundle");
157
- writeFileSync(bundlePath, archive);
158
- return bundlePath;
159
- }
160
-
161
- function makeCorruptBundlePath(parent: string): string {
162
- // Random bytes — not a gzip stream, not a tar, nothing the importer can
163
- // parse. Drives `validation_failed` inside `streamCommitImport`.
164
- const payload = new Uint8Array(4096);
165
- for (let i = 0; i < payload.length; i += 1) {
166
- payload[i] = Math.floor(Math.random() * 256);
167
- }
168
- const bundlePath = join(parent, "fixture-corrupt.vbundle");
169
- writeFileSync(bundlePath, payload);
170
- return bundlePath;
171
- }
172
-
173
- // ---------------------------------------------------------------------------
174
- // Poll helpers — the registry is in-process so we peek at `getJob` directly
175
- // until the job leaves the pending/running states.
176
- // ---------------------------------------------------------------------------
177
-
178
- async function pollJobUntilDone(
179
- jobId: string,
180
- timeoutMs = 30_000,
181
- ): Promise<MigrationJob> {
182
- const startedAt = Date.now();
183
- while (Date.now() - startedAt < timeoutMs) {
184
- const job = migrationJobs.getJob(jobId);
185
- if (!job) {
186
- throw new Error(`Job ${jobId} disappeared from the registry`);
187
- }
188
- if (job.status === "complete" || job.status === "failed") {
189
- return job;
190
- }
191
- await new Promise((resolve) => setTimeout(resolve, 25));
192
- }
193
- throw new Error(`Job ${jobId} did not finish within ${timeoutMs}ms`);
194
- }
195
-
196
- // ---------------------------------------------------------------------------
197
- // Global test-only allowlist: widen the URL validator to accept 127.0.0.1.
198
- // ---------------------------------------------------------------------------
199
-
200
- beforeAll(() => {
201
- _setUrlImportValidatorOptionsForTests({
202
- allowedHosts: ["127.0.0.1", "storage.googleapis.com"],
203
- });
204
- });
205
-
206
- afterAll(() => {
207
- _setUrlImportValidatorOptionsForTests(undefined);
208
- if (originalWorkspaceDir !== undefined) {
209
- process.env.CADET_WORKSPACE_DIR = originalWorkspaceDir;
210
- }
211
- });
212
-
213
- // Each test gets its own workspace dir so the streaming importer's atomic
214
- // swap doesn't leak state across tests.
215
- let testWorkspaceRoot: string;
216
- let testParent: string;
217
-
218
- beforeEach(() => {
219
- testWorkspaceRoot = freshWorkspaceRoot();
220
- testParent = join(testWorkspaceRoot, "..");
221
- setWorkspaceDir(testWorkspaceRoot);
222
- });
223
-
224
- afterEach(() => {
225
- try {
226
- rmSync(testParent, { recursive: true, force: true });
227
- } catch {
228
- /* best effort */
229
- }
230
- });
231
-
232
- // ---------------------------------------------------------------------------
233
- // Response shape types
234
- // ---------------------------------------------------------------------------
235
-
236
- interface AcceptedResponse {
237
- job_id: string;
238
- status: "pending";
239
- type: "import";
240
- }
241
-
242
- interface ConflictResponse {
243
- error: { code: "import_in_progress"; message: string };
244
- }
245
-
246
- interface BadRequestResponse {
247
- error: { code: string; message: string };
248
- }
249
-
250
- interface InvalidBundleUrlResponse {
251
- error: { code: "invalid_bundle_url"; message: string };
252
- }
253
-
254
- // ---------------------------------------------------------------------------
255
- // Tests
256
- // ---------------------------------------------------------------------------
257
-
258
- describe("POST /v1/migrations/import-from-gcs", () => {
259
- test("happy path: 202 → job completes with flat result.summary", async () => {
260
- const bundlePath = makeSmallValidBundlePath(testParent);
261
-
262
- const fixture = await startFixtureServer((_req, res) => {
263
- res.writeHead(200, { "Content-Type": "application/octet-stream" });
264
- createReadStream(bundlePath).pipe(res);
265
- });
266
-
267
- try {
268
- const req = new Request(
269
- "http://localhost/v1/migrations/import-from-gcs",
270
- {
271
- method: "POST",
272
- headers: { "Content-Type": "application/json" },
273
- body: JSON.stringify({ bundle_url: makeFakeSignedUrl(fixture.port) }),
274
- },
275
- );
276
-
277
- const res = await callHandler(
278
- handleMigrationImportFromGcs,
279
- req,
280
- undefined,
281
- 202,
282
- );
283
- expect(res.status).toBe(202);
284
- const body = (await res.json()) as AcceptedResponse;
285
- expect(body.type).toBe("import");
286
- expect(body.status).toBe("pending");
287
- expect(typeof body.job_id).toBe("string");
288
- expect(body.job_id.length).toBeGreaterThan(0);
289
-
290
- const finalJob = await pollJobUntilDone(body.job_id);
291
- expect(finalJob.status).toBe("complete");
292
-
293
- // The job result must match the wire shape the CLI expects when it
294
- // casts `terminal.result` to `ImportResponse`: `success` and `summary`
295
- // live at the top level, NOT under a nested `report` field. Asserting
296
- // here is what locks in the fix for the async-vs-sync shape drift
297
- // that was misreporting successful imports as failed.
298
- const result = finalJob.result as {
299
- success: boolean;
300
- summary: {
301
- total_files: number;
302
- files_created: number;
303
- };
304
- };
305
- expect(result).toBeDefined();
306
- expect(result.success).toBe(true);
307
- expect(result.summary).toBeDefined();
308
- expect(result.summary.total_files).toBeGreaterThan(0);
309
- expect(result.summary.files_created).toBeGreaterThan(0);
310
-
311
- // Workspace was swapped into place.
312
- expect(
313
- existsSync(join(testWorkspaceRoot, "data", "db", "assistant.db")),
314
- ).toBe(true);
315
- expect(existsSync(join(testWorkspaceRoot, "config.json"))).toBe(true);
316
- } finally {
317
- await fixture.close();
318
- }
319
- }, 30_000);
320
-
321
- test("upstream 500: job ends failed with fetch_failed + upstreamStatus 500", async () => {
322
- const fixture = await startFixtureServer((_req, res) => {
323
- res.writeHead(500);
324
- res.end("oh no");
325
- });
326
-
327
- try {
328
- const req = new Request(
329
- "http://localhost/v1/migrations/import-from-gcs",
330
- {
331
- method: "POST",
332
- headers: { "Content-Type": "application/json" },
333
- body: JSON.stringify({ bundle_url: makeFakeSignedUrl(fixture.port) }),
334
- },
335
- );
336
-
337
- const res = await callHandler(
338
- handleMigrationImportFromGcs,
339
- req,
340
- undefined,
341
- 202,
342
- );
343
- expect(res.status).toBe(202);
344
- const body = (await res.json()) as AcceptedResponse;
345
-
346
- const finalJob = await pollJobUntilDone(body.job_id);
347
- expect(finalJob.status).toBe("failed");
348
- expect(finalJob.error?.code).toBe("fetch_failed");
349
- expect(finalJob.error?.upstreamStatus).toBe(500);
350
- } finally {
351
- await fixture.close();
352
- }
353
- }, 30_000);
354
-
355
- test("corrupt bundle: job ends failed with validation_failed", async () => {
356
- const bundlePath = makeCorruptBundlePath(testParent);
357
-
358
- const fixture = await startFixtureServer((_req, res) => {
359
- res.writeHead(200, { "Content-Type": "application/octet-stream" });
360
- createReadStream(bundlePath).pipe(res);
361
- });
362
-
363
- try {
364
- const req = new Request(
365
- "http://localhost/v1/migrations/import-from-gcs",
366
- {
367
- method: "POST",
368
- headers: { "Content-Type": "application/json" },
369
- body: JSON.stringify({ bundle_url: makeFakeSignedUrl(fixture.port) }),
370
- },
371
- );
372
-
373
- const res = await callHandler(
374
- handleMigrationImportFromGcs,
375
- req,
376
- undefined,
377
- 202,
378
- );
379
- expect(res.status).toBe(202);
380
- const body = (await res.json()) as AcceptedResponse;
381
-
382
- const finalJob = await pollJobUntilDone(body.job_id);
383
- expect(finalJob.status).toBe("failed");
384
- // The streaming importer maps a mangled archive to
385
- // `extraction_failed` (gunzip throws on the random payload before the
386
- // tar layer even sees it). Parity with the URL-body path: that path
387
- // returns 500 `{ reason: "extraction_failed" }` for the same input,
388
- // and `runGcsImport` re-throws it as a `GcsImportError` carrying the
389
- // same code.
390
- expect(finalJob.error?.code).toBe("extraction_failed");
391
- } finally {
392
- await fixture.close();
393
- }
394
- }, 30_000);
395
-
396
- test("second import while first is in flight returns 409", async () => {
397
- // A fixture that never responds keeps the first job stuck in `pending`
398
- // (the runner is scheduled on a microtask, but the handler's `fetch`
399
- // hangs on the open socket). Even `pending` counts as in-flight for
400
- // the concurrency check, so the 409 is deterministic regardless of
401
- // event-loop timing.
402
- const fixture = await startFixtureServer((_req, _res) => {
403
- // Intentionally no response — hold the socket open.
404
- });
405
-
406
- let firstJobId: string | undefined;
407
- try {
408
- const firstReq = new Request(
409
- "http://localhost/v1/migrations/import-from-gcs",
410
- {
411
- method: "POST",
412
- headers: { "Content-Type": "application/json" },
413
- body: JSON.stringify({ bundle_url: makeFakeSignedUrl(fixture.port) }),
414
- },
415
- );
416
- const firstRes = await callHandler(
417
- handleMigrationImportFromGcs,
418
- firstReq,
419
- undefined,
420
- 202,
421
- );
422
- expect(firstRes.status).toBe(202);
423
- const firstBody = (await firstRes.json()) as AcceptedResponse;
424
- firstJobId = firstBody.job_id;
425
-
426
- const secondReq = new Request(
427
- "http://localhost/v1/migrations/import-from-gcs",
428
- {
429
- method: "POST",
430
- headers: { "Content-Type": "application/json" },
431
- body: JSON.stringify({ bundle_url: makeFakeSignedUrl(fixture.port) }),
432
- },
433
- );
434
- const secondRes = await callHandler(
435
- handleMigrationImportFromGcs,
436
- secondReq,
437
- undefined,
438
- 202,
439
- );
440
- expect(secondRes.status).toBe(409);
441
- const secondBody = (await secondRes.json()) as ConflictResponse;
442
- expect(secondBody.error.code).toBe("import_in_progress");
443
- expect(secondBody.error.message).toContain(firstBody.job_id);
444
- } finally {
445
- // Close the server — this destroys every tracked socket, which
446
- // unblocks the first job's `fetch` and lets the runner settle into
447
- // `failed`. Wait for that before returning so subsequent tests can
448
- // start their own `"import"` job without tripping the registry's
449
- // single-in-flight invariant.
450
- await fixture.close();
451
- if (firstJobId !== undefined) {
452
- await pollJobUntilDone(firstJobId);
453
- }
454
- }
455
- }, 30_000);
456
-
457
- test("invalid bundle_url (disallowed host) returns 400 without consuming the in-flight slot", async () => {
458
- // Disallowed host — syntactically a valid URL (passes zod), but
459
- // `validateGcsSignedUrl` must reject it synchronously so the single
460
- // in-flight import slot is NOT taken. Without the preflight, this
461
- // would return 202 + job_id and then only fail inside the async
462
- // runner, blocking a correct retry until the doomed job cleared.
463
- const badReq = new Request(
464
- "http://localhost/v1/migrations/import-from-gcs",
465
- {
466
- method: "POST",
467
- headers: { "Content-Type": "application/json" },
468
- body: JSON.stringify({
469
- bundle_url: "http://evil.example.com/bundle?X-Goog-Signature=fake",
470
- }),
471
- },
472
- );
473
- const badRes = await callHandler(
474
- handleMigrationImportFromGcs,
475
- badReq,
476
- undefined,
477
- 202,
478
- );
479
- expect(badRes.status).toBe(400);
480
- const badBody = (await badRes.json()) as InvalidBundleUrlResponse;
481
- expect(badBody.error.code).toBe("invalid_bundle_url");
482
- expect(typeof badBody.error.message).toBe("string");
483
- expect(badBody.error.message.length).toBeGreaterThan(0);
484
-
485
- // A follow-up valid request must still be able to start a job — proving
486
- // the doomed request did not occupy the concurrency slot.
487
- const bundlePath = makeSmallValidBundlePath(testParent);
488
- const fixture = await startFixtureServer((_req, res) => {
489
- res.writeHead(200, { "Content-Type": "application/octet-stream" });
490
- createReadStream(bundlePath).pipe(res);
491
- });
492
- try {
493
- const goodReq = new Request(
494
- "http://localhost/v1/migrations/import-from-gcs",
495
- {
496
- method: "POST",
497
- headers: { "Content-Type": "application/json" },
498
- body: JSON.stringify({ bundle_url: makeFakeSignedUrl(fixture.port) }),
499
- },
500
- );
501
- const goodRes = await callHandler(
502
- handleMigrationImportFromGcs,
503
- goodReq,
504
- undefined,
505
- 202,
506
- );
507
- expect(goodRes.status).toBe(202);
508
- const goodBody = (await goodRes.json()) as AcceptedResponse;
509
- expect(goodBody.type).toBe("import");
510
-
511
- // Drain the job so subsequent tests start from a clean registry.
512
- await pollJobUntilDone(goodBody.job_id);
513
- } finally {
514
- await fixture.close();
515
- }
516
- }, 30_000);
517
-
518
- test("malformed body (missing bundle_url) returns 400", async () => {
519
- const req = new Request("http://localhost/v1/migrations/import-from-gcs", {
520
- method: "POST",
521
- headers: { "Content-Type": "application/json" },
522
- body: JSON.stringify({}),
523
- });
524
- const res = await callHandler(
525
- handleMigrationImportFromGcs,
526
- req,
527
- undefined,
528
- 202,
529
- );
530
- expect(res.status).toBe(400);
531
- const body = (await res.json()) as BadRequestResponse;
532
- expect(body.error.code).toBe("BAD_REQUEST");
533
- });
534
- });