@openparachute/vault 0.6.5-rc.2 → 0.6.5
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/.parachute/module.json +1 -0
- package/core/src/core.test.ts +7 -7
- package/core/src/do-param-cap.test.ts +161 -0
- package/core/src/links.ts +8 -13
- package/core/src/mcp.ts +1 -1
- package/core/src/notes.ts +33 -15
- package/core/src/onboarding.ts +14 -296
- package/core/src/schema.ts +5 -5
- package/core/src/seed-packs.test.ts +357 -0
- package/core/src/seed-packs.ts +823 -0
- package/core/src/sql-in.test.ts +58 -0
- package/core/src/sql-in.ts +76 -0
- package/core/src/tag-hierarchy.ts +1 -1
- package/core/src/tag-schemas.ts +2 -2
- package/core/src/transcription/provider.ts +141 -0
- package/core/src/types.ts +1 -1
- package/core/src/vault-projection.ts +1 -1
- package/core/src/wikilinks.ts +10 -4
- package/package.json +1 -1
- package/src/add-pack.test.ts +142 -0
- package/src/admin-spa.ts +2 -1
- package/src/auth.ts +1 -1
- package/src/cli.ts +806 -7
- package/src/export-watch.ts +1 -1
- package/src/live-frame-parity.test.ts +201 -0
- package/src/module-manifest.ts +8 -0
- package/src/onboarding-seed.test.ts +188 -40
- package/src/onboarding-seed.ts +41 -46
- package/src/routes.ts +20 -3
- package/src/routing.test.ts +2 -2
- package/src/routing.ts +18 -6
- package/src/self-register.test.ts +19 -0
- package/src/self-register.ts +6 -1
- package/src/server.ts +133 -31
- package/src/services-manifest.ts +8 -0
- package/src/subscriptions.ts +100 -42
- package/src/tag-scope.ts +3 -3
- package/src/test-support/live-frame-corpus.ts +72 -0
- package/src/token-store.ts +2 -2
- package/src/transcription/build.test.ts +305 -0
- package/src/transcription/build.ts +332 -0
- package/src/transcription/capability.test.ts +118 -0
- package/src/transcription/capability.ts +96 -0
- package/src/transcription/download.test.ts +101 -0
- package/src/transcription/download.ts +71 -0
- package/src/transcription/install-python.test.ts +366 -0
- package/src/transcription/install-python.ts +471 -0
- package/src/transcription/install.test.ts +167 -0
- package/src/transcription/install.ts +296 -0
- package/src/transcription/providers/onnx-asr.test.ts +229 -0
- package/src/transcription/providers/onnx-asr.ts +239 -0
- package/src/transcription/providers/parakeet-mlx.test.ts +290 -0
- package/src/transcription/providers/parakeet-mlx.ts +242 -0
- package/src/transcription/providers/scribe-http.test.ts +195 -0
- package/src/transcription/providers/scribe-http.ts +144 -0
- package/src/transcription/providers/transcribe-cpp.test.ts +314 -0
- package/src/transcription/providers/transcribe-cpp.ts +293 -0
- package/src/transcription/select.test.ts +299 -0
- package/src/transcription/select.ts +397 -0
- package/src/transcription/tiers.test.ts +197 -0
- package/src/transcription/tiers.ts +184 -0
- package/src/transcription-worker.test.ts +44 -0
- package/src/transcription-worker.ts +57 -122
- package/src/vault-create.test.ts +43 -10
- package/src/vault.test.ts +49 -1
- package/src/ws-server.ts +408 -0
- package/src/ws-subscribe.test.ts +474 -0
- package/src/ws-subscribe.ts +242 -0
- package/src/subscribe.test.ts +0 -609
- package/src/subscribe.ts +0 -248
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming file download for `transcription install` (tarball, GGUF models,
|
|
3
|
+
* CLI sources).
|
|
4
|
+
*
|
|
5
|
+
* ## Why a manual pump instead of `Bun.write(dest, resp)` (vault#534 blocker 1)
|
|
6
|
+
*
|
|
7
|
+
* `Bun.write(dest, response)` HANGS FOREVER on Linux when streaming a large
|
|
8
|
+
* Response body to disk — zero bytes land, no error, no timeout. Reproduced
|
|
9
|
+
* deterministically in real Linux containers on BOTH arches (aarch64 native +
|
|
10
|
+
* x86_64 emulated) across bun 1.2.23 / 1.3.13 / 1.3.14, while the identical
|
|
11
|
+
* code works on macOS. In the same environment `curl` pulls the same URL at
|
|
12
|
+
* full speed and `await resp.arrayBuffer()` gets all 26MB in ~2s — the bug is
|
|
13
|
+
* specifically Bun's Response→file streaming fast path. Manually pumping
|
|
14
|
+
* `resp.body` chunks into a `Bun.file(dest).writer()` moves the same 26MB in
|
|
15
|
+
* <1s and works on both platforms, so that's what we do everywhere (no
|
|
16
|
+
* platform gate). See vault#534 for the full container verification.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { rmSync } from "fs";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Download `url` to `dest` (follows redirects). Streams chunk-by-chunk — never
|
|
23
|
+
* buffers the whole body (models run up to ~660MB). On any mid-stream failure
|
|
24
|
+
* the partial file is removed so a retry never trusts a truncated artifact.
|
|
25
|
+
* When the server sent an honest `content-length` (no content-encoding
|
|
26
|
+
* transform), a byte-count mismatch is treated as a failed download too.
|
|
27
|
+
*/
|
|
28
|
+
export async function downloadTo(url: string, dest: string): Promise<void> {
|
|
29
|
+
const resp = await fetch(url, { redirect: "follow" });
|
|
30
|
+
if (!resp.ok) throw new Error(`download failed (${resp.status}) for ${url}`);
|
|
31
|
+
if (!resp.body) throw new Error(`download failed (empty response body) for ${url}`);
|
|
32
|
+
|
|
33
|
+
// content-length is only the on-the-wire byte count when no transfer
|
|
34
|
+
// decompression happened; fetch auto-decodes content-encoding'd bodies, so
|
|
35
|
+
// only enforce the size check for identity responses (the normal case for
|
|
36
|
+
// release tarballs + GGUFs — both already-compressed binary).
|
|
37
|
+
const encoding = resp.headers.get("content-encoding");
|
|
38
|
+
const lengthHeader = resp.headers.get("content-length");
|
|
39
|
+
const expectedBytes =
|
|
40
|
+
(!encoding || encoding === "identity") && lengthHeader && /^\d+$/.test(lengthHeader)
|
|
41
|
+
? Number(lengthHeader)
|
|
42
|
+
: null;
|
|
43
|
+
|
|
44
|
+
const sink = Bun.file(dest).writer();
|
|
45
|
+
let written = 0;
|
|
46
|
+
try {
|
|
47
|
+
for await (const chunk of resp.body) {
|
|
48
|
+
sink.write(chunk);
|
|
49
|
+
written += chunk.byteLength;
|
|
50
|
+
}
|
|
51
|
+
await sink.end();
|
|
52
|
+
} catch (err) {
|
|
53
|
+
// Flush/close what we can, then remove the partial file — a half-written
|
|
54
|
+
// tarball/model must never survive to be mistaken for a good artifact.
|
|
55
|
+
try {
|
|
56
|
+
await sink.end();
|
|
57
|
+
} catch {
|
|
58
|
+
// best-effort close; the rm below is what matters
|
|
59
|
+
}
|
|
60
|
+
rmSync(dest, { force: true });
|
|
61
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
62
|
+
throw new Error(`download of ${url} failed after ${written} bytes: ${msg}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (expectedBytes !== null && written !== expectedBytes) {
|
|
66
|
+
rmSync(dest, { force: true });
|
|
67
|
+
throw new Error(
|
|
68
|
+
`download of ${url} was truncated: got ${written} bytes, expected ${expectedBytes} (content-length)`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import {
|
|
4
|
+
PYTHON_PROVIDERS,
|
|
5
|
+
installPythonBackend,
|
|
6
|
+
type PythonInstallDeps,
|
|
7
|
+
type PythonProviderName,
|
|
8
|
+
type RunResult,
|
|
9
|
+
} from "./install-python.ts";
|
|
10
|
+
import { pythonVenvDir } from "./select.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Install-routine tests for the python providers (scribe-fold Phase 2b) —
|
|
14
|
+
* every subprocess/platform probe goes through the injected deps, so the tier
|
|
15
|
+
* guards, apt/venv logic, warm-pull, and idempotency are exercised WITHOUT
|
|
16
|
+
* installing anything. Ported discipline from scribe's install-backend tests.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const GB = 2 ** 30;
|
|
20
|
+
const HOME = "/test-home";
|
|
21
|
+
|
|
22
|
+
type DepsOverrides = Partial<Omit<PythonInstallDeps, "run" | "which" | "existsImpl">> & {
|
|
23
|
+
run?: (cmd: string[]) => Promise<RunResult>;
|
|
24
|
+
/** Binaries "on PATH". */
|
|
25
|
+
path?: Record<string, string>;
|
|
26
|
+
/** Files that "exist" on disk. */
|
|
27
|
+
files?: Set<string>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function makeDeps(over: DepsOverrides = {}): { deps: PythonInstallDeps; runs: string[][]; logs: string[] } {
|
|
31
|
+
const runs: string[][] = [];
|
|
32
|
+
const logs: string[] = [];
|
|
33
|
+
const files = over.files ?? new Set<string>();
|
|
34
|
+
const path = over.path ?? {};
|
|
35
|
+
const deps: PythonInstallDeps = {
|
|
36
|
+
run: async (cmd) => {
|
|
37
|
+
runs.push(cmd);
|
|
38
|
+
if (over.run) return over.run(cmd);
|
|
39
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
40
|
+
},
|
|
41
|
+
which: (bin) => path[bin] ?? null,
|
|
42
|
+
platform: over.platform ?? "linux",
|
|
43
|
+
arch: over.arch ?? "x64",
|
|
44
|
+
totalRamBytes: over.totalRamBytes ?? 8 * GB,
|
|
45
|
+
uid: over.uid ?? (() => 1000),
|
|
46
|
+
env: over.env ?? { PARACHUTE_HOME: HOME },
|
|
47
|
+
existsImpl: (p) => files.has(p),
|
|
48
|
+
log: (line) => logs.push(line),
|
|
49
|
+
};
|
|
50
|
+
return { deps, runs, logs };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const venvBin = (bin: string) => join(pythonVenvDir({ PARACHUTE_HOME: HOME }), "bin", bin);
|
|
54
|
+
|
|
55
|
+
/** Deps for a happy-path Linux onnx-asr install: python3+ffmpeg present, and
|
|
56
|
+
* the venv binary "appears" after the pip run. */
|
|
57
|
+
function linuxHappyDeps(provider: PythonProviderName = "onnx-asr") {
|
|
58
|
+
const spec = PYTHON_PROVIDERS[provider];
|
|
59
|
+
const files = new Set<string>();
|
|
60
|
+
const { deps, runs, logs } = makeDeps({
|
|
61
|
+
path: { python3: "/usr/bin/python3", ffmpeg: "/usr/bin/ffmpeg", sudo: "/usr/bin/sudo" },
|
|
62
|
+
files,
|
|
63
|
+
run: async (cmd) => {
|
|
64
|
+
// pip install → the venv binary materializes.
|
|
65
|
+
if (cmd.some((c) => c.endsWith("/pip"))) files.add(venvBin(spec.bin));
|
|
66
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
return { deps, runs, logs, files };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Platform / arch gates (never bypassed)
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
describe("installPythonBackend — platform gates", () => {
|
|
77
|
+
test("parakeet-mlx on Linux → refused (darwin-only), even with --force", async () => {
|
|
78
|
+
const { deps, runs } = makeDeps({ platform: "linux" });
|
|
79
|
+
const out = await installPythonBackend(deps, { provider: "parakeet-mlx", force: true });
|
|
80
|
+
expect(out.ok).toBe(false);
|
|
81
|
+
expect(out.steps[0]!.status).toBe("refused");
|
|
82
|
+
expect(out.summary).toContain("darwin");
|
|
83
|
+
expect(runs.length).toBe(0); // nothing ran
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("parakeet-mlx on Intel mac (darwin/x64) → refused (Apple Silicon only)", async () => {
|
|
87
|
+
const { deps } = makeDeps({ platform: "darwin", arch: "x64", totalRamBytes: 32 * GB });
|
|
88
|
+
const out = await installPythonBackend(deps, { provider: "parakeet-mlx", force: true });
|
|
89
|
+
expect(out.ok).toBe(false);
|
|
90
|
+
expect(out.summary).toContain("arm64");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("onnx-asr has no platform gate (installable on macOS too)", async () => {
|
|
94
|
+
const files = new Set<string>([venvBin("onnx-asr")]); // already installed → skips pip
|
|
95
|
+
const { deps } = makeDeps({
|
|
96
|
+
platform: "darwin",
|
|
97
|
+
arch: "arm64",
|
|
98
|
+
files,
|
|
99
|
+
path: { python3: "/usr/bin/python3", ffmpeg: "/opt/ffmpeg" },
|
|
100
|
+
});
|
|
101
|
+
const out = await installPythonBackend(deps, { provider: "onnx-asr", skipModel: true });
|
|
102
|
+
expect(out.ok).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// RAM floors (the ratified tiers; --force bypasses)
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
describe("installPythonBackend — RAM floors", () => {
|
|
111
|
+
test("onnx-asr below 4GB → refused with the scribe#82 rationale", async () => {
|
|
112
|
+
const { deps } = makeDeps({ totalRamBytes: 2 * GB });
|
|
113
|
+
const out = await installPythonBackend(deps, { provider: "onnx-asr" });
|
|
114
|
+
expect(out.ok).toBe(false);
|
|
115
|
+
const ram = out.steps.find((s) => s.name === "ram-guard")!;
|
|
116
|
+
expect(ram.status).toBe("refused");
|
|
117
|
+
expect(ram.detail).toContain("scribe#82");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("parakeet-mlx below 8GB → refused, steers to transcribe-cpp", async () => {
|
|
121
|
+
const { deps } = makeDeps({ platform: "darwin", arch: "arm64", totalRamBytes: 4 * GB });
|
|
122
|
+
const out = await installPythonBackend(deps, { provider: "parakeet-mlx" });
|
|
123
|
+
expect(out.ok).toBe(false);
|
|
124
|
+
expect(out.summary).toContain("transcribe-cpp");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("nominal-4GB box reporting 3.8GB → passes the onnx-asr floor (slack)", async () => {
|
|
128
|
+
const { deps } = linuxHappyDeps();
|
|
129
|
+
deps.totalRamBytes = 3.8 * GB;
|
|
130
|
+
const out = await installPythonBackend(deps, { provider: "onnx-asr", skipModel: true });
|
|
131
|
+
expect(out.steps.find((s) => s.name === "ram-guard")!.status).toBe("ok");
|
|
132
|
+
expect(out.ok).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("--force bypasses the RAM floor with a loud warning step", async () => {
|
|
136
|
+
const { deps, files } = linuxHappyDeps();
|
|
137
|
+
deps.totalRamBytes = 2 * GB;
|
|
138
|
+
void files;
|
|
139
|
+
const out = await installPythonBackend(deps, { provider: "onnx-asr", force: true, skipModel: true });
|
|
140
|
+
expect(out.ok).toBe(true);
|
|
141
|
+
const ram = out.steps.find((s) => s.name === "ram-guard")!;
|
|
142
|
+
expect(ram.status).toBe("skipped");
|
|
143
|
+
expect(ram.detail).toContain("--force");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// System deps (apt on Linux; instruct on macOS)
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
describe("installPythonBackend — system deps", () => {
|
|
152
|
+
test("Linux with python3+ffmpeg present → apt skipped entirely", async () => {
|
|
153
|
+
const { deps, runs } = linuxHappyDeps();
|
|
154
|
+
const out = await installPythonBackend(deps, { provider: "onnx-asr", skipModel: true });
|
|
155
|
+
expect(out.steps.find((s) => s.name === "system-deps")!.status).toBe("skipped");
|
|
156
|
+
expect(runs.some((r) => r.includes("apt-get"))).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("Linux missing ffmpeg, non-root with sudo → sudo apt-get install", async () => {
|
|
160
|
+
const files = new Set<string>();
|
|
161
|
+
const { deps, runs } = makeDeps({
|
|
162
|
+
path: { python3: "/usr/bin/python3", sudo: "/usr/bin/sudo", "apt-get": "/usr/bin/apt-get" },
|
|
163
|
+
files,
|
|
164
|
+
run: async (cmd) => {
|
|
165
|
+
if (cmd.some((c) => c.endsWith("/pip"))) files.add(venvBin("onnx-asr"));
|
|
166
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
const out = await installPythonBackend(deps, { provider: "onnx-asr", skipModel: true });
|
|
170
|
+
expect(out.ok).toBe(true);
|
|
171
|
+
const aptInstall = runs.find((r) => r.includes("install") && r.includes("apt-get"))!;
|
|
172
|
+
expect(aptInstall[0]).toBe("sudo");
|
|
173
|
+
expect(aptInstall).toContain("ffmpeg");
|
|
174
|
+
expect(aptInstall).toContain("python3-venv");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("Linux as root → apt without sudo", async () => {
|
|
178
|
+
const files = new Set<string>();
|
|
179
|
+
const { deps, runs } = makeDeps({
|
|
180
|
+
uid: () => 0,
|
|
181
|
+
path: { python3: "/usr/bin/python3", "apt-get": "/usr/bin/apt-get" },
|
|
182
|
+
files,
|
|
183
|
+
run: async (cmd) => {
|
|
184
|
+
if (cmd.some((c) => c.endsWith("/pip"))) files.add(venvBin("onnx-asr"));
|
|
185
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
await installPythonBackend(deps, { provider: "onnx-asr", skipModel: true });
|
|
189
|
+
const aptInstall = runs.find((r) => r.includes("install") && r.includes("apt-get"))!;
|
|
190
|
+
expect(aptInstall[0]).toBe("apt-get");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("Linux, deps missing, no root and no sudo → failed step with the exact command to run", async () => {
|
|
194
|
+
const { deps } = makeDeps({ path: { "apt-get": "/usr/bin/apt-get" } });
|
|
195
|
+
const out = await installPythonBackend(deps, { provider: "onnx-asr" });
|
|
196
|
+
expect(out.ok).toBe(false);
|
|
197
|
+
expect(out.summary).toContain("sudo apt-get install");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("a transient apt-get update failure does NOT abort (install still runs)", async () => {
|
|
201
|
+
const files = new Set<string>();
|
|
202
|
+
const { deps } = makeDeps({
|
|
203
|
+
path: { python3: "/usr/bin/python3", sudo: "/usr/bin/sudo", "apt-get": "/usr/bin/apt-get" },
|
|
204
|
+
files,
|
|
205
|
+
run: async (cmd) => {
|
|
206
|
+
if (cmd.includes("update")) return { exitCode: 100, stdout: "", stderr: "mirror down" };
|
|
207
|
+
if (cmd.some((c) => c.endsWith("/pip"))) files.add(venvBin("onnx-asr"));
|
|
208
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
const out = await installPythonBackend(deps, { provider: "onnx-asr", skipModel: true });
|
|
212
|
+
expect(out.ok).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("macOS missing ffmpeg → instruct (skipped step naming brew), never drives brew", async () => {
|
|
216
|
+
const files = new Set<string>([venvBin("parakeet-mlx")]);
|
|
217
|
+
const { deps, runs } = makeDeps({
|
|
218
|
+
platform: "darwin",
|
|
219
|
+
arch: "arm64",
|
|
220
|
+
totalRamBytes: 16 * GB,
|
|
221
|
+
path: { python3: "/usr/bin/python3" },
|
|
222
|
+
files,
|
|
223
|
+
});
|
|
224
|
+
const out = await installPythonBackend(deps, { provider: "parakeet-mlx" });
|
|
225
|
+
const sys = out.steps.find((s) => s.name === "system-deps")!;
|
|
226
|
+
expect(sys.status).toBe("skipped");
|
|
227
|
+
expect(sys.detail).toContain("brew install ffmpeg");
|
|
228
|
+
expect(runs.some((r) => r[0] === "brew")).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("macOS missing python3 entirely → failed with an actionable fix", async () => {
|
|
232
|
+
const { deps } = makeDeps({ platform: "darwin", arch: "arm64", totalRamBytes: 16 * GB });
|
|
233
|
+
const out = await installPythonBackend(deps, { provider: "parakeet-mlx" });
|
|
234
|
+
expect(out.ok).toBe(false);
|
|
235
|
+
expect(out.summary).toContain("python3");
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// venv + pip + idempotency
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
describe("installPythonBackend — venv package install", () => {
|
|
244
|
+
test("happy path: venv created under PARACHUTE_HOME, pip installs the extras target", async () => {
|
|
245
|
+
const { deps, runs } = linuxHappyDeps();
|
|
246
|
+
const out = await installPythonBackend(deps, { provider: "onnx-asr", skipModel: true });
|
|
247
|
+
expect(out.ok).toBe(true);
|
|
248
|
+
expect(out.binPath).toBe(venvBin("onnx-asr"));
|
|
249
|
+
expect(out.venv).toBe(pythonVenvDir({ PARACHUTE_HOME: HOME }));
|
|
250
|
+
const mkVenv = runs.find((r) => r[0] === "python3" && r.includes("venv"))!;
|
|
251
|
+
expect(mkVenv[3]).toBe(pythonVenvDir({ PARACHUTE_HOME: HOME }));
|
|
252
|
+
const pip = runs.find((r) => r[0]?.endsWith("/pip"))!;
|
|
253
|
+
expect(pip).toContain("onnx-asr[cpu,hub]");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("idempotent: an already-runnable binary skips venv+pip entirely", async () => {
|
|
257
|
+
const files = new Set<string>([venvBin("onnx-asr")]);
|
|
258
|
+
const { deps, runs } = makeDeps({
|
|
259
|
+
path: { python3: "/usr/bin/python3", ffmpeg: "/usr/bin/ffmpeg" },
|
|
260
|
+
files,
|
|
261
|
+
});
|
|
262
|
+
const out = await installPythonBackend(deps, { provider: "onnx-asr", skipModel: true });
|
|
263
|
+
expect(out.ok).toBe(true);
|
|
264
|
+
expect(out.steps.find((s) => s.name === "package")!.status).toBe("skipped");
|
|
265
|
+
expect(runs.some((r) => r[0] === "python3")).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("an operator's own PATH install counts as runnable (no venv forced)", async () => {
|
|
269
|
+
const files = new Set<string>(["/usr/local/bin/parakeet-mlx"]);
|
|
270
|
+
const { deps } = makeDeps({
|
|
271
|
+
platform: "darwin",
|
|
272
|
+
arch: "arm64",
|
|
273
|
+
totalRamBytes: 16 * GB,
|
|
274
|
+
path: { python3: "/usr/bin/python3", ffmpeg: "/opt/ffmpeg", "parakeet-mlx": "/usr/local/bin/parakeet-mlx" },
|
|
275
|
+
files,
|
|
276
|
+
});
|
|
277
|
+
const out = await installPythonBackend(deps, { provider: "parakeet-mlx" });
|
|
278
|
+
expect(out.ok).toBe(true);
|
|
279
|
+
expect(out.binPath).toBe("/usr/local/bin/parakeet-mlx");
|
|
280
|
+
expect(out.venv).toBe(""); // not vault's venv
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("pip failure → failed step, non-fatal outcome (no throw)", async () => {
|
|
284
|
+
const { deps } = makeDeps({
|
|
285
|
+
path: { python3: "/usr/bin/python3", ffmpeg: "/usr/bin/ffmpeg" },
|
|
286
|
+
run: async (cmd) =>
|
|
287
|
+
cmd[0]?.endsWith("/pip")
|
|
288
|
+
? { exitCode: 1, stdout: "", stderr: "no matching distribution" }
|
|
289
|
+
: { exitCode: 0, stdout: "", stderr: "" },
|
|
290
|
+
});
|
|
291
|
+
const out = await installPythonBackend(deps, { provider: "onnx-asr" });
|
|
292
|
+
expect(out.ok).toBe(false);
|
|
293
|
+
expect(out.summary).toContain("pip install onnx-asr[cpu,hub] failed");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("venv creation failure names python3-venv", async () => {
|
|
297
|
+
const { deps } = makeDeps({
|
|
298
|
+
path: { python3: "/usr/bin/python3", ffmpeg: "/usr/bin/ffmpeg" },
|
|
299
|
+
run: async (cmd) =>
|
|
300
|
+
cmd[0] === "python3"
|
|
301
|
+
? { exitCode: 1, stdout: "", stderr: "ensurepip missing" }
|
|
302
|
+
: { exitCode: 0, stdout: "", stderr: "" },
|
|
303
|
+
});
|
|
304
|
+
const out = await installPythonBackend(deps, { provider: "onnx-asr" });
|
|
305
|
+
expect(out.ok).toBe(false);
|
|
306
|
+
expect(out.summary).toContain("python3-venv");
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// Model warm-pull (best-effort) + verify
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
describe("installPythonBackend — warm-pull + verify", () => {
|
|
315
|
+
test("onnx-asr warm-pulls the ratified model via `<bin> <model> --help`", async () => {
|
|
316
|
+
const { deps, runs } = linuxHappyDeps();
|
|
317
|
+
const out = await installPythonBackend(deps, { provider: "onnx-asr" });
|
|
318
|
+
expect(out.ok).toBe(true);
|
|
319
|
+
const pull = runs.find((r) => r.includes("--help"))!;
|
|
320
|
+
expect(pull[0]).toBe(venvBin("onnx-asr"));
|
|
321
|
+
expect(pull[1]).toBe("nemo-parakeet-tdt-0.6b-v3");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("a warm-pull failure is non-fatal (model lazy-loads on first transcription)", async () => {
|
|
325
|
+
const files = new Set<string>();
|
|
326
|
+
const { deps } = makeDeps({
|
|
327
|
+
path: { python3: "/usr/bin/python3", ffmpeg: "/usr/bin/ffmpeg" },
|
|
328
|
+
files,
|
|
329
|
+
run: async (cmd) => {
|
|
330
|
+
if (cmd.some((c) => c.endsWith("/pip"))) files.add(venvBin("onnx-asr"));
|
|
331
|
+
if (cmd.includes("--help")) return { exitCode: 1, stdout: "", stderr: "offline" };
|
|
332
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
const out = await installPythonBackend(deps, { provider: "onnx-asr" });
|
|
336
|
+
expect(out.ok).toBe(true);
|
|
337
|
+
expect(out.steps.find((s) => s.name === "model-warm-pull")!.status).toBe("skipped");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("parakeet-mlx never warm-pulls (model downloads on first use)", async () => {
|
|
341
|
+
const files = new Set<string>([venvBin("parakeet-mlx")]);
|
|
342
|
+
const { deps, runs } = makeDeps({
|
|
343
|
+
platform: "darwin",
|
|
344
|
+
arch: "arm64",
|
|
345
|
+
totalRamBytes: 16 * GB,
|
|
346
|
+
path: { python3: "/usr/bin/python3", ffmpeg: "/opt/ffmpeg" },
|
|
347
|
+
files,
|
|
348
|
+
});
|
|
349
|
+
const out = await installPythonBackend(deps, { provider: "parakeet-mlx" });
|
|
350
|
+
expect(out.ok).toBe(true);
|
|
351
|
+
expect(out.steps.find((s) => s.name === "model-warm-pull")!.status).toBe("skipped");
|
|
352
|
+
expect(runs.some((r) => r.includes("--help"))).toBe(false);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("HONEST verify: pip 'succeeds' but no binary appears → ok:false, no activation signal", async () => {
|
|
356
|
+
// The honest-install rule (PRs #532/#533): never report ok without a
|
|
357
|
+
// runnable binary — the CLI only flips TRANSCRIPTION_PROVIDER on ok.
|
|
358
|
+
const { deps } = makeDeps({
|
|
359
|
+
path: { python3: "/usr/bin/python3", ffmpeg: "/usr/bin/ffmpeg" },
|
|
360
|
+
});
|
|
361
|
+
const out = await installPythonBackend(deps, { provider: "onnx-asr", skipModel: true });
|
|
362
|
+
expect(out.ok).toBe(false);
|
|
363
|
+
expect(out.binPath).toBeUndefined();
|
|
364
|
+
expect(out.steps.find((s) => s.name === "verify")!.status).toBe("failed");
|
|
365
|
+
});
|
|
366
|
+
});
|