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