@openparachute/vault 0.6.4 → 0.6.5-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/core/src/__fixtures__/golden-vault.ts +125 -0
  2. package/core/src/__fixtures__/portable-export-golden.json +10 -0
  3. package/core/src/do-param-cap.test.ts +161 -0
  4. package/core/src/links.ts +8 -13
  5. package/core/src/mcp.ts +306 -314
  6. package/core/src/notes.ts +54 -62
  7. package/core/src/onboarding.ts +14 -296
  8. package/core/src/portable-md-batching.test.ts +67 -0
  9. package/core/src/portable-md-golden.test.ts +55 -0
  10. package/core/src/portable-md.ts +579 -435
  11. package/core/src/schema.ts +21 -43
  12. package/core/src/seed-packs.test.ts +191 -0
  13. package/core/src/seed-packs.ts +559 -0
  14. package/core/src/sql-in.test.ts +58 -0
  15. package/core/src/sql-in.ts +76 -0
  16. package/core/src/store.ts +14 -9
  17. package/core/src/transcription/provider.ts +141 -0
  18. package/core/src/txn.test.ts +229 -0
  19. package/core/src/txn.ts +105 -0
  20. package/core/src/types.ts +9 -0
  21. package/core/src/vault-projection.ts +1 -1
  22. package/core/src/wikilinks.ts +10 -4
  23. package/package.json +1 -1
  24. package/src/add-pack.test.ts +142 -0
  25. package/src/cli.ts +778 -7
  26. package/src/onboarding-seed.test.ts +126 -40
  27. package/src/onboarding-seed.ts +41 -46
  28. package/src/routes.ts +25 -7
  29. package/src/server.ts +108 -31
  30. package/src/transcription/build.test.ts +224 -0
  31. package/src/transcription/build.ts +252 -0
  32. package/src/transcription/capability.test.ts +118 -0
  33. package/src/transcription/capability.ts +96 -0
  34. package/src/transcription/install-python.test.ts +366 -0
  35. package/src/transcription/install-python.ts +471 -0
  36. package/src/transcription/install.test.ts +167 -0
  37. package/src/transcription/install.ts +296 -0
  38. package/src/transcription/providers/onnx-asr.test.ts +229 -0
  39. package/src/transcription/providers/onnx-asr.ts +239 -0
  40. package/src/transcription/providers/parakeet-mlx.test.ts +290 -0
  41. package/src/transcription/providers/parakeet-mlx.ts +242 -0
  42. package/src/transcription/providers/scribe-http.test.ts +195 -0
  43. package/src/transcription/providers/scribe-http.ts +144 -0
  44. package/src/transcription/providers/transcribe-cpp.test.ts +314 -0
  45. package/src/transcription/providers/transcribe-cpp.ts +293 -0
  46. package/src/transcription/select.test.ts +259 -0
  47. package/src/transcription/select.ts +334 -0
  48. package/src/transcription/tiers.test.ts +197 -0
  49. package/src/transcription/tiers.ts +184 -0
  50. package/src/transcription-worker.test.ts +44 -0
  51. package/src/transcription-worker.ts +57 -122
  52. package/src/vault-create.test.ts +38 -10
  53. package/src/vault.test.ts +48 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Transcription capability flag (scribe-fold Phase 1).
