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