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