3
+ *
4
+ * The vault landing (`GET /vault/<name>/api/vault`) surfaces a `transcription`
5
+ * capability so a surface (Notes) can gate its microphone affordance on
6
+ * whether transcription is actually possible — distinct from the
7
+ * `auto_transcribe.enabled` POLICY toggle (which defaults on even when no
8
+ * provider is reachable). `enabled` is true only when a provider is configured
9
+ * AND its `available()` probe passes.
10
+ *
11
+ * `minutes_remaining` is deliberately omitted: it's a cloud/plan concern, and
12
+ * self-host is unmetered. The cloud voice build, whose Workers-AI provider
13
+ * implements the same `TranscriptionProvider` interface, will add metering at
14
+ * that layer.
15
+ */
16
+
17
+ import type { TranscriptionProvider } from "../../core/src/transcription/provider.ts";
18
+ import { ScribeHttpProvider } from "./providers/scribe-http.ts";
19
+ import { TranscribeCppProvider } from "./providers/transcribe-cpp.ts";
20
+ import { ParakeetMlxProvider } from "./providers/parakeet-mlx.ts";
21
+ import { OnnxAsrProvider } from "./providers/onnx-asr.ts";
22
+ import { getCachedScribeUrl } from "../scribe-discovery.ts";
23
+ import { resolveScribeAuthToken } from "../scribe-env.ts";
24
+ import {
25
+ resolveTranscriptionProviderName,
26
+ resolveTranscribeCppPaths,
27
+ resolveParakeetMlxBin,
28
+ resolveParakeetMlxModel,
29
+ resolveOnnxAsrBin,
30
+ resolveOnnxAsrModel,
31
+ } from "./select.ts";
32
+
33
+ export interface TranscriptionCapability {
34
+ /** True when a provider is configured AND available. Notes gates the mic on this. */
35
+ enabled: boolean;
36
+ /** The active provider's name (e.g. "scribe-http"). Omitted when disabled. */
37
+ provider?: string;
38
+ }
39
+
40
+ /**
41
+ * Build the default provider from live per-process config, honoring
42
+ * `TRANSCRIPTION_PROVIDER` (scribe-fold Phase 2a) so the capability flag
43
+ * reflects whichever provider is actually configured:
44
+ *
45
+ * - `transcribe-cpp` → the local provider, resolving the installed binary +
46
+ * GGUF model paths; `available()` is `false` until `transcription install`
47
+ * has run.
48
+ * - `parakeet-mlx` / `onnx-asr` (scribe-fold Phase 2b) → the Python-based
49
+ * local providers, resolving the binary via env override → managed venv →
50
+ * PATH; `available()` is `false` until a runnable binary exists.
51
+ * - `scribe-http` (default) → the remote provider (URL from
52
+ * `SCRIBE_URL`/services.json, bearer from `SCRIBE_AUTH_TOKEN`). When scribe
53
+ * isn't discoverable the URL is undefined and it reports itself unavailable
54
+ * rather than throwing.
55
+ */
56
+ export function defaultTranscriptionProvider(): TranscriptionProvider {
57
+ const name = resolveTranscriptionProviderName();
58
+ if (name === "transcribe-cpp") {
59
+ const paths = resolveTranscribeCppPaths();
60
+ return new TranscribeCppProvider({ binPath: paths.binPath, modelPath: paths.modelPath });
61
+ }
62
+ if (name === "parakeet-mlx") {
63
+ return new ParakeetMlxProvider({
64
+ binPath: resolveParakeetMlxBin(),
65
+ model: resolveParakeetMlxModel(),
66
+ });
67
+ }
68
+ if (name === "onnx-asr") {
69
+ return new OnnxAsrProvider({
70
+ binPath: resolveOnnxAsrBin(),
71
+ model: resolveOnnxAsrModel(),
72
+ });
73
+ }
74
+ return new ScribeHttpProvider({
75
+ url: getCachedScribeUrl(),
76
+ token: resolveScribeAuthToken(),
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Resolve the transcription capability for the vault landing. `enabled` is the
82
+ * provider's `available()` result; `provider` is its name when enabled. A
83
+ * "no provider configured" deployment resolves cleanly to `{ enabled: false }`
84
+ * (no throw), so the landing never crashes when scribe is absent.
85
+ *
86
+ * The `provider` arg is an injection seam for tests + a future explicit
87
+ * provider selection; production omits it and uses the default scribe-http
88
+ * provider built from live config.
89
+ */
90
+ export async function resolveTranscriptionCapability(
91
+ provider: TranscriptionProvider = defaultTranscriptionProvider(),
92
+ ): Promise<TranscriptionCapability> {
93
+ const avail = await provider.available();
94
+ if (!avail.ok) return { enabled: false };
95
+ return { enabled: true, provider: provider.name };
96
+ }
@@ -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
+ });