@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,557 +0,0 @@
1
- /**
2
- * HTTP-layer integration tests for POST /v1/migrations/export.
3
- *
4
- * Tests cover:
5
- * - Success: valid .vbundle archive returned with correct headers
6
- * - Archive structure: returned archive passes vbundle validation
7
- * - Manifest correctness: schema_version, file entries, checksums
8
- * - Custom description: optional JSON body sets manifest description
9
- * - Real data: exported archive contains actual file contents from disk
10
- * - Graceful fallback: missing db/config produces valid archive with defaults
11
- * - Auth: route policy enforcement (settings.write scope required)
12
- * - Integration: existing routes are unaffected by the new endpoint
13
- */
14
- import { createHash } from "node:crypto";
15
- import { mkdirSync, writeFileSync } from "node:fs";
16
- import { join } from "node:path";
17
- import { gunzipSync } from "node:zlib";
18
- import { beforeAll, describe, expect, test } from "bun:test";
19
-
20
- const testDir = process.env.CADET_WORKSPACE_DIR!;
21
- const testDbDir = join(testDir, "data", "db");
22
- const testDbPath = join(testDbDir, "assistant.db");
23
- const testConfigPath = join(testDir, "config.json");
24
-
25
- process.env.DISABLE_HTTP_AUTH = "true";
26
-
27
- import { defaultV1Options } from "../runtime/migrations/__tests__/v1-test-helpers.js";
28
- import { validateVBundle } from "../runtime/migrations/vbundle-validator.js";
29
- import {
30
- handleMigrationExport,
31
- handleMigrationValidate,
32
- } from "../runtime/routes/migration-routes.js";
33
- import { callHandler } from "./helpers/call-route-handler.js";
34
- // Test fixture data: a minimal SQLite header to simulate a real database file
35
- const SQLITE_HEADER = new Uint8Array([
36
- 0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74,
37
- 0x20, 0x33, 0x00,
38
- ]);
39
- const TEST_CONFIG = { provider: "anthropic", model: "test-model" };
40
-
41
- beforeAll(() => {
42
- // Write test fixture files so the export reads real data
43
- mkdirSync(testDbDir, { recursive: true });
44
- writeFileSync(testDbPath, SQLITE_HEADER);
45
- writeFileSync(testConfigPath, JSON.stringify(TEST_CONFIG, null, 2) + "\n");
46
- });
47
-
48
- // ---------------------------------------------------------------------------
49
- // Tar parsing helper (mirrors vbundle-validator's internal parser)
50
- // ---------------------------------------------------------------------------
51
-
52
- interface TarEntry {
53
- name: string;
54
- data: Uint8Array;
55
- }
56
-
57
- function parseTarEntries(gzippedData: Uint8Array): TarEntry[] {
58
- const tarData = gunzipSync(gzippedData);
59
- const entries: TarEntry[] = [];
60
- let offset = 0;
61
- const BLOCK_SIZE = 512;
62
-
63
- while (offset + BLOCK_SIZE <= tarData.length) {
64
- const header = tarData.subarray(offset, offset + BLOCK_SIZE);
65
- if (header.every((b) => b === 0)) {
66
- break;
67
- }
68
-
69
- let end = 0;
70
- while (end < 100 && header[end] !== 0) {
71
- end++;
72
- }
73
- const name = new TextDecoder().decode(header.subarray(0, end));
74
-
75
- let sizeEnd = 124;
76
- while (sizeEnd < 136 && header[sizeEnd] !== 0) {
77
- sizeEnd++;
78
- }
79
- const sizeStr = new TextDecoder().decode(header.subarray(124, sizeEnd));
80
- const size = parseInt(sizeStr, 8) || 0;
81
-
82
- const dataStart = offset + BLOCK_SIZE;
83
- const data = tarData.subarray(dataStart, dataStart + size);
84
- const dataBlocks = Math.ceil(size / BLOCK_SIZE);
85
-
86
- if (header[156] === "0".charCodeAt(0) || header[156] === 0) {
87
- entries.push({ name, data: new Uint8Array(data) });
88
- }
89
-
90
- offset = dataStart + dataBlocks * BLOCK_SIZE;
91
- }
92
-
93
- return entries;
94
- }
95
-
96
- function sha256Hex(data: Uint8Array): string {
97
- return createHash("sha256").update(data).digest("hex");
98
- }
99
-
100
- // ---------------------------------------------------------------------------
101
- // Success tests
102
- // ---------------------------------------------------------------------------
103
-
104
- describe("handleMigrationExport", () => {
105
- test("POST returns 200 with binary vbundle archive", async () => {
106
- const req = new Request("http://localhost/v1/migrations/export", {
107
- method: "POST",
108
- });
109
-
110
- const res = await callHandler(handleMigrationExport, req);
111
-
112
- expect(res.status).toBe(200);
113
- expect(res.headers.get("Content-Type")).toBe("application/octet-stream");
114
- expect(res.headers.get("Content-Disposition")).toMatch(
115
- /^attachment; filename="export-.*\.vbundle"$/,
116
- );
117
- expect(res.headers.get("Content-Length")).toBeDefined();
118
- expect(Number(res.headers.get("Content-Length"))).toBeGreaterThan(0);
119
- expect(res.headers.get("X-Vbundle-Schema-Version")).toBe("1");
120
- expect(res.headers.get("X-Vbundle-Manifest-Sha256")).toBeDefined();
121
- expect(res.headers.get("X-Vbundle-Manifest-Sha256")!.length).toBe(64);
122
- });
123
-
124
- test("returned archive passes vbundle validation", async () => {
125
- const req = new Request("http://localhost/v1/migrations/export", {
126
- method: "POST",
127
- });
128
-
129
- const res = await callHandler(handleMigrationExport, req);
130
- const arrayBuffer = await res.arrayBuffer();
131
- const archiveData = new Uint8Array(arrayBuffer);
132
-
133
- const validationResult = validateVBundle(archiveData);
134
-
135
- expect(validationResult.is_valid).toBe(true);
136
- expect(validationResult.errors).toHaveLength(0);
137
- expect(validationResult.manifest).toBeDefined();
138
- });
139
-
140
- test("manifest has correct structure and metadata", async () => {
141
- const req = new Request("http://localhost/v1/migrations/export", {
142
- method: "POST",
143
- });
144
-
145
- const res = await callHandler(handleMigrationExport, req);
146
- const arrayBuffer = await res.arrayBuffer();
147
- const archiveData = new Uint8Array(arrayBuffer);
148
-
149
- const validationResult = validateVBundle(archiveData);
150
- const manifest = validationResult.manifest!;
151
-
152
- expect(manifest.schema_version).toBe(1);
153
- expect(manifest.bundle_id).toBeDefined();
154
- expect(manifest.created_at).toBeDefined();
155
- expect(manifest.checksum).toBeDefined();
156
-
157
- // Verify file entries — workspace walk uses workspace/ prefix
158
- const filePaths = manifest.contents.map((f) => f.path);
159
- expect(filePaths).toContain("workspace/data/db/assistant.db");
160
- expect(filePaths).toContain("workspace/config.json");
161
-
162
- // Verify each file entry has proper sha256 and size
163
- for (const file of manifest.contents) {
164
- expect(file.sha256).toBeDefined();
165
- expect(file.sha256.length).toBe(64);
166
- expect(file.size_bytes).toBeGreaterThanOrEqual(0);
167
- }
168
- });
169
-
170
- test("custom description in JSON body is accepted but no longer surfaced in manifest", async () => {
171
- const req = new Request("http://localhost/v1/migrations/export", {
172
- method: "POST",
173
- headers: { "Content-Type": "application/json" },
174
- body: JSON.stringify({ description: "My custom export description" }),
175
- });
176
-
177
- const res = await callHandler(handleMigrationExport, req);
178
- const arrayBuffer = await res.arrayBuffer();
179
- const archiveData = new Uint8Array(arrayBuffer);
180
-
181
- const validationResult = validateVBundle(archiveData);
182
- expect(validationResult.is_valid).toBe(true);
183
- // v1 manifests intentionally drop the legacy `description` field —
184
- // assert the export still succeeds even though the body carried one.
185
- });
186
-
187
- test("invalid JSON body is gracefully handled with defaults", async () => {
188
- const req = new Request("http://localhost/v1/migrations/export", {
189
- method: "POST",
190
- headers: { "Content-Type": "application/json" },
191
- body: "not valid json{{{",
192
- });
193
-
194
- const res = await callHandler(handleMigrationExport, req);
195
-
196
- expect(res.status).toBe(200);
197
- expect(res.headers.get("Content-Type")).toBe("application/octet-stream");
198
-
199
- const arrayBuffer = await res.arrayBuffer();
200
- const archiveData = new Uint8Array(arrayBuffer);
201
- const validationResult = validateVBundle(archiveData);
202
-
203
- expect(validationResult.is_valid).toBe(true);
204
- });
205
-
206
- test("POST without Content-Type returns valid archive", async () => {
207
- const req = new Request("http://localhost/v1/migrations/export", {
208
- method: "POST",
209
- });
210
-
211
- const res = await callHandler(handleMigrationExport, req);
212
-
213
- expect(res.status).toBe(200);
214
-
215
- const arrayBuffer = await res.arrayBuffer();
216
- const archiveData = new Uint8Array(arrayBuffer);
217
- const validationResult = validateVBundle(archiveData);
218
-
219
- expect(validationResult.is_valid).toBe(true);
220
- });
221
-
222
- test("Content-Length header matches actual body length", async () => {
223
- const req = new Request("http://localhost/v1/migrations/export", {
224
- method: "POST",
225
- });
226
-
227
- const res = await callHandler(handleMigrationExport, req);
228
- const arrayBuffer = await res.arrayBuffer();
229
-
230
- expect(Number(res.headers.get("Content-Length"))).toBe(
231
- arrayBuffer.byteLength,
232
- );
233
- });
234
- });
235
-
236
- // ---------------------------------------------------------------------------
237
- // Real data population tests
238
- // ---------------------------------------------------------------------------
239
-
240
- describe("export data population", () => {
241
- test("archive contains actual database file content from disk", async () => {
242
- const req = new Request("http://localhost/v1/migrations/export", {
243
- method: "POST",
244
- });
245
-
246
- const res = await callHandler(handleMigrationExport, req);
247
- const archiveData = new Uint8Array(await res.arrayBuffer());
248
- const entries = parseTarEntries(archiveData);
249
-
250
- const dbEntry = entries.find(
251
- (e) => e.name === "workspace/data/db/assistant.db",
252
- );
253
- expect(dbEntry).toBeDefined();
254
- expect(dbEntry!.data.length).toBe(SQLITE_HEADER.length);
255
- // Verify the exported data matches the test fixture exactly
256
- expect(sha256Hex(dbEntry!.data)).toBe(sha256Hex(SQLITE_HEADER));
257
- });
258
-
259
- test("archive contains actual config file content from disk", async () => {
260
- const req = new Request("http://localhost/v1/migrations/export", {
261
- method: "POST",
262
- });
263
-
264
- const res = await callHandler(handleMigrationExport, req);
265
- const archiveData = new Uint8Array(await res.arrayBuffer());
266
- const entries = parseTarEntries(archiveData);
267
-
268
- const configEntry = entries.find((e) => e.name === "workspace/config.json");
269
- expect(configEntry).toBeDefined();
270
-
271
- const configContent = new TextDecoder().decode(configEntry!.data);
272
- const parsedConfig = JSON.parse(configContent);
273
- expect(parsedConfig.provider).toBe("anthropic");
274
- expect(parsedConfig.model).toBe("test-model");
275
- });
276
-
277
- test("manifest file sizes match actual file sizes on disk", async () => {
278
- const req = new Request("http://localhost/v1/migrations/export", {
279
- method: "POST",
280
- });
281
-
282
- const res = await callHandler(handleMigrationExport, req);
283
- const archiveData = new Uint8Array(await res.arrayBuffer());
284
-
285
- const validationResult = validateVBundle(archiveData);
286
- const manifest = validationResult.manifest!;
287
-
288
- const dbFile = manifest.contents.find(
289
- (f) => f.path === "workspace/data/db/assistant.db",
290
- );
291
- expect(dbFile).toBeDefined();
292
- expect(dbFile!.size_bytes).toBe(SQLITE_HEADER.length);
293
-
294
- const configFile = manifest.contents.find(
295
- (f) => f.path === "workspace/config.json",
296
- );
297
- expect(configFile).toBeDefined();
298
- const expectedConfigSize = Buffer.byteLength(
299
- JSON.stringify(TEST_CONFIG, null, 2) + "\n",
300
- );
301
- expect(configFile!.size_bytes).toBe(expectedConfigSize);
302
- });
303
-
304
- test("manifest checksums match actual file content", async () => {
305
- const req = new Request("http://localhost/v1/migrations/export", {
306
- method: "POST",
307
- });
308
-
309
- const res = await callHandler(handleMigrationExport, req);
310
- const archiveData = new Uint8Array(await res.arrayBuffer());
311
- const entries = parseTarEntries(archiveData);
312
-
313
- const validationResult = validateVBundle(archiveData);
314
- const manifest = validationResult.manifest!;
315
-
316
- for (const fileEntry of manifest.contents) {
317
- const tarEntry = entries.find((e) => e.name === fileEntry.path);
318
- expect(tarEntry).toBeDefined();
319
- expect(sha256Hex(tarEntry!.data)).toBe(fileEntry.sha256);
320
- }
321
- });
322
-
323
- test("database content is non-empty (real data, not skeleton)", async () => {
324
- const req = new Request("http://localhost/v1/migrations/export", {
325
- method: "POST",
326
- });
327
-
328
- const res = await callHandler(handleMigrationExport, req);
329
- const archiveData = new Uint8Array(await res.arrayBuffer());
330
-
331
- const validationResult = validateVBundle(archiveData);
332
- const manifest = validationResult.manifest!;
333
-
334
- const dbFile = manifest.contents.find(
335
- (f) => f.path === "workspace/data/db/assistant.db",
336
- );
337
- expect(dbFile).toBeDefined();
338
- // The skeleton used size 0 — real export should have actual content
339
- expect(dbFile!.size_bytes).toBeGreaterThan(0);
340
- });
341
-
342
- test("config content is real JSON (not empty object placeholder)", async () => {
343
- const req = new Request("http://localhost/v1/migrations/export", {
344
- method: "POST",
345
- });
346
-
347
- const res = await callHandler(handleMigrationExport, req);
348
- const archiveData = new Uint8Array(await res.arrayBuffer());
349
- const entries = parseTarEntries(archiveData);
350
-
351
- const configEntry = entries.find((e) => e.name === "workspace/config.json");
352
- expect(configEntry).toBeDefined();
353
-
354
- const configContent = new TextDecoder().decode(configEntry!.data);
355
- // The skeleton used "{}" — real export should have actual config
356
- expect(configContent).not.toBe("{}");
357
- expect(JSON.parse(configContent)).toHaveProperty("provider");
358
- });
359
- });
360
-
361
- // ---------------------------------------------------------------------------
362
- // Graceful fallback tests
363
- // ---------------------------------------------------------------------------
364
-
365
- describe("export graceful fallback", () => {
366
- test("nonexistent workspace produces archive that fails the v1 contents refine", async () => {
367
- const { buildExportVBundle } =
368
- await import("../runtime/migrations/vbundle-builder.js");
369
-
370
- const result = buildExportVBundle({
371
- workspaceDir: join(testDir, "nonexistent-workspace"),
372
- ...defaultV1Options(),
373
- });
374
-
375
- // Under v1, every bundle MUST declare a `data/db/assistant.db` (or
376
- // `workspace/data/db/assistant.db`) entry. A nonexistent workspace
377
- // produces an empty contents array, which the validator's `.refine()`
378
- // rejects. Documenting this so a future regression on the workspace
379
- // walk doesn't silently produce a structurally-empty bundle.
380
- const validationResult = validateVBundle(result.archive);
381
- expect(validationResult.is_valid).toBe(false);
382
- expect(result.manifest.contents).toHaveLength(0);
383
- });
384
-
385
- test("workspace walk includes db and config under workspace/ prefix", async () => {
386
- const { buildExportVBundle } =
387
- await import("../runtime/migrations/vbundle-builder.js");
388
-
389
- const result = buildExportVBundle({
390
- workspaceDir: testDir,
391
- ...defaultV1Options(),
392
- });
393
-
394
- const validationResult = validateVBundle(result.archive);
395
- expect(validationResult.is_valid).toBe(true);
396
-
397
- const dbFile = result.manifest.contents.find(
398
- (f) => f.path === "workspace/data/db/assistant.db",
399
- );
400
- expect(dbFile).toBeDefined();
401
- expect(dbFile!.size_bytes).toBeGreaterThan(0);
402
-
403
- const configFile = result.manifest.contents.find(
404
- (f) => f.path === "workspace/config.json",
405
- );
406
- expect(configFile).toBeDefined();
407
- expect(configFile!.size_bytes).toBeGreaterThan(0);
408
- });
409
- });
410
-
411
- // ---------------------------------------------------------------------------
412
- // Config sanitization tests
413
- // ---------------------------------------------------------------------------
414
-
415
- describe("export config sanitization", () => {
416
- test("exported config.json has environment-specific fields stripped", async () => {
417
- // Write a config with environment-specific fields that should be stripped
418
- const configWithEnvFields = {
419
- provider: "anthropic",
420
- model: "test-model",
421
- ingress: {
422
- publicBaseUrl: "https://my-tunnel.example.com",
423
- enabled: true,
424
- port: 8080,
425
- },
426
- daemon: {
427
- autoStart: true,
428
- logLevel: "debug",
429
- },
430
- skills: {
431
- load: {
432
- extraDirs: ["/home/user/custom-skills", "/opt/skills"],
433
- autoReload: true,
434
- },
435
- },
436
- memory: { enabled: true },
437
- };
438
- writeFileSync(testConfigPath, JSON.stringify(configWithEnvFields, null, 2));
439
-
440
- const req = new Request("http://localhost/v1/migrations/export", {
441
- method: "POST",
442
- });
443
-
444
- const res = await callHandler(handleMigrationExport, req);
445
- const archiveData = new Uint8Array(await res.arrayBuffer());
446
- const entries = parseTarEntries(archiveData);
447
-
448
- const configEntry = entries.find((e) => e.name === "workspace/config.json");
449
- expect(configEntry).toBeDefined();
450
-
451
- const parsedConfig = JSON.parse(
452
- new TextDecoder().decode(configEntry!.data),
453
- );
454
-
455
- // Environment-specific fields should be stripped/reset
456
- expect(parsedConfig.ingress.publicBaseUrl).toBe("");
457
- expect(parsedConfig.ingress.enabled).toBeUndefined();
458
- expect(parsedConfig.daemon).toBeUndefined();
459
- expect(parsedConfig.skills.load.extraDirs).toEqual([]);
460
-
461
- // Non-environment-specific fields should be preserved
462
- expect(parsedConfig.provider).toBe("anthropic");
463
- expect(parsedConfig.model).toBe("test-model");
464
- expect(parsedConfig.ingress.port).toBe(8080);
465
- expect(parsedConfig.skills.load.autoReload).toBe(true);
466
- expect(parsedConfig.memory.enabled).toBe(true);
467
- });
468
- });
469
-
470
- // ---------------------------------------------------------------------------
471
- // Auth policy registration tests
472
- // ---------------------------------------------------------------------------
473
-
474
- describe("route policy registration", () => {
475
- test("migrations/export policy requires settings.write scope", async () => {
476
- const { ROUTES } = await import("../runtime/routes/index.js");
477
- const policy =
478
- ROUTES.find((r) => r.endpoint === "migrations/export")?.policy ?? null;
479
-
480
- expect(policy).not.toBeNull();
481
- expect(policy?.requiredScopes).toContain("settings.write");
482
- expect(policy?.allowedPrincipalTypes).toContain("actor");
483
- expect(policy?.allowedPrincipalTypes).toContain("svc_gateway");
484
- expect(policy?.allowedPrincipalTypes).toContain("local");
485
- });
486
-
487
- test("migrations/validate policy is still registered", async () => {
488
- const { ROUTES } = await import("../runtime/routes/index.js");
489
- const policy =
490
- ROUTES.find((r) => r.endpoint === "migrations/validate")?.policy ?? null;
491
-
492
- expect(policy).not.toBeNull();
493
- expect(policy?.requiredScopes).toContain("settings.read");
494
- });
495
- });
496
-
497
- // ---------------------------------------------------------------------------
498
- // Auth policy shape tests
499
- // ---------------------------------------------------------------------------
500
-
501
- describe("auth policy shape", () => {
502
- test("export policy requires settings.write and would deny without it", async () => {
503
- const { ROUTES } = await import("../runtime/routes/index.js");
504
- const policy =
505
- ROUTES.find((r) => r.endpoint === "migrations/export")?.policy ?? null;
506
-
507
- expect(policy).not.toBeNull();
508
- // Verify the policy shape means a caller without settings.write would be denied
509
- expect(policy!.requiredScopes).toEqual(["settings.write"]);
510
- // Verify only the expected principal types are allowed
511
- expect(policy!.allowedPrincipalTypes).toEqual(
512
- expect.arrayContaining(["actor", "svc_gateway", "svc_daemon", "local"]),
513
- );
514
- expect(policy!.allowedPrincipalTypes).toHaveLength(4);
515
- });
516
-
517
- test("export policy differs from validate policy on scopes (validate is read-only)", async () => {
518
- const { ROUTES } = await import("../runtime/routes/index.js");
519
- const exportPolicy =
520
- ROUTES.find((r) => r.endpoint === "migrations/export")?.policy ?? null;
521
- const validatePolicy =
522
- ROUTES.find((r) => r.endpoint === "migrations/validate")?.policy ?? null;
523
-
524
- // validate is read-only so requires settings.read; export requires settings.write
525
- expect(exportPolicy!.requiredScopes).toEqual(["settings.write"]);
526
- expect(validatePolicy!.requiredScopes).toEqual(["settings.read"]);
527
- // Both share the same principal types
528
- expect(exportPolicy!.allowedPrincipalTypes).toEqual(
529
- validatePolicy!.allowedPrincipalTypes,
530
- );
531
- });
532
- });
533
-
534
- // ---------------------------------------------------------------------------
535
- // Integration: existing routes unaffected
536
- // ---------------------------------------------------------------------------
537
-
538
- describe("integration: existing routes unaffected", () => {
539
- test("validate endpoint still works correctly", async () => {
540
- // Just confirm that importing both handlers works without conflicts
541
- const validateHandler = handleMigrationValidate;
542
- const exportHandler = handleMigrationExport;
543
-
544
- expect(typeof validateHandler).toBe("function");
545
- expect(typeof exportHandler).toBe("function");
546
- });
547
-
548
- test("GET /v1/health still works (not intercepted by migration routes)", async () => {
549
- const { handleDetailedHealth } =
550
- await import("../runtime/routes/identity-routes.js");
551
- const res = await handleDetailedHealth();
552
- const body = (await res.json()) as Record<string, unknown>;
553
-
554
- expect(res.status).toBe(200);
555
- expect(body.status).toBe("healthy");
556
- });
557
- });