@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,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
- });