@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,538 +0,0 @@
1
- /**
2
- * Symlink-handling coverage for the streaming `streamCommitImport`.
3
- *
4
- * Scenarios:
5
- * 1. Round-trip class 1: a typeflag-2 manifest entry is recreated as a real
6
- * symlink in the destination workspace. Reading through the link returns
7
- * its sibling regular file's bytes.
8
- * 2. Defense-in-depth: a hand-built bundle whose manifest declares an
9
- * absolute `link_target` is rejected at the streaming validator —
10
- * `streamCommitImport` returns `validation_failed` and nothing lands on
11
- * disk.
12
- * 3. Defense-in-depth: a hand-built bundle whose manifest declares a
13
- * `..`-traversal `link_target` is rejected the same way.
14
- * 4. Buffer/streaming parity: build ONE fixture bundle and run it through
15
- * both `commitImport` (workspace A) and `streamCommitImport`
16
- * (workspace B). Compare on-disk states recursively — symlinks must
17
- * survive both paths identically. Reports must agree on shape, action,
18
- * paths (modulo workspace prefix), size, sha, and backup_path.
19
- */
20
-
21
- import { createHash } from "node:crypto";
22
- import {
23
- lstatSync,
24
- mkdirSync,
25
- mkdtempSync,
26
- readdirSync,
27
- readFileSync,
28
- readlinkSync,
29
- realpathSync,
30
- rmSync,
31
- } from "node:fs";
32
- import { tmpdir } from "node:os";
33
- import { join, relative } from "node:path";
34
- import { Readable } from "node:stream";
35
- import { gzipSync } from "node:zlib";
36
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
37
-
38
- import { buildVBundle } from "../vbundle-builder.js";
39
- import { DefaultPathResolver } from "../vbundle-import-analyzer.js";
40
- import { commitImport } from "../vbundle-importer.js";
41
- import { streamCommitImport } from "../vbundle-streaming-importer.js";
42
- import { buildTestManifest, defaultV1Options } from "./v1-test-helpers.js";
43
-
44
- // ---------------------------------------------------------------------------
45
- // Fixture helpers
46
- // ---------------------------------------------------------------------------
47
-
48
- function freshWorkspaceDir(prefix: string): string {
49
- // Wrap with realpathSync so macOS /var → /private/var canonicalization
50
- // matches the importer's internal `resolve(workspaceDir)`.
51
- const parent = realpathSync(mkdtempSync(join(tmpdir(), prefix)));
52
- return join(parent, "workspace");
53
- }
54
-
55
- function readableFrom(buf: Uint8Array): Readable {
56
- return Readable.from([Buffer.from(buf)]);
57
- }
58
-
59
- function sha256Hex(input: string | Uint8Array): string {
60
- return createHash("sha256").update(input).digest("hex");
61
- }
62
-
63
- /**
64
- * Build a minimal gzip-compressed tar containing a manifest plus a
65
- * tar-typeflag-2 symlink entry whose linkname matches the manifest's
66
- * declared `link_target`. Used for the defense-in-depth tests where we need
67
- * the streaming validator to actually parse the bundle and reject the
68
- * manifest's absolute / traversal target — `buildVBundle` won't help because
69
- * its tar emit's 100-byte ustar limit on linkname rejects long paths up
70
- * front (and we want the validator to be the one rejecting).
71
- */
72
- function buildSymlinkOnlyBundle(input: {
73
- archivePath: string;
74
- linkTarget: string;
75
- }): Uint8Array {
76
- const dbBody = new TextEncoder().encode("db-bytes");
77
- const manifest = buildTestManifest({
78
- contents: [
79
- {
80
- // Required by the manifest schema cross-field refine.
81
- path: "workspace/data/db/assistant.db",
82
- sha256: sha256Hex(dbBody),
83
- size_bytes: dbBody.length,
84
- },
85
- {
86
- path: input.archivePath,
87
- sha256: sha256Hex(input.linkTarget),
88
- size_bytes: 0,
89
- link_target: input.linkTarget,
90
- },
91
- ],
92
- });
93
- const manifestJson = JSON.stringify(manifest);
94
- const manifestBytes = new TextEncoder().encode(manifestJson);
95
-
96
- const manifestEntry = makeTarEntry({
97
- name: "manifest.json",
98
- body: manifestBytes,
99
- typeflag: "0",
100
- });
101
- const dbEntry = makeTarEntry({
102
- name: "workspace/data/db/assistant.db",
103
- body: dbBody,
104
- typeflag: "0",
105
- });
106
- const symlinkEntry = makeTarEntry({
107
- name: input.archivePath,
108
- body: new Uint8Array(0),
109
- typeflag: "2",
110
- linkname: input.linkTarget,
111
- });
112
-
113
- const trailer = new Uint8Array(1024);
114
- const tar = new Uint8Array(
115
- manifestEntry.length +
116
- dbEntry.length +
117
- symlinkEntry.length +
118
- trailer.length,
119
- );
120
- tar.set(manifestEntry, 0);
121
- tar.set(dbEntry, manifestEntry.length);
122
- tar.set(symlinkEntry, manifestEntry.length + dbEntry.length);
123
- tar.set(trailer, manifestEntry.length + dbEntry.length + symlinkEntry.length);
124
- return gzipSync(tar);
125
- }
126
-
127
- function makeTarEntry(input: {
128
- name: string;
129
- body: Uint8Array;
130
- typeflag: string;
131
- linkname?: string;
132
- }): Uint8Array {
133
- const enc = new TextEncoder();
134
- const header = new Uint8Array(512);
135
-
136
- // Name (offset 0, 100 bytes).
137
- const nameBytes = enc.encode(input.name);
138
- header.set(nameBytes.subarray(0, Math.min(100, nameBytes.length)), 0);
139
-
140
- // Mode 0o644 (offset 100, 8 bytes, octal + null).
141
- header.set(enc.encode("0000644\0"), 100);
142
- // uid / gid (offset 108 / 116, 8 bytes each).
143
- header.set(enc.encode("0000000\0"), 108);
144
- header.set(enc.encode("0000000\0"), 116);
145
-
146
- // Size (offset 124, 12 bytes, octal + null).
147
- const size = input.body.length;
148
- const sizeStr = size.toString(8).padStart(11, "0") + "\0";
149
- header.set(enc.encode(sizeStr), 124);
150
-
151
- // Mtime (offset 136, 12 bytes).
152
- header.set(enc.encode("00000000000\0"), 136);
153
-
154
- // Checksum placeholder (offset 148, 8 bytes — fill with spaces for the sum).
155
- for (let i = 148; i < 156; i++) {
156
- header[i] = 0x20;
157
- }
158
-
159
- // Typeflag (offset 156, 1 byte).
160
- header[156] = input.typeflag.charCodeAt(0);
161
-
162
- // Linkname (offset 157, 100 bytes).
163
- if (input.linkname) {
164
- const linknameBytes = enc.encode(input.linkname);
165
- header.set(
166
- linknameBytes.subarray(0, Math.min(100, linknameBytes.length)),
167
- 157,
168
- );
169
- }
170
-
171
- // Magic + version "ustar\0" + "00" (offset 257, 8 bytes total).
172
- header.set(enc.encode("ustar\x0000"), 257);
173
-
174
- // Compute checksum: sum every byte of the header with the checksum field
175
- // treated as spaces (which we already initialized).
176
- let sum = 0;
177
- for (let i = 0; i < 512; i++) {
178
- sum += header[i];
179
- }
180
- const cksum = sum.toString(8).padStart(6, "0") + "\0 ";
181
- header.set(enc.encode(cksum), 148);
182
-
183
- // Pad body to 512-byte boundary.
184
- const padded = new Uint8Array(Math.ceil(size / 512) * 512);
185
- padded.set(input.body, 0);
186
-
187
- const out = new Uint8Array(header.length + padded.length);
188
- out.set(header, 0);
189
- out.set(padded, header.length);
190
- return out;
191
- }
192
-
193
- // ---------------------------------------------------------------------------
194
- // Workspace comparison helper (parity test)
195
- // ---------------------------------------------------------------------------
196
-
197
- /**
198
- * Recursively assert that two workspace dirs have the same structure and
199
- * content: every relative path appears in both with the same lstat type
200
- * (symlink/file/directory), same byte content for files, same readlink
201
- * target for symlinks.
202
- */
203
- function compareWorkspaces(wsA: string, wsB: string): void {
204
- const seenA = new Set<string>();
205
- walkRel(wsA, "", seenA);
206
- const seenB = new Set<string>();
207
- walkRel(wsB, "", seenB);
208
-
209
- // Set equality.
210
- expect([...seenA].sort()).toEqual([...seenB].sort());
211
-
212
- for (const rel of seenA) {
213
- const aPath = join(wsA, rel);
214
- const bPath = join(wsB, rel);
215
- const aStat = lstatSync(aPath);
216
- const bStat = lstatSync(bPath);
217
- expect(aStat.isSymbolicLink()).toBe(bStat.isSymbolicLink());
218
- expect(aStat.isFile()).toBe(bStat.isFile());
219
- expect(aStat.isDirectory()).toBe(bStat.isDirectory());
220
-
221
- if (aStat.isSymbolicLink()) {
222
- expect(readlinkSync(aPath)).toBe(readlinkSync(bPath));
223
- } else if (aStat.isFile()) {
224
- const aBytes = readFileSync(aPath);
225
- const bBytes = readFileSync(bPath);
226
- expect(aBytes.equals(bBytes)).toBe(true);
227
- }
228
- }
229
- }
230
-
231
- function walkRel(root: string, sub: string, into: Set<string>): void {
232
- const dir = sub ? join(root, sub) : root;
233
- let entries;
234
- try {
235
- entries = readdirSync(dir);
236
- } catch {
237
- return;
238
- }
239
- for (const name of entries) {
240
- // Skip streaming-importer scratch artifacts. The streaming swap creates
241
- // a `.pre-import-<ts-uuid>` backup dir inside the workspace and removes
242
- // it via fire-and-forget rm() after success — so the dir may still be
243
- // on disk when the test inspects the workspace. Buffer importer never
244
- // produces these.
245
- if (
246
- !sub &&
247
- (name.startsWith(".import-") ||
248
- name.startsWith(".pre-import-") ||
249
- name === ".import-marker.json")
250
- ) {
251
- continue;
252
- }
253
- const rel = sub ? join(sub, name) : name;
254
- into.add(rel);
255
- const full = join(dir, name);
256
- let st;
257
- try {
258
- st = lstatSync(full);
259
- } catch {
260
- continue;
261
- }
262
- if (st.isDirectory()) {
263
- walkRel(root, rel, into);
264
- }
265
- }
266
- }
267
-
268
- // ---------------------------------------------------------------------------
269
- // Tests
270
- // ---------------------------------------------------------------------------
271
-
272
- describe("streamCommitImport — symlinks", () => {
273
- let workspaceDir: string;
274
- beforeEach(() => {
275
- workspaceDir = freshWorkspaceDir("vbundle-stream-symlink-");
276
- });
277
- afterEach(() => {
278
- try {
279
- rmSync(join(workspaceDir, ".."), { recursive: true, force: true });
280
- } catch {
281
- // best-effort
282
- }
283
- });
284
-
285
- test("round-trip class 1: streams a typeflag-2 entry into a real symlink", async () => {
286
- const files = [
287
- {
288
- path: "workspace/data/db/assistant.db",
289
- data: new TextEncoder().encode("db-bytes"),
290
- },
291
- {
292
- path: "workspace/skills/bar.md",
293
- data: new TextEncoder().encode("hello bar"),
294
- },
295
- {
296
- path: "workspace/skills/foo.md",
297
- data: new Uint8Array(0),
298
- linkTarget: "bar.md",
299
- },
300
- ];
301
- const { archive } = buildVBundle({ files, ...defaultV1Options() });
302
-
303
- const result = await streamCommitImport({
304
- source: readableFrom(archive),
305
- pathResolver: new DefaultPathResolver(
306
- workspaceDir,
307
- undefined,
308
- () => null,
309
- ),
310
- workspaceDir,
311
- });
312
-
313
- expect(result.ok).toBe(true);
314
- if (!result.ok) {
315
- throw new Error("unreachable");
316
- }
317
-
318
- const fooPath = join(workspaceDir, "skills/foo.md");
319
- expect(lstatSync(fooPath).isSymbolicLink()).toBe(true);
320
- expect(readlinkSync(fooPath)).toBe("bar.md");
321
- expect(readFileSync(fooPath, "utf8")).toBe("hello bar");
322
-
323
- const fooReport = result.report.files.find(
324
- (f) => f.path === "workspace/skills/foo.md",
325
- );
326
- expect(fooReport).toBeDefined();
327
- expect(fooReport!.action).toBe("created");
328
- expect(fooReport!.size).toBe(0);
329
- expect(fooReport!.backup_path).toBeNull();
330
- expect(fooReport!.sha256).toBe(sha256Hex("bar.md"));
331
- });
332
-
333
- test("defense-in-depth: absolute link_target → validation_failed, nothing on disk", async () => {
334
- const archive = buildSymlinkOnlyBundle({
335
- archivePath: "workspace/skills/abs.md",
336
- linkTarget: "/etc/passwd",
337
- });
338
-
339
- const result = await streamCommitImport({
340
- source: readableFrom(archive),
341
- pathResolver: new DefaultPathResolver(
342
- workspaceDir,
343
- undefined,
344
- () => null,
345
- ),
346
- workspaceDir,
347
- });
348
-
349
- expect(result.ok).toBe(false);
350
- if (result.ok) {
351
- throw new Error("unreachable");
352
- }
353
- expect(result.reason).toBe("validation_failed");
354
- if (result.reason !== "validation_failed") {
355
- throw new Error("unreachable");
356
- }
357
- expect(
358
- result.errors.some((e) => e.code === "symlink_target_escapes_archive"),
359
- ).toBe(true);
360
-
361
- expect(() => lstatSync(join(workspaceDir, "skills/abs.md"))).toThrow();
362
- });
363
-
364
- test("legacy-format symlink entry survives streaming import (rename path)", async () => {
365
- // Regression: `promoteLegacyStagedFiles` previously fell back to
366
- // `copyFile` on EXDEV, which dereferences the source symlink and writes
367
- // the target's content as a regular file. We can't easily simulate
368
- // EXDEV in a unit test (it requires crossing real filesystems), but
369
- // exercising the rename path with a legacy-format symlink entry pins
370
- // the contract and proves the symlink survives the legacy promote.
371
- const files = [
372
- // Legacy `skills/*` entries (no `workspace/` prefix) route through
373
- // `legacyStaged` -> `promoteLegacyStagedFiles`. The `data/db/...`
374
- // entry (also legacy-format) satisfies the manifest schema's
375
- // cross-field refine and routes through the same legacy path.
376
- {
377
- path: "data/db/assistant.db",
378
- data: new TextEncoder().encode("db-bytes"),
379
- },
380
- {
381
- path: "skills/bar.md",
382
- data: new TextEncoder().encode("legacy bar"),
383
- },
384
- {
385
- path: "skills/foo.md",
386
- data: new Uint8Array(0),
387
- linkTarget: "bar.md",
388
- },
389
- ];
390
- const { archive } = buildVBundle({ files, ...defaultV1Options() });
391
-
392
- const result = await streamCommitImport({
393
- source: readableFrom(archive),
394
- pathResolver: new DefaultPathResolver(
395
- workspaceDir,
396
- undefined,
397
- () => null,
398
- ),
399
- workspaceDir,
400
- });
401
-
402
- expect(result.ok).toBe(true);
403
- if (!result.ok) {
404
- throw new Error("unreachable");
405
- }
406
-
407
- const fooPath = join(workspaceDir, "skills/foo.md");
408
- expect(lstatSync(fooPath).isSymbolicLink()).toBe(true);
409
- expect(readlinkSync(fooPath)).toBe("bar.md");
410
- expect(readFileSync(join(workspaceDir, "skills/bar.md"), "utf8")).toBe(
411
- "legacy bar",
412
- );
413
- });
414
-
415
- test("defense-in-depth: '..' traversal link_target → validation_failed", async () => {
416
- const archive = buildSymlinkOnlyBundle({
417
- archivePath: "workspace/skills/escape.md",
418
- linkTarget: "../../../tmp/escape",
419
- });
420
-
421
- const result = await streamCommitImport({
422
- source: readableFrom(archive),
423
- pathResolver: new DefaultPathResolver(
424
- workspaceDir,
425
- undefined,
426
- () => null,
427
- ),
428
- workspaceDir,
429
- });
430
-
431
- expect(result.ok).toBe(false);
432
- if (result.ok) {
433
- throw new Error("unreachable");
434
- }
435
- expect(result.reason).toBe("validation_failed");
436
- if (result.reason !== "validation_failed") {
437
- throw new Error("unreachable");
438
- }
439
- expect(
440
- result.errors.some((e) => e.code === "symlink_target_escapes_archive"),
441
- ).toBe(true);
442
-
443
- expect(() => lstatSync(join(workspaceDir, "skills/escape.md"))).toThrow();
444
- });
445
- });
446
-
447
- // ---------------------------------------------------------------------------
448
- // Buffer/streaming parity test (PRIMARY): same fixture, both importers,
449
- // identical on-disk state.
450
- // ---------------------------------------------------------------------------
451
-
452
- describe("streamCommitImport — buffer/streaming parity for symlinks", () => {
453
- let bufferWs: string;
454
- let streamWs: string;
455
- beforeEach(() => {
456
- bufferWs = freshWorkspaceDir("vbundle-parity-buffer-");
457
- streamWs = freshWorkspaceDir("vbundle-parity-stream-");
458
- });
459
- afterEach(() => {
460
- for (const ws of [bufferWs, streamWs]) {
461
- try {
462
- rmSync(join(ws, ".."), { recursive: true, force: true });
463
- } catch {
464
- // best-effort
465
- }
466
- }
467
- });
468
-
469
- test("commitImport and streamCommitImport produce identical workspaces for a symlink-bearing bundle", async () => {
470
- const files = [
471
- {
472
- path: "workspace/data/db/assistant.db",
473
- data: new TextEncoder().encode("db-bytes"),
474
- },
475
- {
476
- path: "workspace/skills/bar.md",
477
- data: new TextEncoder().encode("hello bar"),
478
- },
479
- {
480
- path: "workspace/skills/foo.md",
481
- data: new Uint8Array(0),
482
- linkTarget: "bar.md",
483
- },
484
- ];
485
- const { archive } = buildVBundle({ files, ...defaultV1Options() });
486
-
487
- // Buffer path.
488
- mkdirSync(bufferWs, { recursive: true });
489
- const bufferResult = commitImport({
490
- archiveData: archive,
491
- pathResolver: new DefaultPathResolver(bufferWs, undefined, () => null),
492
- workspaceDir: bufferWs,
493
- });
494
-
495
- // Streaming path. `streamCommitImport` creates the workspace dir itself
496
- // when it doesn't exist.
497
- const streamResult = await streamCommitImport({
498
- source: readableFrom(archive),
499
- pathResolver: new DefaultPathResolver(streamWs, undefined, () => null),
500
- workspaceDir: streamWs,
501
- });
502
-
503
- expect(bufferResult.ok).toBe(true);
504
- expect(streamResult.ok).toBe(true);
505
- if (!bufferResult.ok || !streamResult.ok) {
506
- throw new Error("unreachable");
507
- }
508
-
509
- // On-disk parity: every path, type, content, and link target must match.
510
- compareWorkspaces(bufferWs, streamWs);
511
-
512
- // Symlink survived under both.
513
- const bufferLink = join(bufferWs, "skills/foo.md");
514
- const streamLink = join(streamWs, "skills/foo.md");
515
- expect(lstatSync(bufferLink).isSymbolicLink()).toBe(true);
516
- expect(lstatSync(streamLink).isSymbolicLink()).toBe(true);
517
- expect(readlinkSync(bufferLink)).toBe(readlinkSync(streamLink));
518
- expect(readlinkSync(streamLink)).toBe("bar.md");
519
-
520
- // Report parity: per-entry symlink metadata must agree, modulo the
521
- // workspace-dir prefix (which differs between bufferWs and streamWs).
522
- const bufferEntry = bufferResult.report.files.find(
523
- (f) => f.path === "workspace/skills/foo.md",
524
- );
525
- const streamEntry = streamResult.report.files.find(
526
- (f) => f.path === "workspace/skills/foo.md",
527
- );
528
- expect(bufferEntry).toBeDefined();
529
- expect(streamEntry).toBeDefined();
530
- expect(streamEntry!.action).toBe(bufferEntry!.action);
531
- expect(streamEntry!.size).toBe(bufferEntry!.size);
532
- expect(streamEntry!.sha256).toBe(bufferEntry!.sha256);
533
- expect(streamEntry!.backup_path).toBe(bufferEntry!.backup_path);
534
- expect(relative(bufferWs, bufferEntry!.disk_path)).toBe(
535
- relative(streamWs, streamEntry!.disk_path),
536
- );
537
- });
538
- });