@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,319 +0,0 @@
1
- /**
2
- * Tests for the symlink-aware directory walkers used by vbundle export.
3
- *
4
- * Two walkers exist (`walkDirectory` for the buffered path,
5
- * `walkDirectoryForMetadata` for the streaming path) and they must classify
6
- * symlinks identically: bundleable in-workspace targets become typeflag-2
7
- * entries, while broken / directory / outside-workspace / inside-skipDir
8
- * targets are dropped and reported. Each scenario below is exercised against
9
- * both walkers via describe.each so any drift between them fails loudly.
10
- *
11
- * One additional integration test drives `buildExportVBundle` end-to-end and
12
- * checks both that the dropped paths are summarized in a single aggregated
13
- * `log.warn` call and that a class-1 round-trip survives `validateVBundle`.
14
- */
15
-
16
- import {
17
- mkdirSync,
18
- mkdtempSync,
19
- realpathSync,
20
- rmSync,
21
- symlinkSync,
22
- writeFileSync,
23
- } from "node:fs";
24
- import { tmpdir } from "node:os";
25
- import { join } from "node:path";
26
- import { afterEach, describe, expect, mock, test } from "bun:test";
27
-
28
- const mockLogWarn = mock((_obj: unknown, _msg: string) => {});
29
- mock.module("../../../util/logger.js", () => ({
30
- getLogger: () => ({
31
- warn: mockLogWarn,
32
- info: () => {},
33
- error: () => {},
34
- debug: () => {},
35
- trace: () => {},
36
- }),
37
- }));
38
-
39
- const { buildExportVBundle, walkDirectory, walkDirectoryForMetadata } =
40
- await import("../vbundle-builder.js");
41
- const { validateVBundle } = await import("../vbundle-validator.js");
42
- const { defaultV1Options } = await import("./v1-test-helpers.js");
43
-
44
- // ---------------------------------------------------------------------------
45
- // Workspace fixtures
46
- // ---------------------------------------------------------------------------
47
-
48
- const cleanupQueue: string[] = [];
49
-
50
- function makeTempDir(prefix: string): string {
51
- // Resolve to the canonical path so walker comparisons (which call
52
- // realpathSync on the symlink target) line up with the workspace root on
53
- // platforms where tmpdir() is itself a symlink (macOS: /var → /private/var).
54
- const dir = realpathSync(mkdtempSync(join(tmpdir(), prefix)));
55
- cleanupQueue.push(dir);
56
- return dir;
57
- }
58
-
59
- afterEach(() => {
60
- while (cleanupQueue.length > 0) {
61
- const dir = cleanupQueue.pop();
62
- if (dir) {
63
- rmSync(dir, { recursive: true, force: true });
64
- }
65
- }
66
- mockLogWarn.mockClear();
67
- });
68
-
69
- // ---------------------------------------------------------------------------
70
- // Walker adapters — normalize both walker shapes to a common surface so the
71
- // parametrized tests below can assert the same fields against either one.
72
- // ---------------------------------------------------------------------------
73
-
74
- interface NormalizedSymlinkEntry {
75
- archivePath: string;
76
- linkTarget: string;
77
- }
78
-
79
- interface NormalizedWalkResult {
80
- fileArchivePaths: string[];
81
- symlinks: NormalizedSymlinkEntry[];
82
- droppedSymlinks: string[];
83
- }
84
-
85
- interface WalkerAdapter {
86
- name: string;
87
- walk: (
88
- dir: string,
89
- prefix: string,
90
- skipDirs: string[],
91
- ) => NormalizedWalkResult;
92
- }
93
-
94
- const walkers: WalkerAdapter[] = [
95
- {
96
- name: "walkDirectory",
97
- walk: (dir, prefix, skipDirs) => {
98
- const result = walkDirectory(dir, prefix, {
99
- includeBinary: true,
100
- skipDirs,
101
- });
102
- return {
103
- fileArchivePaths: result.files
104
- .filter((f) => f.linkTarget === undefined)
105
- .map((f) => f.path),
106
- symlinks: result.files
107
- .filter((f) => f.linkTarget !== undefined)
108
- .map((f) => ({ archivePath: f.path, linkTarget: f.linkTarget! })),
109
- droppedSymlinks: result.droppedSymlinks,
110
- };
111
- },
112
- },
113
- {
114
- name: "walkDirectoryForMetadata",
115
- walk: (dir, prefix, skipDirs) => {
116
- const result = walkDirectoryForMetadata(dir, prefix, {
117
- includeBinary: true,
118
- skipDirs,
119
- });
120
- return {
121
- fileArchivePaths: result.files.map((f) => f.archivePath),
122
- symlinks: result.symlinks.map((s) => ({
123
- archivePath: s.archivePath,
124
- linkTarget: s.linkTarget,
125
- })),
126
- droppedSymlinks: result.droppedSymlinks,
127
- };
128
- },
129
- },
130
- ];
131
-
132
- // ---------------------------------------------------------------------------
133
- // Per-walker classification tests
134
- // ---------------------------------------------------------------------------
135
-
136
- describe.each(walkers)("$name — symlink classification", ({ walk }) => {
137
- test("class 1: in-workspace symlink is emitted with relative linkTarget", () => {
138
- const root = makeTempDir("vbundle-walker-class1-");
139
- mkdirSync(join(root, "skills"), { recursive: true });
140
- writeFileSync(join(root, "skills", "bar.md"), "bar contents");
141
- symlinkSync("bar.md", join(root, "skills", "foo.md"));
142
-
143
- const result = walk(root, "workspace", []);
144
-
145
- expect(result.droppedSymlinks).toEqual([]);
146
- expect(result.fileArchivePaths).toContain("workspace/skills/bar.md");
147
- expect(result.symlinks).toEqual([
148
- {
149
- archivePath: "workspace/skills/foo.md",
150
- linkTarget: "bar.md",
151
- },
152
- ]);
153
- });
154
-
155
- test("class 2: symlink targeting a path outside the workspace is dropped", () => {
156
- const root = makeTempDir("vbundle-walker-class2-");
157
- const outside = makeTempDir("vbundle-walker-outside-");
158
- mkdirSync(join(root, "skills"), { recursive: true });
159
- const outsideTarget = join(outside, "external.md");
160
- writeFileSync(outsideTarget, "external contents");
161
- symlinkSync(outsideTarget, join(root, "skills", "dev"));
162
-
163
- const result = walk(root, "workspace", []);
164
-
165
- expect(result.symlinks).toEqual([]);
166
- expect(result.droppedSymlinks).toContain(join("skills", "dev"));
167
- });
168
-
169
- test("class 3: symlink targeting a skipDir is dropped", () => {
170
- const root = makeTempDir("vbundle-walker-class3-");
171
- mkdirSync(join(root, "embedding-models"), { recursive: true });
172
- mkdirSync(join(root, "skills"), { recursive: true });
173
- writeFileSync(join(root, "embedding-models", "x.bin"), "blob");
174
- symlinkSync(
175
- join(root, "embedding-models", "x.bin"),
176
- join(root, "skills", "cache"),
177
- );
178
-
179
- const result = walk(root, "workspace", ["embedding-models"]);
180
-
181
- expect(result.symlinks).toEqual([]);
182
- expect(result.droppedSymlinks).toContain(join("skills", "cache"));
183
- // The skipDir itself is not walked, so its files are not emitted either.
184
- expect(result.fileArchivePaths).not.toContain(
185
- "workspace/embedding-models/x.bin",
186
- );
187
- });
188
-
189
- test("directory targets are out of scope and dropped", () => {
190
- const root = makeTempDir("vbundle-walker-dirtarget-");
191
- mkdirSync(join(root, "skills", "sub-dir"), { recursive: true });
192
- writeFileSync(join(root, "skills", "sub-dir", "nested.md"), "nested");
193
- symlinkSync(
194
- join(root, "skills", "sub-dir"),
195
- join(root, "skills", "dir-link"),
196
- );
197
-
198
- const result = walk(root, "workspace", []);
199
-
200
- expect(result.symlinks).toEqual([]);
201
- expect(result.droppedSymlinks).toContain(join("skills", "dir-link"));
202
- // The real sub-dir is still walked through normally.
203
- expect(result.fileArchivePaths).toContain(
204
- "workspace/skills/sub-dir/nested.md",
205
- );
206
- });
207
-
208
- test("broken symlinks are dropped without throwing", () => {
209
- const root = makeTempDir("vbundle-walker-broken-");
210
- mkdirSync(join(root, "skills"), { recursive: true });
211
- symlinkSync(
212
- join(root, "skills", "does-not-exist.md"),
213
- join(root, "skills", "broken"),
214
- );
215
-
216
- const result = walk(root, "workspace", []);
217
-
218
- expect(result.symlinks).toEqual([]);
219
- expect(result.droppedSymlinks).toContain(join("skills", "broken"));
220
- });
221
-
222
- test("class 1: in-workspace symlink is emitted when walk root is a non-canonical path with symlinked prefix", () => {
223
- // Intentionally NOT canonicalized — on macOS this stays as
224
- // /var/folders/... while realpathSync canonicalizes to
225
- // /private/var/folders/.... Without the realpathSync(walkRoot) fix in
226
- // classifySymlink, the containment check misclassifies in-workspace
227
- // symlinks as "outside workspace" and silently drops them.
228
- const ws = mkdtempSync(join(tmpdir(), "vbundle-walker-noncanon-"));
229
- cleanupQueue.push(ws);
230
- mkdirSync(join(ws, "skills"), { recursive: true });
231
- writeFileSync(join(ws, "skills", "bar.md"), "hello");
232
- symlinkSync("bar.md", join(ws, "skills", "foo.md"));
233
-
234
- const result = walk(ws, "workspace", []);
235
-
236
- expect(result.droppedSymlinks).toEqual([]);
237
- expect(result.symlinks).toEqual([
238
- {
239
- archivePath: "workspace/skills/foo.md",
240
- linkTarget: "bar.md",
241
- },
242
- ]);
243
- });
244
- });
245
-
246
- // ---------------------------------------------------------------------------
247
- // Aggregated warning + round-trip integration
248
- // ---------------------------------------------------------------------------
249
-
250
- describe("buildExportVBundle aggregated symlink warning", () => {
251
- test("emits a single warn call covering every dropped symlink", () => {
252
- const root = makeTempDir("vbundle-walker-agg-");
253
- const outsideA = makeTempDir("vbundle-walker-agg-outA-");
254
- const outsideB = makeTempDir("vbundle-walker-agg-outB-");
255
- // Required workspace fixture so the walker has at least one regular file.
256
- writeFileSync(join(root, "config.json"), JSON.stringify({ test: true }));
257
- mkdirSync(join(root, "data", "db"), { recursive: true });
258
- writeFileSync(join(root, "data", "db", "assistant.db"), "fake-db");
259
-
260
- mkdirSync(join(root, "skills"), { recursive: true });
261
- mkdirSync(join(root, "embedding-models"), { recursive: true });
262
- writeFileSync(join(root, "embedding-models", "blob.bin"), "blob");
263
-
264
- const externalA = join(outsideA, "ext.md");
265
- const externalB = join(outsideB, "ext.md");
266
- writeFileSync(externalA, "ext-a");
267
- writeFileSync(externalB, "ext-b");
268
-
269
- symlinkSync(externalA, join(root, "skills", "ext-a"));
270
- symlinkSync(externalB, join(root, "skills", "ext-b"));
271
- symlinkSync(
272
- join(root, "embedding-models", "blob.bin"),
273
- join(root, "skills", "cache"),
274
- );
275
-
276
- buildExportVBundle({
277
- workspaceDir: root,
278
- ...defaultV1Options(),
279
- });
280
-
281
- expect(mockLogWarn).toHaveBeenCalledTimes(1);
282
- const [meta, msg] = mockLogWarn.mock.calls[0];
283
- expect(meta).toMatchObject({ count: 3 });
284
- expect((meta as { paths: string[] }).paths.sort()).toEqual(
285
- [
286
- join("skills", "cache"),
287
- join("skills", "ext-a"),
288
- join("skills", "ext-b"),
289
- ].sort(),
290
- );
291
- expect(typeof msg).toBe("string");
292
- });
293
-
294
- test("class-1 symlink survives buildExportVBundle round-trip via validateVBundle", () => {
295
- const root = makeTempDir("vbundle-walker-rt-");
296
- writeFileSync(join(root, "config.json"), JSON.stringify({ test: true }));
297
- mkdirSync(join(root, "data", "db"), { recursive: true });
298
- writeFileSync(join(root, "data", "db", "assistant.db"), "fake-db");
299
- mkdirSync(join(root, "skills"), { recursive: true });
300
- writeFileSync(join(root, "skills", "bar.md"), "bar contents");
301
- symlinkSync("bar.md", join(root, "skills", "foo.md"));
302
-
303
- const result = buildExportVBundle({
304
- workspaceDir: root,
305
- ...defaultV1Options(),
306
- });
307
-
308
- const validation = validateVBundle(result.archive);
309
- expect(validation.errors).toEqual([]);
310
- expect(validation.is_valid).toBe(true);
311
-
312
- const symlinkEntry = result.manifest.contents.find(
313
- (f) => f.path === "workspace/skills/foo.md",
314
- );
315
- expect(symlinkEntry).toBeDefined();
316
- expect(symlinkEntry?.link_target).toBe("bar.md");
317
- expect(symlinkEntry?.size_bytes).toBe(0);
318
- });
319
- });
@@ -1,240 +0,0 @@
1
- /**
2
- * Tests for `parseVBundleStream` — the streaming tar reader for `.vbundle`
3
- * archives.
4
- *
5
- * Covered:
6
- * - Happy path: 3-file archive yields entries in order with correct names/sizes.
7
- * - Manifest-first invariant: first entry is `manifest.json`.
8
- * - Truncated gzip mid-stream: generator throws.
9
- * - Valid gzip but malformed tar payload: generator throws.
10
- * - Early termination (break in for-await loop): upstream source is destroyed.
11
- */
12
-
13
- import { Readable } from "node:stream";
14
- import { gzipSync } from "node:zlib";
15
- import { describe, expect, test } from "bun:test";
16
-
17
- import { buildVBundle } from "../vbundle-builder.js";
18
- import {
19
- parseVBundleStream,
20
- type StreamedTarEntry,
21
- } from "../vbundle-tar-stream.js";
22
- import { defaultV1Options } from "./v1-test-helpers.js";
23
-
24
- // ---------------------------------------------------------------------------
25
- // Helpers
26
- // ---------------------------------------------------------------------------
27
-
28
- async function collectBody(entry: StreamedTarEntry): Promise<Uint8Array> {
29
- const chunks: Buffer[] = [];
30
- for await (const chunk of entry.body) {
31
- chunks.push(chunk as Buffer);
32
- }
33
- return new Uint8Array(Buffer.concat(chunks));
34
- }
35
-
36
- function readableFromBuffer(buf: Uint8Array): Readable {
37
- // `Readable.from(Uint8Array)` iterates byte-by-byte (each element becomes
38
- // a chunk), which is not what we want here. Wrap in an array so the whole
39
- // buffer arrives as a single chunk — closer to how the HTTP client will
40
- // feed bytes in production.
41
- return Readable.from([Buffer.from(buf)]);
42
- }
43
-
44
- /** Build a minimal vbundle archive with the given extra files (plus manifest). */
45
- function buildMinimalVBundle(
46
- extraFiles: { path: string; data: Uint8Array }[],
47
- ): Uint8Array {
48
- const { archive } = buildVBundle({
49
- files: extraFiles,
50
- ...defaultV1Options(),
51
- });
52
- return archive;
53
- }
54
-
55
- // ---------------------------------------------------------------------------
56
- // Happy path
57
- // ---------------------------------------------------------------------------
58
-
59
- describe("parseVBundleStream — happy path", () => {
60
- test("yields entries in order with correct names and sizes", async () => {
61
- const fileA = new TextEncoder().encode("alpha\n");
62
- const fileB = new TextEncoder().encode("beta beta\n");
63
- const fileC = new TextEncoder().encode("gamma gamma gamma\n");
64
-
65
- const archive = buildMinimalVBundle([
66
- { path: "workspace/a.txt", data: fileA },
67
- { path: "workspace/b.txt", data: fileB },
68
- { path: "workspace/c.txt", data: fileC },
69
- ]);
70
-
71
- const source = readableFromBuffer(archive);
72
- const seen: { name: string; size: number; body: Uint8Array }[] = [];
73
-
74
- for await (const entry of parseVBundleStream(source)) {
75
- const body = await collectBody(entry);
76
- seen.push({
77
- name: entry.header.name,
78
- size: entry.header.size,
79
- body,
80
- });
81
- }
82
-
83
- // manifest.json is emitted first, then the 3 files in insertion order.
84
- expect(seen.length).toBe(4);
85
- expect(seen[0]?.name).toBe("manifest.json");
86
- expect(seen[1]?.name).toBe("workspace/a.txt");
87
- expect(seen[2]?.name).toBe("workspace/b.txt");
88
- expect(seen[3]?.name).toBe("workspace/c.txt");
89
-
90
- // Sizes in the header match the body lengths.
91
- expect(seen[1]?.size).toBe(fileA.length);
92
- expect(seen[1]?.body.length).toBe(fileA.length);
93
- expect(seen[2]?.size).toBe(fileB.length);
94
- expect(seen[2]?.body.length).toBe(fileB.length);
95
- expect(seen[3]?.size).toBe(fileC.length);
96
- expect(seen[3]?.body.length).toBe(fileC.length);
97
-
98
- // Body contents round-trip correctly.
99
- expect(new TextDecoder().decode(seen[1]?.body)).toBe("alpha\n");
100
- expect(new TextDecoder().decode(seen[2]?.body)).toBe("beta beta\n");
101
- expect(new TextDecoder().decode(seen[3]?.body)).toBe("gamma gamma gamma\n");
102
- });
103
- });
104
-
105
- // ---------------------------------------------------------------------------
106
- // Manifest-first invariant
107
- // ---------------------------------------------------------------------------
108
-
109
- describe("parseVBundleStream — manifest-first", () => {
110
- test("first entry is manifest.json", async () => {
111
- const archive = buildMinimalVBundle([
112
- { path: "z-last.txt", data: new TextEncoder().encode("zzz") },
113
- ]);
114
-
115
- const iter = parseVBundleStream(readableFromBuffer(archive));
116
- const first = await iter.next();
117
- expect(first.done).toBe(false);
118
- expect(first.value?.header.name).toBe("manifest.json");
119
- expect(first.value?.header.type).toBe("file");
120
-
121
- // Drain the manifest body so the iterator can advance, then finish.
122
- if (first.value) {
123
- await collectBody(first.value);
124
- }
125
- // Exhaust the iterator to release resources. Drain each body so the
126
- // extractor can advance. (We don't assert on the remaining entries here.)
127
- for await (const entry of iter) {
128
- await collectBody(entry);
129
- }
130
- });
131
- });
132
-
133
- // ---------------------------------------------------------------------------
134
- // Error handling
135
- // ---------------------------------------------------------------------------
136
-
137
- describe("parseVBundleStream — errors", () => {
138
- test("throws on truncated gzip mid-stream", async () => {
139
- const archive = buildMinimalVBundle([
140
- { path: "workspace/a.txt", data: new TextEncoder().encode("hello") },
141
- ]);
142
-
143
- // Lop off the trailing bytes of the gzip member so the decoder fails
144
- // partway through.
145
- const truncated = archive.subarray(0, Math.max(32, archive.length - 20));
146
- const source = readableFromBuffer(new Uint8Array(truncated));
147
-
148
- let threw = false;
149
- try {
150
- for await (const entry of parseVBundleStream(source)) {
151
- // Drain whatever the decoder can produce before the error hits.
152
- try {
153
- await collectBody(entry);
154
- } catch {
155
- // Body streams may error mid-drain; that also counts as a failure
156
- // in the outer iterator on the next advance.
157
- threw = true;
158
- break;
159
- }
160
- }
161
- } catch {
162
- threw = true;
163
- }
164
-
165
- expect(threw).toBe(true);
166
- });
167
-
168
- test("throws on valid gzip wrapping malformed tar bytes", async () => {
169
- // Gzipped junk — valid gzip member, but the inflated bytes aren't a tar.
170
- const junk = new TextEncoder().encode("this is not a tar archive payload");
171
- // Pad out so the extractor has enough bytes to attempt a header parse.
172
- const padded = new Uint8Array(1024);
173
- padded.set(junk, 0);
174
- const gz = gzipSync(padded);
175
- const source = readableFromBuffer(new Uint8Array(gz));
176
-
177
- let threw = false;
178
- try {
179
- for await (const entry of parseVBundleStream(source)) {
180
- try {
181
- await collectBody(entry);
182
- } catch {
183
- threw = true;
184
- break;
185
- }
186
- }
187
- } catch {
188
- threw = true;
189
- }
190
-
191
- expect(threw).toBe(true);
192
- });
193
- });
194
-
195
- // ---------------------------------------------------------------------------
196
- // Early-termination cleanup
197
- // ---------------------------------------------------------------------------
198
-
199
- describe("parseVBundleStream — cleanup", () => {
200
- test("destroys upstream source when caller breaks out of the loop", async () => {
201
- const archive = buildMinimalVBundle([
202
- { path: "workspace/a.txt", data: new TextEncoder().encode("hello") },
203
- { path: "workspace/b.txt", data: new TextEncoder().encode("world") },
204
- ]);
205
-
206
- const source = readableFromBuffer(archive);
207
- let destroyCalls = 0;
208
- const originalDestroy = source.destroy.bind(source);
209
- source.destroy = ((err?: Error) => {
210
- destroyCalls += 1;
211
- return originalDestroy(err);
212
- }) as typeof source.destroy;
213
-
214
- for await (const entry of parseVBundleStream(source)) {
215
- // Consume just the first entry (manifest.json), then bail.
216
- await collectBody(entry);
217
- break;
218
- }
219
-
220
- expect(destroyCalls).toBeGreaterThan(0);
221
- // Source should now be destroyed (no dangling listeners keeping it alive).
222
- expect(source.destroyed).toBe(true);
223
- });
224
-
225
- test("throws (does not hang) when source is already destroyed before parse", async () => {
226
- const source = readableFromBuffer(new Uint8Array(0));
227
- source.destroy(new Error("upstream torn down"));
228
-
229
- let threw = false;
230
- try {
231
- for await (const _entry of parseVBundleStream(source)) {
232
- // unreachable
233
- }
234
- } catch {
235
- threw = true;
236
- }
237
-
238
- expect(threw).toBe(true);
239
- }, 1000);
240
- });