@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.
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/ces-client/src/http-credentials.ts +18 -1
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
- package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
- package/openapi.yaml +2 -708
- package/package.json +2 -3
- package/scripts/sync-web-search-catalog.ts +1 -2
- package/src/__tests__/cloud-profile-guard.test.ts +41 -5
- package/src/__tests__/config-schema.test.ts +6 -38
- package/src/__tests__/conversation-queue.test.ts +45 -6
- package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
- package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
- package/src/__tests__/persona-resolver.test.ts +0 -51
- package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
- package/src/__tests__/run-due-schedules.test.ts +0 -4
- package/src/__tests__/schedule-store.test.ts +0 -12
- package/src/__tests__/stt-stream-session.test.ts +4 -4
- package/src/__tests__/telephony-tts-guard.test.ts +2 -50
- package/src/__tests__/voice-config-update.test.ts +12 -167
- package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
- package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
- package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
- package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
- package/src/calls/resolve-call-tts-provider.ts +6 -25
- package/src/calls/telephony-credential-preflight.ts +0 -10
- package/src/calls/telephony-tts-capability.ts +1 -17
- package/src/cli/commands/tts.ts +1 -2
- package/src/cli/index.help.ts +0 -2
- package/src/cli/program.ts +0 -2
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
- package/src/config/schemas/__tests__/stt.test.ts +10 -14
- package/src/config/schemas/llm.ts +3 -5
- package/src/config/schemas/services.ts +5 -6
- package/src/config/schemas/stt.ts +1 -3
- package/src/config/schemas/tts.ts +1 -30
- package/src/daemon/daemon-readiness.ts +4 -29
- package/src/daemon/lifecycle.ts +0 -31
- package/src/daemon/providers-setup.ts +0 -8
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/heartbeat/heartbeat-service.ts +1 -66
- package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
- package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
- package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
- package/src/live-voice/live-voice-credential-preflight.ts +9 -33
- package/src/live-voice/live-voice-session.ts +10 -6
- package/src/live-voice/live-voice-tts.ts +1 -8
- package/src/plugins/defaults/memory/worker.ts +3 -3
- package/src/prompts/persona-resolver.ts +0 -35
- package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
- package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
- package/src/providers/search-provider-catalog.ts +7 -39
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
- package/src/providers/speech-to-text/provider-catalog.ts +2 -73
- package/src/providers/speech-to-text/resolve.ts +3 -120
- package/src/runtime/http-router-types.ts +1 -3
- package/src/runtime/http-server.ts +1 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +13 -11
- package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
- package/src/runtime/routes/conversation-query-routes.ts +0 -96
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
- package/src/runtime/routes/index.ts +0 -4
- package/src/runtime/routes/live-voice-routes.test.ts +4 -11
- package/src/runtime/routes/live-voice-routes.ts +8 -11
- package/src/runtime/routes/secret-routes.ts +0 -6
- package/src/runtime/routes/tts-routes.ts +0 -38
- package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
- package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
- package/src/schedule/schedule-store.ts +0 -7
- package/src/schedule/scheduler.ts +0 -5
- package/src/schedule/worker.ts +1 -1
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
- package/src/stt/daemon-batch-transcriber.ts +4 -38
- package/src/stt/types.ts +1 -9
- package/src/tools/network/__tests__/web-search.test.ts +3 -250
- package/src/tools/network/web-search.ts +4 -165
- package/src/tts/__tests__/provider-resolution.test.ts +0 -5
- package/src/tts/provider-catalog.ts +0 -4
- package/src/tts/types.ts +0 -1
- package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
- package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
- package/src/__tests__/migration-export-http.test.ts +0 -557
- package/src/__tests__/migration-export-streaming.test.ts +0 -350
- package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
- package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
- package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
- package/src/__tests__/migration-import-from-path.test.ts +0 -324
- package/src/__tests__/migration-import-from-url.test.ts +0 -588
- package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
- package/src/__tests__/migration-jobs-status.test.ts +0 -139
- package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
- package/src/__tests__/migration-transport.test.ts +0 -1089
- package/src/__tests__/migration-validate-http.test.ts +0 -634
- package/src/__tests__/migration-wizard.test.ts +0 -1395
- package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
- package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
- package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
- package/src/__tests__/validation-results-screen.test.ts +0 -1130
- package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
- package/src/background-wake/background-wake-routes.test.ts +0 -906
- package/src/background-wake/cloud-client.test.ts +0 -328
- package/src/background-wake/cloud-client.ts +0 -196
- package/src/background-wake/next-wake.test.ts +0 -335
- package/src/background-wake/next-wake.ts +0 -188
- package/src/background-wake/publisher.ts +0 -93
- package/src/background-wake/wake-intent-hooks.test.ts +0 -234
- package/src/cli/commands/__tests__/backup.test.ts +0 -363
- package/src/cli/commands/backup.help.ts +0 -180
- package/src/cli/commands/backup.ts +0 -398
- package/src/cloud/cloud-speech.test.ts +0 -335
- package/src/cloud/cloud-speech.ts +0 -300
- package/src/config/cloud-speech-defaults.ts +0 -300
- package/src/config/sanitize-for-transfer.ts +0 -111
- package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
- package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
- package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
- package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
- package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
- package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
- package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
- package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
- package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
- package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
- package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
- package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
- package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
- package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
- package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
- package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
- package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
- package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
- package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
- package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
- package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
- package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
- package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
- package/src/runtime/migrations/gcs-signed-url.ts +0 -190
- package/src/runtime/migrations/job-registry.ts +0 -297
- package/src/runtime/migrations/migration-transport.ts +0 -612
- package/src/runtime/migrations/migration-wizard.ts +0 -747
- package/src/runtime/migrations/origin-mode.ts +0 -42
- package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
- package/src/runtime/migrations/staged-import-path.ts +0 -116
- package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
- package/src/runtime/migrations/validation-results-screen.ts +0 -469
- package/src/runtime/migrations/vbundle-builder.ts +0 -1382
- package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
- package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
- package/src/runtime/migrations/vbundle-importer.ts +0 -826
- package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
- package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
- package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
- package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
- package/src/runtime/migrations/vbundle-validator.ts +0 -724
- package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
- package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
- package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
- package/src/runtime/routes/background-wake-routes.ts +0 -387
- package/src/runtime/routes/migration-routes.ts +0 -2584
- package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
- package/src/tools/network/cloud-search-proxy.ts +0 -211
- package/src/tts/__tests__/cadet-provider.test.ts +0 -253
- package/src/tts/providers/cadet-provider.ts +0 -244
- 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
|
-
});
|