@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,588 +0,0 @@
1
- /**
2
- * Integration tests for the JSON `{url}` body on POST /v1/migrations/import.
3
- *
4
- * Covered:
5
- * - Happy path: a local http server serves a valid .vbundle, the handler
6
- * fetches it, streams it through `streamCommitImport`, and returns the
7
- * same success-report shape the raw-bytes path returns.
8
- * - GCS 500: upstream server returns 500 → handler returns 502 with
9
- * `{ reason: "fetch_failed", upstream_status: 500 }`.
10
- * - Malformed body: `{}` and `{ "url": "" }` → 400.
11
- * - Invalid GCS URL: a real https://evil.com URL is rejected with the
12
- * redacted `Invalid URL: host` message; the raw URL is not echoed back
13
- * in the response body.
14
- *
15
- * Memory-ceiling coverage lives at the streaming-importer layer — see
16
- * `vbundle-streaming-importer.test.ts` ("streamCommitImport — memory
17
- * ceiling"). The URL handler adds only fixed HTTP/framing overhead on top
18
- * of that pipeline, not bundle-size-proportional allocation.
19
- *
20
- * The raw-bytes ingress path is exercised by a separate test file,
21
- * `migration-import-commit-http.test.ts`.
22
- */
23
-
24
- import {
25
- createReadStream,
26
- existsSync,
27
- mkdirSync,
28
- mkdtempSync,
29
- realpathSync,
30
- rmSync,
31
- writeFileSync,
32
- } from "node:fs";
33
- import { createServer, type Server } from "node:http";
34
- import { tmpdir } from "node:os";
35
- import { join } from "node:path";
36
- import { Database } from "bun:sqlite";
37
- import {
38
- afterAll,
39
- afterEach,
40
- beforeAll,
41
- beforeEach,
42
- describe,
43
- expect,
44
- test,
45
- } from "bun:test";
46
-
47
- // ---------------------------------------------------------------------------
48
- // Test isolation: per-file workspace root.
49
- //
50
- // The shared test preload points CADET_WORKSPACE_DIR at a tmp dir. The
51
- // streaming importer does an atomic rename of the workspace dir itself,
52
- // which implicitly invalidates the shared tmp dir for subsequent tests in
53
- // the same file. Each test below creates its own isolated workspace and
54
- // re-points getWorkspaceDir() at it via the env var before invoking the
55
- // handler.
56
- // ---------------------------------------------------------------------------
57
-
58
- const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
59
-
60
- function freshWorkspaceRoot(): string {
61
- const parent = realpathSync(
62
- mkdtempSync(join(tmpdir(), "migration-import-from-url-")),
63
- );
64
- // The streaming importer renames workspaceDir itself, so put the
65
- // workspace inside a parent dir we own.
66
- const workspaceDir = join(parent, "workspace");
67
- mkdirSync(workspaceDir, { recursive: true });
68
- return workspaceDir;
69
- }
70
-
71
- function setWorkspaceDir(dir: string): void {
72
- process.env.CADET_WORKSPACE_DIR = dir;
73
- }
74
-
75
- process.env.DISABLE_HTTP_AUTH = "true";
76
-
77
- // ---------------------------------------------------------------------------
78
- // Imports (after mocks so module-level code picks up the stubs)
79
- // ---------------------------------------------------------------------------
80
-
81
- import { defaultV1Options } from "../runtime/migrations/__tests__/v1-test-helpers.js";
82
- import { buildVBundle } from "../runtime/migrations/vbundle-builder.js";
83
- import {
84
- _setUrlImportValidatorOptionsForTests,
85
- handleMigrationImport,
86
- } from "../runtime/routes/migration-routes.js";
87
- import { resetDbForTesting } from "./db-test-helpers.js";
88
- import { callHandler } from "./helpers/call-route-handler.js";
89
- // ---------------------------------------------------------------------------
90
- // Local http fixture server
91
- // ---------------------------------------------------------------------------
92
-
93
- interface FixtureServer {
94
- server: Server;
95
- port: number;
96
- close: () => Promise<void>;
97
- }
98
-
99
- async function startFixtureServer(
100
- handler: (
101
- req: import("node:http").IncomingMessage,
102
- res: import("node:http").ServerResponse,
103
- ) => void,
104
- ): Promise<FixtureServer> {
105
- const server = createServer(handler);
106
- await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
107
- const address = server.address();
108
- if (!address || typeof address === "string") {
109
- throw new Error("fixture server bound to unexpected address");
110
- }
111
- const port = address.port;
112
- return {
113
- server,
114
- port,
115
- close: async () => {
116
- await new Promise<void>((resolve) => server.close(() => resolve()));
117
- },
118
- };
119
- }
120
-
121
- function makeFakeSignedUrl(port: number): string {
122
- // `validateGcsSignedUrl` requires a signature query param; pin a dummy.
123
- return `http://127.0.0.1:${port}/bundle?X-Goog-Signature=fake`;
124
- }
125
-
126
- // ---------------------------------------------------------------------------
127
- // Fixture helpers
128
- // ---------------------------------------------------------------------------
129
-
130
- function makeSmallValidBundlePath(parent: string): string {
131
- const { archive } = buildVBundle({
132
- files: [
133
- {
134
- path: "workspace/data/db/assistant.db",
135
- data: new TextEncoder().encode("SQLite format 3\0"),
136
- },
137
- {
138
- path: "workspace/config.json",
139
- data: new TextEncoder().encode(
140
- JSON.stringify({ provider: "anthropic", model: "test-model" }),
141
- ),
142
- },
143
- ],
144
- ...defaultV1Options(),
145
- });
146
- const bundlePath = join(parent, "fixture-small.vbundle");
147
- writeFileSync(bundlePath, archive);
148
- return bundlePath;
149
- }
150
-
151
- // ---------------------------------------------------------------------------
152
- // Global test-only allowlist: widen the URL validator to accept 127.0.0.1.
153
- // ---------------------------------------------------------------------------
154
-
155
- beforeAll(() => {
156
- _setUrlImportValidatorOptionsForTests({
157
- allowedHosts: ["127.0.0.1", "storage.googleapis.com"],
158
- });
159
- });
160
-
161
- afterAll(() => {
162
- _setUrlImportValidatorOptionsForTests(undefined);
163
- if (originalWorkspaceDir !== undefined) {
164
- process.env.CADET_WORKSPACE_DIR = originalWorkspaceDir;
165
- }
166
- });
167
-
168
- // Each test gets its own workspace dir so the streaming importer's atomic
169
- // swap doesn't leak state across tests.
170
- let testWorkspaceRoot: string;
171
- let testParent: string;
172
-
173
- beforeEach(() => {
174
- testWorkspaceRoot = freshWorkspaceRoot();
175
- testParent = join(testWorkspaceRoot, "..");
176
- setWorkspaceDir(testWorkspaceRoot);
177
- });
178
-
179
- afterEach(() => {
180
- try {
181
- rmSync(testParent, { recursive: true, force: true });
182
- } catch {
183
- /* best effort */
184
- }
185
- });
186
-
187
- // ---------------------------------------------------------------------------
188
- // Response shape types
189
- // ---------------------------------------------------------------------------
190
-
191
- interface ImportCommitResponse {
192
- success: boolean;
193
- summary: {
194
- total_files: number;
195
- files_created: number;
196
- files_overwritten: number;
197
- files_skipped: number;
198
- backups_created: number;
199
- };
200
- files: Array<{
201
- path: string;
202
- disk_path: string;
203
- action: string;
204
- size: number;
205
- sha256: string;
206
- backup_path: string | null;
207
- }>;
208
- manifest: Record<string, unknown>;
209
- warnings: string[];
210
- }
211
-
212
- interface BadRequestResponse {
213
- error: { code: string; message: string };
214
- }
215
-
216
- // ---------------------------------------------------------------------------
217
- // Tests: JSON URL body
218
- // ---------------------------------------------------------------------------
219
-
220
- describe("handleMigrationImport — JSON {url} body", () => {
221
- test("happy path: fetches fixture bundle from local http server and imports", async () => {
222
- const bundlePath = makeSmallValidBundlePath(testParent);
223
-
224
- const fixture = await startFixtureServer((req, res) => {
225
- // Prove the server itself streams the response body.
226
- res.writeHead(200, { "Content-Type": "application/octet-stream" });
227
- createReadStream(bundlePath).pipe(res);
228
- });
229
-
230
- try {
231
- const req = new Request("http://localhost/v1/migrations/import", {
232
- method: "POST",
233
- headers: { "Content-Type": "application/json" },
234
- body: JSON.stringify({ url: makeFakeSignedUrl(fixture.port) }),
235
- });
236
-
237
- const res = await callHandler(handleMigrationImport, req);
238
- const body = (await res.json()) as ImportCommitResponse;
239
-
240
- expect(res.status).toBe(200);
241
- expect(body.success).toBe(true);
242
- expect(body.summary).toBeDefined();
243
- expect(body.summary.total_files).toBeGreaterThan(0);
244
- expect(body.files.length).toBeGreaterThan(0);
245
- expect(body.manifest).toBeDefined();
246
-
247
- // Workspace was swapped into place and contains the fixture files.
248
- expect(
249
- existsSync(join(testWorkspaceRoot, "data", "db", "assistant.db")),
250
- ).toBe(true);
251
- expect(existsSync(join(testWorkspaceRoot, "config.json"))).toBe(true);
252
- } finally {
253
- await fixture.close();
254
- }
255
- });
256
-
257
- test("upstream 500 returns 502 with reason: fetch_failed", async () => {
258
- const fixture = await startFixtureServer((_req, res) => {
259
- res.writeHead(500);
260
- res.end("oh no");
261
- });
262
-
263
- try {
264
- const req = new Request("http://localhost/v1/migrations/import", {
265
- method: "POST",
266
- headers: { "Content-Type": "application/json" },
267
- body: JSON.stringify({ url: makeFakeSignedUrl(fixture.port) }),
268
- });
269
-
270
- const res = await callHandler(handleMigrationImport, req);
271
- const body = (await res.json()) as {
272
- error: { code: string; message: string };
273
- };
274
-
275
- expect(res.status).toBe(502);
276
- expect(body.error.code).toBe("BAD_GATEWAY");
277
- expect(body.error.message).toContain("500");
278
- } finally {
279
- await fixture.close();
280
- }
281
- });
282
-
283
- test('malformed body: {"url": ""} returns 400', async () => {
284
- const req = new Request("http://localhost/v1/migrations/import", {
285
- method: "POST",
286
- headers: { "Content-Type": "application/json" },
287
- body: JSON.stringify({ url: "" }),
288
- });
289
-
290
- const res = await callHandler(handleMigrationImport, req);
291
- const body = (await res.json()) as BadRequestResponse;
292
-
293
- expect(res.status).toBe(400);
294
- expect(body.error.code).toBe("BAD_REQUEST");
295
- });
296
-
297
- test("missing url key: {} returns 400", async () => {
298
- const req = new Request("http://localhost/v1/migrations/import", {
299
- method: "POST",
300
- headers: { "Content-Type": "application/json" },
301
- body: JSON.stringify({}),
302
- });
303
-
304
- const res = await callHandler(handleMigrationImport, req);
305
- const body = (await res.json()) as BadRequestResponse;
306
-
307
- expect(res.status).toBe(400);
308
- expect(body.error.code).toBe("BAD_REQUEST");
309
- });
310
-
311
- test("unparseable JSON body returns 400", async () => {
312
- const req = new Request("http://localhost/v1/migrations/import", {
313
- method: "POST",
314
- headers: { "Content-Type": "application/json" },
315
- body: "{not-json",
316
- });
317
-
318
- const res = await callHandler(handleMigrationImport, req);
319
- const body = (await res.json()) as BadRequestResponse;
320
-
321
- expect(res.status).toBe(400);
322
- expect(body.error.code).toBe("BAD_REQUEST");
323
- });
324
-
325
- test("invalid GCS URL returns 400 and does not leak the URL", async () => {
326
- const rawUrl = "https://evil.example.com/bucket/obj?X-Goog-Signature=fake";
327
- const req = new Request("http://localhost/v1/migrations/import", {
328
- method: "POST",
329
- headers: { "Content-Type": "application/json" },
330
- body: JSON.stringify({ url: rawUrl }),
331
- });
332
-
333
- const res = await callHandler(handleMigrationImport, req);
334
- const rawBody = await res.text();
335
-
336
- expect(res.status).toBe(400);
337
-
338
- const parsed = JSON.parse(rawBody) as BadRequestResponse;
339
- expect(parsed.error.code).toBe("BAD_REQUEST");
340
- // The message should carry the redacted reason, not the URL itself.
341
- expect(parsed.error.message.toLowerCase()).toContain("invalid url");
342
- // Defense-in-depth: the raw URL must not appear anywhere in the response.
343
- expect(rawBody).not.toContain("evil.example.com");
344
- expect(rawBody).not.toContain("X-Goog-Signature=fake");
345
- });
346
-
347
- test("non-https scheme on default allowlist is rejected", async () => {
348
- // Temporarily reset to the strict default allowlist so this test
349
- // exercises the production validator configuration.
350
- _setUrlImportValidatorOptionsForTests(undefined);
351
- try {
352
- const req = new Request("http://localhost/v1/migrations/import", {
353
- method: "POST",
354
- headers: { "Content-Type": "application/json" },
355
- body: JSON.stringify({
356
- url: "http://storage.googleapis.com/b/o?X-Goog-Signature=x",
357
- }),
358
- });
359
-
360
- const res = await callHandler(handleMigrationImport, req);
361
- const body = (await res.json()) as BadRequestResponse;
362
-
363
- expect(res.status).toBe(400);
364
- expect(body.error.code).toBe("BAD_REQUEST");
365
- expect(body.error.message).toContain("scheme");
366
- } finally {
367
- _setUrlImportValidatorOptionsForTests({
368
- allowedHosts: ["127.0.0.1", "storage.googleapis.com"],
369
- });
370
- }
371
- });
372
- });
373
-
374
- // ---------------------------------------------------------------------------
375
- // Gap A regression: no-swap success path must not append a stale
376
- // "newer migration" warning sourced from the live DB. The warning would
377
- // wrongly attribute live-DB state to the imported bundle when no bundle
378
- // files actually land on disk (e.g. credentials-only bundle).
379
- // ---------------------------------------------------------------------------
380
-
381
- describe("handleMigrationImport — no-swap path omits newer-migration warning", () => {
382
- test("credentials-only bundle does not inherit live-DB migration warnings", async () => {
383
- // Seed the live workspace DB with a step:* checkpoint that's NOT
384
- // in the known step list. validateMigrationState treats this as a
385
- // "newer version" and would otherwise push a warning into the report. With
386
- // the gate in appendNewerMigrationWarningsIfAny the warning must be
387
- // suppressed when the import didn't modify the workspace.
388
- const dbDir = join(testWorkspaceRoot, "data", "db");
389
- mkdirSync(dbDir, { recursive: true });
390
- const dbPath = join(dbDir, "assistant.db");
391
- const seed = new Database(dbPath);
392
- try {
393
- seed.exec(`
394
- CREATE TABLE memory_checkpoints (
395
- key TEXT PRIMARY KEY,
396
- value TEXT NOT NULL,
397
- updated_at INTEGER NOT NULL
398
- );
399
- `);
400
- seed
401
- .query(
402
- `INSERT INTO memory_checkpoints (key, value, updated_at) VALUES (?, ?, ?)`,
403
- )
404
- .run("step:migrationFromTheFuture", "1", Date.now());
405
- } finally {
406
- seed.close();
407
- }
408
-
409
- // Drop any cached Drizzle singleton so getDb() re-opens from the
410
- // seeded path above when the handler calls it post-import.
411
- resetDbForTesting();
412
-
413
- // All-`cadet:*` credentials bundle: the streaming importer returns
414
- // ok=true with zero files_created/overwritten (no-swap success),
415
- // and the credential-import callback filters every entry as a
416
- // cloud credential so CES is never invoked.
417
- //
418
- // The synthetic `data/db/assistant.db` entry satisfies the v1
419
- // manifest schema's contents refine; it's a no-op on disk because
420
- // legacy bundles never carry workspace data.
421
- const { archive } = buildVBundle({
422
- files: [
423
- { path: "data/db/assistant.db", data: new Uint8Array() },
424
- {
425
- path: "credentials/cadet:device-id",
426
- data: new TextEncoder().encode("test-device-id"),
427
- },
428
- ],
429
- ...defaultV1Options(),
430
- });
431
- const bundlePath = join(testParent, "fixture-creds-only.vbundle");
432
- writeFileSync(bundlePath, archive);
433
-
434
- const fixture = await startFixtureServer((_req, res) => {
435
- res.writeHead(200, { "Content-Type": "application/octet-stream" });
436
- createReadStream(bundlePath).pipe(res);
437
- });
438
-
439
- try {
440
- const req = new Request("http://localhost/v1/migrations/import", {
441
- method: "POST",
442
- headers: { "Content-Type": "application/json" },
443
- body: JSON.stringify({ url: makeFakeSignedUrl(fixture.port) }),
444
- });
445
-
446
- const res = await callHandler(handleMigrationImport, req);
447
- const body = (await res.json()) as ImportCommitResponse;
448
-
449
- expect(res.status).toBe(200);
450
- expect(body.success).toBe(true);
451
- // Only the synthetic data/db/assistant.db lands; the credential
452
- // entry is filtered out and no `workspace/*` entries exist, so
453
- // the workspace itself is otherwise untouched.
454
- expect(body.summary.files_overwritten).toBe(1);
455
-
456
- // The gate must suppress the newer-migration warning text. The
457
- // helper's wording starts with "Imported data contains" and ends
458
- // with "migration(s) from a newer version" — matching either
459
- // substring is sufficient.
460
- const stale = body.warnings.filter((w) =>
461
- w.includes("from a newer version"),
462
- );
463
- expect(stale).toEqual([]);
464
- } finally {
465
- await fixture.close();
466
- // Close the cached DB handle before the workspace dir gets rm'd
467
- // in afterEach so we don't leak WAL/SHM files across tests.
468
- resetDbForTesting();
469
- }
470
- });
471
- });
472
-
473
- // ---------------------------------------------------------------------------
474
- // Gap B regression: an upstream body that drops mid-stream (peer reset,
475
- // socket.destroy() after headers are sent) must surface as 502
476
- // fetch_failed, not 500 extraction_failed. The tag-at-source plumbing in
477
- // the URL handler routes importer-rethrown upstream errors to the fetch-
478
- // failed branch.
479
- // ---------------------------------------------------------------------------
480
-
481
- describe("handleMigrationImport — upstream body dropped mid-stream", () => {
482
- test("socket destroy mid-body returns 502 with reason: fetch_failed", async () => {
483
- // Build a real gzipped tar prefix, then serve only the first N bytes
484
- // and tear down the connection. gunzip inside streamCommitImport
485
- // will surface this as a stream error; we tag it at the source so
486
- // the handler maps it to 502 fetch_failed. Use random payload bytes
487
- // so the gzip output is ~incompressible — a compressed all-zeros
488
- // payload shrinks to <500 bytes total, which doesn't give us enough
489
- // material to reliably deliver a partial body with gunzip state
490
- // still alive across the socket teardown.
491
- const incompressible = new Uint8Array(64 * 1024);
492
- for (let i = 0; i < incompressible.length; i += 1) {
493
- incompressible[i] = Math.floor(Math.random() * 256);
494
- }
495
- const { archive } = buildVBundle({
496
- files: [
497
- {
498
- path: "workspace/data/db/assistant.db",
499
- data: incompressible,
500
- },
501
- ],
502
- ...defaultV1Options(),
503
- });
504
- // Safety net: if someone changes buildVBundle to return very small
505
- // outputs, drop the test early rather than flaking on a too-short
506
- // truncation window. 512 bytes is plenty given 64 KB of random data
507
- // gzips to essentially its original size.
508
- expect(archive.byteLength).toBeGreaterThan(512);
509
-
510
- // Truncate to the first 256 bytes so upstream cannot deliver a
511
- // usable tar stream. gunzip will error on the abrupt close.
512
- const truncatedPrefix = archive.slice(0, 256);
513
-
514
- const fixture = await startFixtureServer((_req, res) => {
515
- // Chunked transfer (no Content-Length) + socket.destroy() mid-body
516
- // is the cleanest way to force Bun's fetch to surface a
517
- // post-headers stream error rather than an initial-fetch throw.
518
- // We write the prefix in a couple of chunks so the client side can
519
- // return from fetch() and hand the body stream to the importer
520
- // before we tear the socket down.
521
- res.writeHead(200, {
522
- "Content-Type": "application/octet-stream",
523
- "Transfer-Encoding": "chunked",
524
- });
525
- const half = Math.floor(truncatedPrefix.byteLength / 2);
526
- res.write(truncatedPrefix.slice(0, half), () => {
527
- // Give the runtime a chance to deliver the first chunk to the
528
- // consumer and enter the streaming import pipeline, then rip
529
- // the socket out so the body stream surfaces an abort error.
530
- setTimeout(() => {
531
- res.socket?.destroy();
532
- }, 100);
533
- });
534
- });
535
-
536
- try {
537
- const req = new Request("http://localhost/v1/migrations/import", {
538
- method: "POST",
539
- headers: { "Content-Type": "application/json" },
540
- body: JSON.stringify({ url: makeFakeSignedUrl(fixture.port) }),
541
- });
542
-
543
- const res = await callHandler(handleMigrationImport, req);
544
- const body = (await res.json()) as {
545
- error: { code: string; message: string };
546
- };
547
-
548
- expect(res.status).toBe(502);
549
- expect(body.error.code).toBe("BAD_GATEWAY");
550
- } finally {
551
- await fixture.close();
552
- }
553
- }, 15_000);
554
- });
555
-
556
- // ---------------------------------------------------------------------------
557
- // Regression: raw-bytes path still works through the same handler.
558
- // ---------------------------------------------------------------------------
559
-
560
- describe("handleMigrationImport — raw-bytes regression", () => {
561
- test("application/octet-stream body still imports successfully", async () => {
562
- const { archive } = buildVBundle({
563
- files: [
564
- {
565
- path: "workspace/data/db/assistant.db",
566
- data: new TextEncoder().encode("SQLite format 3\0"),
567
- },
568
- ],
569
- ...defaultV1Options(),
570
- });
571
-
572
- const req = new Request("http://localhost/v1/migrations/import", {
573
- method: "POST",
574
- headers: { "Content-Type": "application/octet-stream" },
575
- body: archive.buffer.slice(
576
- archive.byteOffset,
577
- archive.byteOffset + archive.byteLength,
578
- ) as ArrayBuffer,
579
- });
580
-
581
- const res = await callHandler(handleMigrationImport, req);
582
- const body = (await res.json()) as ImportCommitResponse;
583
-
584
- expect(res.status).toBe(200);
585
- expect(body.success).toBe(true);
586
- expect(body.files.length).toBeGreaterThan(0);
587
- });
588
- });