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