@openparachute/vault 0.7.5 → 0.7.6-rc.2
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/package.json +1 -1
- package/src/attachment-tickets.ts +17 -1
- package/src/auto-transcribe.test.ts +105 -0
- package/src/auto-transcribe.ts +103 -4
- package/src/cli.ts +97 -82
- package/src/mcp-tools.ts +28 -7
- package/src/mirror-remote-guard.test.ts +159 -0
- package/src/mirror-remote-guard.ts +124 -0
- package/src/mirror-routes.test.ts +147 -0
- package/src/mirror-routes.ts +125 -5
- package/src/routes.ts +33 -2
- package/src/transcription/capability.test.ts +48 -1
- package/src/transcription/capability.ts +23 -0
- package/src/transcription/providers/whisper-cpp.test.ts +44 -1
- package/src/transcription/providers/whisper-cpp.ts +34 -3
- package/src/transcription/select.test.ts +83 -20
- package/src/transcription/select.ts +48 -16
- package/src/transcription-routes.test.ts +29 -6
- package/src/transcription-status-cli.test.ts +221 -0
- package/src/vault.test.ts +120 -0
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* The active provider is chosen by the `TRANSCRIPTION_PROVIDER` env var
|
|
14
14
|
* (persisted in `~/.parachute/vault/.env`), resolved here so the worker boot
|
|
15
15
|
* (`server.ts`) and the capability flag (`capability.ts`) agree on one source
|
|
16
|
-
* of truth. Unset ⇒ `
|
|
17
|
-
*
|
|
16
|
+
* of truth. Unset ⇒ `whisper-cpp` UNLESS a scribe is actually reachable, in
|
|
17
|
+
* which case `scribe-http` (see `resolveTranscriptionProviderName`).
|
|
18
18
|
*
|
|
19
19
|
* The `transcribe-cli` binary, GGUF model, and runtime shared libraries live
|
|
20
20
|
* under `$PARACHUTE_HOME/transcription/` (parallel to the vault's other
|
|
@@ -30,6 +30,7 @@ import { join } from "path";
|
|
|
30
30
|
import { homedir } from "os";
|
|
31
31
|
import { existsSync, readFileSync } from "fs";
|
|
32
32
|
import { DEFAULT_MODEL_ID } from "./models.ts";
|
|
33
|
+
import { resolveScribeUrl } from "../scribe-discovery.ts";
|
|
33
34
|
|
|
34
35
|
export const TRANSCRIPTION_PROVIDERS = [
|
|
35
36
|
"whisper-cpp",
|
|
@@ -54,34 +55,65 @@ export type TranscriptionProviderName = (typeof TRANSCRIPTION_PROVIDERS)[number]
|
|
|
54
55
|
export const DEFAULT_PARAKEET_MLX_MODEL = "mlx-community/parakeet-tdt-0.6b-v3";
|
|
55
56
|
export const DEFAULT_ONNX_ASR_MODEL = "nemo-parakeet-tdt-0.6b-v3";
|
|
56
57
|
|
|
58
|
+
/** Injection seam: "is a scribe actually reachable from here?" */
|
|
59
|
+
export interface ProviderResolveDeps {
|
|
60
|
+
scribeConfiguredImpl?: () => boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
57
63
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
64
|
+
* The default provider when nothing is configured.
|
|
65
|
+
*
|
|
66
|
+
* This used to be an unconditional `scribe-http`, which was the wrong default
|
|
67
|
+
* the moment scribe stopped shipping: a fresh box resolved to a provider with
|
|
68
|
+
* no backend, so it accepted audio and transcribed nothing (vault#643 made that
|
|
69
|
+
* visible; it did not make it correct). `whisper-cpp` is the right default —
|
|
70
|
+
* it's local, and `transcription install` now installs a binary + model and
|
|
71
|
+
* verifies a real transcription before claiming success (vault#636), which is
|
|
72
|
+
* the precondition this flip was waiting on.
|
|
62
73
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
74
|
+
* But flipping unconditionally would break the boxes that DO have a working
|
|
75
|
+
* scribe: their config is the absence of config, so "unset" can't be read as
|
|
76
|
+
* "wants local". So the default asks whether a scribe is actually reachable —
|
|
77
|
+
* the same `SCRIBE_URL`-then-`services.json` resolution the provider itself
|
|
78
|
+
* uses — and defers to it when one is. A working box keeps working; a box with
|
|
79
|
+
* nothing gets the provider it can actually install.
|
|
80
|
+
*
|
|
81
|
+
* Deliberately NOT a migration that writes `TRANSCRIPTION_PROVIDER` to `.env`:
|
|
82
|
+
* pinning a value at upgrade time would freeze whichever answer was true that
|
|
83
|
+
* day, and an operator who later removes scribe would stay pinned to a dead
|
|
84
|
+
* provider — the exact failure being fixed here.
|
|
85
|
+
*/
|
|
86
|
+
function defaultProviderName(
|
|
87
|
+
env: NodeJS.ProcessEnv,
|
|
88
|
+
deps: ProviderResolveDeps,
|
|
89
|
+
): TranscriptionProviderName {
|
|
90
|
+
const scribeConfigured =
|
|
91
|
+
deps.scribeConfiguredImpl ?? (() => resolveScribeUrl(env, undefined, { warn: undefined }) !== undefined);
|
|
92
|
+
return scribeConfigured() ? "scribe-http" : "whisper-cpp";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolve the configured provider name. `TRANSCRIPTION_PROVIDER` selects it;
|
|
97
|
+
* unset (or blank) ⇒ {@link defaultProviderName}. An unrecognized value warns
|
|
98
|
+
* once and falls back rather than failing boot — a typo shouldn't take
|
|
99
|
+
* transcription offline hard.
|
|
70
100
|
*/
|
|
71
101
|
export function resolveTranscriptionProviderName(
|
|
72
102
|
env: NodeJS.ProcessEnv = process.env,
|
|
73
103
|
logger: { warn?: (...args: unknown[]) => void } = console,
|
|
104
|
+
deps: ProviderResolveDeps = {},
|
|
74
105
|
): TranscriptionProviderName {
|
|
75
106
|
const raw = env.TRANSCRIPTION_PROVIDER?.trim();
|
|
76
|
-
if (!raw) return
|
|
107
|
+
if (!raw) return defaultProviderName(env, deps);
|
|
77
108
|
if ((TRANSCRIPTION_PROVIDERS as readonly string[]).includes(raw)) {
|
|
78
109
|
return raw as TranscriptionProviderName;
|
|
79
110
|
}
|
|
111
|
+
const fallback = defaultProviderName(env, deps);
|
|
80
112
|
logger.warn?.(
|
|
81
|
-
`[transcribe] unknown TRANSCRIPTION_PROVIDER="${raw}" — falling back to
|
|
113
|
+
`[transcribe] unknown TRANSCRIPTION_PROVIDER="${raw}" — falling back to ${fallback}. ` +
|
|
82
114
|
`Valid values: ${TRANSCRIPTION_PROVIDERS.join(", ")}.`,
|
|
83
115
|
);
|
|
84
|
-
return
|
|
116
|
+
return fallback;
|
|
85
117
|
}
|
|
86
118
|
|
|
87
119
|
/** The ecosystem root (shared with `config.ts`'s `configDirPath`), per-call so
|
|
@@ -44,12 +44,13 @@ beforeEach(() => {
|
|
|
44
44
|
process.env.PATH = "";
|
|
45
45
|
delete process.env.WHISPER_CPP_BIN_DIR;
|
|
46
46
|
delete process.env.TRANSCRIPTION_PROVIDER;
|
|
47
|
+
delete process.env.SCRIBE_URL;
|
|
47
48
|
delete process.env.TRANSCRIPTION_MODEL;
|
|
48
49
|
});
|
|
49
50
|
|
|
50
51
|
afterEach(() => {
|
|
51
52
|
rmSync(home, { recursive: true, force: true });
|
|
52
|
-
for (const k of ["PARACHUTE_HOME", "PATH", "WHISPER_CPP_BIN_DIR", "TRANSCRIPTION_PROVIDER", "TRANSCRIPTION_MODEL"]) {
|
|
53
|
+
for (const k of ["PARACHUTE_HOME", "PATH", "WHISPER_CPP_BIN_DIR", "TRANSCRIPTION_PROVIDER", "TRANSCRIPTION_MODEL", "SCRIBE_URL"]) {
|
|
53
54
|
if (ORIG[k] === undefined) delete process.env[k];
|
|
54
55
|
else process.env[k] = ORIG[k];
|
|
55
56
|
}
|
|
@@ -68,11 +69,33 @@ function installFfmpeg() {
|
|
|
68
69
|
present.add(join(home, "ff", "ffmpeg"));
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
describe("snapshot — the default
|
|
72
|
-
test("
|
|
73
|
-
// The fresh-install state
|
|
74
|
-
// scribe
|
|
75
|
-
//
|
|
72
|
+
describe("snapshot — the default provider on a fresh box", () => {
|
|
73
|
+
test("nothing configured → whisper-cpp, and it reports what's missing", () => {
|
|
74
|
+
// The fresh-install state. This used to resolve to `scribe-http` with no
|
|
75
|
+
// scribe anywhere — a provider that could never run, which is how audio was
|
|
76
|
+
// accepted and silently never transcribed. The default is now the local
|
|
77
|
+
// provider, so "not ready" is a list of things `transcription install`
|
|
78
|
+
// fixes rather than a dead end.
|
|
79
|
+
const s = buildTranscriptionSnapshot(deps(false));
|
|
80
|
+
expect(s.provider).toBe("whisper-cpp");
|
|
81
|
+
expect(s.ready).toBe(false);
|
|
82
|
+
expect(s.reason).toMatch(/parakeet-cli|model file|ffmpeg/);
|
|
83
|
+
expect(s.fix_command).toBe("parachute-vault transcription install");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("a box with a reachable scribe still resolves to scribe-http", () => {
|
|
87
|
+
// The flip's safety property, asserted at the snapshot level: an operator
|
|
88
|
+
// running scribe today configured it by NOT setting TRANSCRIPTION_PROVIDER,
|
|
89
|
+
// so the flip must not move them off it.
|
|
90
|
+
process.env.SCRIBE_URL = "http://127.0.0.1:1943";
|
|
91
|
+
const s = buildTranscriptionSnapshot(deps(true));
|
|
92
|
+
expect(s.provider).toBe("scribe-http");
|
|
93
|
+
// ...and with a worker live, it reports as working rather than as broken.
|
|
94
|
+
expect(s.ready).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("an explicitly-pinned scribe-http with nothing behind it still says why", () => {
|
|
98
|
+
process.env.TRANSCRIPTION_PROVIDER = "scribe-http";
|
|
76
99
|
const s = buildTranscriptionSnapshot(deps(false));
|
|
77
100
|
expect(s.provider).toBe("scribe-http");
|
|
78
101
|
expect(s.ready).toBe(false);
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `transcription status` — that it reports the ACTIVE provider honestly.
|
|
3
|
+
*
|
|
4
|
+
* Found live: a box with a working whisper-cpp install (binary, model, and
|
|
5
|
+
* ffmpeg all present, transcribing fine) was told
|
|
6
|
+
*
|
|
7
|
+
* ⚠ provider is whisper-cpp but no runnable whisper-cpp install was found —
|
|
8
|
+
* transcription is offline until one is available
|
|
9
|
+
*
|
|
10
|
+
* The command predates whisper-cpp (vault#635) and never grew a branch for it,
|
|
11
|
+
* so the `activeRunnable` disjunction simply omitted it and could only ever
|
|
12
|
+
* evaluate false. A status command that reports working software as broken is
|
|
13
|
+
* worse than no status command — it sends someone to re-install a 400 MB model
|
|
14
|
+
* to fix nothing.
|
|
15
|
+
*
|
|
16
|
+
* The fix shares `buildTranscriptionSnapshot` with the admin SPA's page, so
|
|
17
|
+
* these tests pin the property that actually prevents recurrence: the CLI and
|
|
18
|
+
* the UI answer "is transcription working" from ONE implementation.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, expect, test, afterEach } from "bun:test";
|
|
22
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, chmodSync } from "fs";
|
|
23
|
+
import { tmpdir } from "os";
|
|
24
|
+
import { join, resolve } from "path";
|
|
25
|
+
import { buildTranscriptionSnapshot } from "./transcription-routes.ts";
|
|
26
|
+
import { TRANSCRIPTION_PROVIDERS } from "./transcription/select.ts";
|
|
27
|
+
|
|
28
|
+
describe("every provider the CLI can resolve is one status knows about", () => {
|
|
29
|
+
test("the provider list has no member without a readiness story", () => {
|
|
30
|
+
// The regression in one assertion: whisper-cpp was resolvable as the active
|
|
31
|
+
// provider while nothing computed its readiness. Any future provider added
|
|
32
|
+
// to TRANSCRIPTION_PROVIDERS has to be handled too.
|
|
33
|
+
const handled = new Set([
|
|
34
|
+
"scribe-http",
|
|
35
|
+
"whisper-cpp",
|
|
36
|
+
"transcribe-cpp",
|
|
37
|
+
"parakeet-mlx",
|
|
38
|
+
"onnx-asr",
|
|
39
|
+
]);
|
|
40
|
+
for (const p of TRANSCRIPTION_PROVIDERS) {
|
|
41
|
+
expect(handled.has(p)).toBe(true);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("the snapshot is the single source of truth", () => {
|
|
47
|
+
test("readiness comes back as a decidable boolean with a reason when false", () => {
|
|
48
|
+
const snap = buildTranscriptionSnapshot({
|
|
49
|
+
active: false,
|
|
50
|
+
resolveBinaryImpl: () => undefined,
|
|
51
|
+
resolveFfmpegImpl: () => undefined,
|
|
52
|
+
existsImpl: () => false,
|
|
53
|
+
});
|
|
54
|
+
expect(typeof snap.ready).toBe("boolean");
|
|
55
|
+
expect(snap.ready).toBe(false);
|
|
56
|
+
// A false readiness must always carry something actionable, or the CLI has
|
|
57
|
+
// nothing honest to print.
|
|
58
|
+
expect(snap.reason).toBeTruthy();
|
|
59
|
+
expect(snap.fix_command).toBeTruthy();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a fully-present whisper-cpp install reports ready, with paths to print", () => {
|
|
63
|
+
const prev = process.env.TRANSCRIPTION_PROVIDER;
|
|
64
|
+
process.env.TRANSCRIPTION_PROVIDER = "whisper-cpp";
|
|
65
|
+
try {
|
|
66
|
+
const snap = buildTranscriptionSnapshot({
|
|
67
|
+
active: true,
|
|
68
|
+
resolveBinaryImpl: () => "/opt/homebrew/bin/parakeet-cli",
|
|
69
|
+
resolveFfmpegImpl: () => "/opt/homebrew/bin/ffmpeg",
|
|
70
|
+
existsImpl: () => true,
|
|
71
|
+
});
|
|
72
|
+
expect(snap.provider).toBe("whisper-cpp");
|
|
73
|
+
expect(snap.ready).toBe(true);
|
|
74
|
+
expect(snap.reason).toBeNull();
|
|
75
|
+
// These are exactly what the CLI prints — a ready snapshot must carry
|
|
76
|
+
// them or the output degrades to "yes" with no evidence.
|
|
77
|
+
expect(snap.binary.path).toBe("/opt/homebrew/bin/parakeet-cli");
|
|
78
|
+
expect(snap.ffmpeg.path).toBe("/opt/homebrew/bin/ffmpeg");
|
|
79
|
+
expect(snap.model?.installed).toBe(true);
|
|
80
|
+
} finally {
|
|
81
|
+
if (prev === undefined) delete process.env.TRANSCRIPTION_PROVIDER;
|
|
82
|
+
else process.env.TRANSCRIPTION_PROVIDER = prev;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("a missing binary still reports WHERE it looked — the launchd trap", () => {
|
|
87
|
+
// What makes "NOT FOUND" actionable on macOS, where the binary is often
|
|
88
|
+
// installed but invisible to a launchd-supervised vault.
|
|
89
|
+
const snap = buildTranscriptionSnapshot({
|
|
90
|
+
active: false,
|
|
91
|
+
resolveBinaryImpl: () => undefined,
|
|
92
|
+
resolveFfmpegImpl: () => "/usr/bin/ffmpeg",
|
|
93
|
+
existsImpl: () => true,
|
|
94
|
+
});
|
|
95
|
+
expect(snap.binary.path).toBeNull();
|
|
96
|
+
expect(snap.binary.searched.length).toBeGreaterThan(0);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("what `transcription status` must not claim", () => {
|
|
101
|
+
test("readiness never depends on the in-process worker registry", () => {
|
|
102
|
+
// A one-shot CLI process has no transcription worker — `active` is
|
|
103
|
+
// structurally false there. Keying the headline off it would print
|
|
104
|
+
// "ready, but the worker isn't running yet" on every healthy box, which is
|
|
105
|
+
// the same class of lie the command was just fixed for.
|
|
106
|
+
const installed = {
|
|
107
|
+
resolveBinaryImpl: () => "/opt/homebrew/bin/parakeet-cli",
|
|
108
|
+
resolveFfmpegImpl: () => "/opt/homebrew/bin/ffmpeg",
|
|
109
|
+
existsImpl: () => true,
|
|
110
|
+
};
|
|
111
|
+
const prev = process.env.TRANSCRIPTION_PROVIDER;
|
|
112
|
+
process.env.TRANSCRIPTION_PROVIDER = "whisper-cpp";
|
|
113
|
+
try {
|
|
114
|
+
// `ready` is identical whether or not a worker happens to be live; only
|
|
115
|
+
// `active` differs. The CLI reports the former.
|
|
116
|
+
const withWorker = buildTranscriptionSnapshot({ ...installed, active: true });
|
|
117
|
+
const without = buildTranscriptionSnapshot({ ...installed, active: false });
|
|
118
|
+
expect(withWorker.ready).toBe(true);
|
|
119
|
+
expect(without.ready).toBe(true);
|
|
120
|
+
expect(without.active).toBe(false);
|
|
121
|
+
} finally {
|
|
122
|
+
if (prev === undefined) delete process.env.TRANSCRIPTION_PROVIDER;
|
|
123
|
+
else process.env.TRANSCRIPTION_PROVIDER = prev;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The command must report the CONFIG FILE, not its own process environment.
|
|
130
|
+
*
|
|
131
|
+
* Found live (UniOps, 2026-08-02): a box whose `.env` said `whisper-cpp` was
|
|
132
|
+
* told `scribe-http` — the retired service — while `status` was the very tool
|
|
133
|
+
* being used to diagnose why transcription was dead. Same class as the bug at
|
|
134
|
+
* the top of this file: the daemon loads `~/.parachute/vault/.env` at boot
|
|
135
|
+
* (`server.ts` → `loadEnvFile()`), a one-shot CLI process never did, and every
|
|
136
|
+
* resolver underneath reads `process.env`.
|
|
137
|
+
*
|
|
138
|
+
* These spawn the real CLI because that is where the defect lived — the
|
|
139
|
+
* resolvers were always correct when handed the right env; nothing but a
|
|
140
|
+
* process boundary reproduces it. No in-test `Bun.serve` is involved, so
|
|
141
|
+
* `Bun.spawnSync` is fine here (CLAUDE.md, "Subprocess tests + Bun.serve").
|
|
142
|
+
*
|
|
143
|
+
* Each case sets a value that DIFFERS from the fallback. A test using a value
|
|
144
|
+
* the fallback happens to produce would pass without the fix — which is
|
|
145
|
+
* precisely how this shipped: on an unconfigured box the default agrees with
|
|
146
|
+
* the file, so the command looked right until someone changed something.
|
|
147
|
+
*/
|
|
148
|
+
describe("`transcription status` reads ~/.parachute/vault/.env", () => {
|
|
149
|
+
const CLI = resolve(import.meta.dir, "cli.ts");
|
|
150
|
+
const homes: string[] = [];
|
|
151
|
+
|
|
152
|
+
afterEach(() => {
|
|
153
|
+
for (const h of homes.splice(0)) rmSync(h, { recursive: true, force: true });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/** A temp PARACHUTE_HOME whose `vault/.env` holds exactly these lines. */
|
|
157
|
+
function homeWithEnv(lines: string[]): string {
|
|
158
|
+
const home = mkdtempSync(join(tmpdir(), "pv-transcription-env-"));
|
|
159
|
+
homes.push(home);
|
|
160
|
+
mkdirSync(join(home, "vault"), { recursive: true });
|
|
161
|
+
writeFileSync(join(home, "vault", ".env"), `${lines.join("\n")}\n`);
|
|
162
|
+
return home;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Run `transcription status` with the file's values ONLY on disk. */
|
|
166
|
+
function status(home: string): string {
|
|
167
|
+
// Strip the inherited values so the child cannot pass by reading the
|
|
168
|
+
// parent's environment — the file is the only source in play.
|
|
169
|
+
const env: Record<string, string | undefined> = { ...process.env, PARACHUTE_HOME: home };
|
|
170
|
+
for (const k of [
|
|
171
|
+
"TRANSCRIPTION_PROVIDER",
|
|
172
|
+
"TRANSCRIPTION_MODEL",
|
|
173
|
+
"WHISPER_CPP_BIN_DIR",
|
|
174
|
+
"SCRIBE_URL",
|
|
175
|
+
"PARACHUTE_HUB_ORIGIN",
|
|
176
|
+
]) {
|
|
177
|
+
delete env[k];
|
|
178
|
+
}
|
|
179
|
+
const proc = Bun.spawnSync({
|
|
180
|
+
cmd: ["bun", CLI, "transcription", "status"],
|
|
181
|
+
stdout: "pipe",
|
|
182
|
+
stderr: "pipe",
|
|
183
|
+
env,
|
|
184
|
+
});
|
|
185
|
+
return new TextDecoder().decode(proc.stdout);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
test("the provider comes from the file, not the fallback default", () => {
|
|
189
|
+
// `scribe-http` is never the fallback on a box with no scribe in
|
|
190
|
+
// services.json — the default there is whisper-cpp. So this asserts the
|
|
191
|
+
// file was read rather than that two paths coincided.
|
|
192
|
+
const out = status(homeWithEnv(["TRANSCRIPTION_PROVIDER=scribe-http", "SCRIBE_URL=http://127.0.0.1:1943"]));
|
|
193
|
+
expect(out).toContain("scribe-http");
|
|
194
|
+
expect(out).not.toContain("(whisper-cpp)");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("the model comes from the file", () => {
|
|
198
|
+
const out = status(homeWithEnv(["TRANSCRIPTION_PROVIDER=whisper-cpp", "TRANSCRIPTION_MODEL=whisper-tiny.en"]));
|
|
199
|
+
expect(out).toContain("Whisper Tiny (English)");
|
|
200
|
+
expect(out).not.toContain("Parakeet TDT 0.6b v3");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("a binary-dir override in the file is honored — the false 'not found'", () => {
|
|
204
|
+
// The sharpest symptom: an installed, working binary reported missing
|
|
205
|
+
// because the `.env` override naming its directory never reached the
|
|
206
|
+
// resolver. This is the `runnable: no` line UniOps flagged as suspect.
|
|
207
|
+
const home = homeWithEnv([]);
|
|
208
|
+
const bin = join(home, "fakebin");
|
|
209
|
+
mkdirSync(bin, { recursive: true });
|
|
210
|
+
const exe = join(bin, "parakeet-cli");
|
|
211
|
+
writeFileSync(exe, "#!/bin/sh\nexit 0\n");
|
|
212
|
+
chmodSync(exe, 0o755);
|
|
213
|
+
writeFileSync(
|
|
214
|
+
join(home, "vault", ".env"),
|
|
215
|
+
`TRANSCRIPTION_PROVIDER=whisper-cpp\nWHISPER_CPP_BIN_DIR=${bin}\n`,
|
|
216
|
+
);
|
|
217
|
+
const out = status(home);
|
|
218
|
+
expect(out).toContain(exe);
|
|
219
|
+
expect(out).not.toContain("parakeet-cli not found");
|
|
220
|
+
});
|
|
221
|
+
});
|
package/src/vault.test.ts
CHANGED
|
@@ -2901,6 +2901,63 @@ describe("HTTP /notes", async () => {
|
|
|
2901
2901
|
expect((note!.metadata as any)?.transcribe_stub).toBeUndefined();
|
|
2902
2902
|
});
|
|
2903
2903
|
|
|
2904
|
+
// The explicit opt-out. `transcribe: false` is a caller saying no, and it
|
|
2905
|
+
// used to be indistinguishable from saying nothing — `body.transcribe` was
|
|
2906
|
+
// read once, as `=== true`. So a user who turned the capture's transcribe
|
|
2907
|
+
// toggle OFF had their decision answered by the auto-transcribe guess,
|
|
2908
|
+
// which exists for callers who expressed no preference.
|
|
2909
|
+
//
|
|
2910
|
+
// Contrast with the test directly above: same request, same suite, same
|
|
2911
|
+
// absent provider — absent gets `failed`, `false` gets nothing at all.
|
|
2912
|
+
test("transcribe: false is honoured — no transcription, and NO failure marker", async () => {
|
|
2913
|
+
await store.createNote("note body", { id: "v2c" });
|
|
2914
|
+
const res = await handleNotes(
|
|
2915
|
+
mkReq("POST", "/notes/v2c/attachments", {
|
|
2916
|
+
path: "memos/memo-optout.webm",
|
|
2917
|
+
mimeType: "audio/webm",
|
|
2918
|
+
transcribe: false,
|
|
2919
|
+
}),
|
|
2920
|
+
store,
|
|
2921
|
+
"/v2c/attachments",
|
|
2922
|
+
);
|
|
2923
|
+
expect(res.status).toBe(201);
|
|
2924
|
+
const att = await res.json() as any;
|
|
2925
|
+
// Not enqueued...
|
|
2926
|
+
expect(att.metadata?.transcribe_status).toBeUndefined();
|
|
2927
|
+
// ...and NOT recorded as a misconfiguration either. Nothing failed; the
|
|
2928
|
+
// caller asked for nothing to happen and nothing happened.
|
|
2929
|
+
expect(att.metadata?.transcribe_error).toBeUndefined();
|
|
2930
|
+
expect(att.metadata?.transcribe_origin).toBeUndefined();
|
|
2931
|
+
// The note is untouched — no stub to fill, because no transcript is coming.
|
|
2932
|
+
const note = await store.getNote("v2c");
|
|
2933
|
+
expect((note!.metadata as any)?.transcribe_stub).toBeUndefined();
|
|
2934
|
+
});
|
|
2935
|
+
|
|
2936
|
+
test("false and absent are no longer the same request", async () => {
|
|
2937
|
+
// The regression this pair exists to prevent, stated as one assertion:
|
|
2938
|
+
// if these two ever produce equal metadata again, the opt-out has been
|
|
2939
|
+
// collapsed back into "no opinion".
|
|
2940
|
+
await store.createNote("body", { id: "v2d" });
|
|
2941
|
+
await store.createNote("body", { id: "v2e" });
|
|
2942
|
+
const mk = async (id: string, extra: Record<string, unknown>) => {
|
|
2943
|
+
const r = await handleNotes(
|
|
2944
|
+
mkReq("POST", `/notes/${id}/attachments`, {
|
|
2945
|
+
path: `memos/${id}.webm`,
|
|
2946
|
+
mimeType: "audio/webm",
|
|
2947
|
+
...extra,
|
|
2948
|
+
}),
|
|
2949
|
+
store,
|
|
2950
|
+
`/${id}/attachments`,
|
|
2951
|
+
);
|
|
2952
|
+
return ((await r.json()) as any).metadata ?? {};
|
|
2953
|
+
};
|
|
2954
|
+
const absent = await mk("v2d", {});
|
|
2955
|
+
const optedOut = await mk("v2e", { transcribe: false });
|
|
2956
|
+
expect(absent.transcribe_status).toBe("failed");
|
|
2957
|
+
expect(optedOut.transcribe_status).toBeUndefined();
|
|
2958
|
+
expect(absent).not.toEqual(optedOut);
|
|
2959
|
+
});
|
|
2960
|
+
|
|
2904
2961
|
test("NON-audio with no flag still leaves metadata completely empty", async () => {
|
|
2905
2962
|
await store.createNote("note body", { id: "v2b" });
|
|
2906
2963
|
const res = await handleNotes(
|
|
@@ -7541,6 +7598,23 @@ describe("manage-token MCP tool (vault#403, MGT — hub-JWT attenuation proxy)",
|
|
|
7541
7598
|
closeAllStores();
|
|
7542
7599
|
});
|
|
7543
7600
|
|
|
7601
|
+
test("long_lived=true permits a 30-day TTL", async () => {
|
|
7602
|
+
installHubStub();
|
|
7603
|
+
const { vaultName, auth } = await setupAdminSession("mint-long-lived");
|
|
7604
|
+
const { closeAllStores } = await import("./vault-store.ts");
|
|
7605
|
+
const { parsed } = await callTool(vaultName, auth, "manage-token", {
|
|
7606
|
+
action: "mint",
|
|
7607
|
+
scope: "vault:read",
|
|
7608
|
+
ttl_seconds: 2592000,
|
|
7609
|
+
long_lived: true,
|
|
7610
|
+
});
|
|
7611
|
+
expect(parsed.action).toBe("mint");
|
|
7612
|
+
expect(parsed.error).toBeUndefined();
|
|
7613
|
+
const mint = hubCalls.find((c) => c.url.endsWith("/api/auth/mint-token"));
|
|
7614
|
+
expect(mint!.body.expires_in).toBe(2592000);
|
|
7615
|
+
closeAllStores();
|
|
7616
|
+
});
|
|
7617
|
+
|
|
7544
7618
|
test("tag-scoped caller's mint includes permissions.scoped_tags", async () => {
|
|
7545
7619
|
installHubStub();
|
|
7546
7620
|
const { vaultName, auth } = await setupAdminSession("mint-scoped", ["task", "project"]);
|
|
@@ -7568,6 +7642,52 @@ describe("manage-token MCP tool (vault#403, MGT — hub-JWT attenuation proxy)",
|
|
|
7568
7642
|
const { closeAllStores } = await import("./vault-store.ts");
|
|
7569
7643
|
const { parsed } = await callTool(vaultName, auth, "manage-token", { action: "mint", scope: "vault:read", ttl_seconds: 3601 });
|
|
7570
7644
|
expect(parsed.error).toBe("invalid_request");
|
|
7645
|
+
expect(parsed.message).toBe("manage-token mint: ttl_seconds must be in (0, 3600]; got 3601.");
|
|
7646
|
+
expect(hubCalls.length).toBe(0);
|
|
7647
|
+
closeAllStores();
|
|
7648
|
+
});
|
|
7649
|
+
|
|
7650
|
+
test("long_lived=true rejects TTL above the 90-day cap locally", async () => {
|
|
7651
|
+
installHubStub();
|
|
7652
|
+
const { vaultName, auth } = await setupAdminSession("mint-long-over");
|
|
7653
|
+
const { closeAllStores } = await import("./vault-store.ts");
|
|
7654
|
+
const { parsed } = await callTool(vaultName, auth, "manage-token", {
|
|
7655
|
+
action: "mint",
|
|
7656
|
+
scope: "vault:read",
|
|
7657
|
+
ttl_seconds: 7776001,
|
|
7658
|
+
long_lived: true,
|
|
7659
|
+
});
|
|
7660
|
+
expect(parsed.error).toBe("invalid_request");
|
|
7661
|
+
expect(parsed.message).toContain("7776000");
|
|
7662
|
+
expect(hubCalls.length).toBe(0);
|
|
7663
|
+
closeAllStores();
|
|
7664
|
+
});
|
|
7665
|
+
|
|
7666
|
+
test("long_lived=false keeps the 1-hour cap", async () => {
|
|
7667
|
+
installHubStub();
|
|
7668
|
+
const { vaultName, auth } = await setupAdminSession("mint-long-false");
|
|
7669
|
+
const { closeAllStores } = await import("./vault-store.ts");
|
|
7670
|
+
const { parsed } = await callTool(vaultName, auth, "manage-token", {
|
|
7671
|
+
action: "mint",
|
|
7672
|
+
scope: "vault:read",
|
|
7673
|
+
ttl_seconds: 7200,
|
|
7674
|
+
long_lived: false,
|
|
7675
|
+
});
|
|
7676
|
+
expect(parsed.error).toBe("invalid_request");
|
|
7677
|
+
expect(hubCalls.length).toBe(0);
|
|
7678
|
+
closeAllStores();
|
|
7679
|
+
});
|
|
7680
|
+
|
|
7681
|
+
test("omitted long_lived keeps the 1-hour cap", async () => {
|
|
7682
|
+
installHubStub();
|
|
7683
|
+
const { vaultName, auth } = await setupAdminSession("mint-long-omitted");
|
|
7684
|
+
const { closeAllStores } = await import("./vault-store.ts");
|
|
7685
|
+
const { parsed } = await callTool(vaultName, auth, "manage-token", {
|
|
7686
|
+
action: "mint",
|
|
7687
|
+
scope: "vault:read",
|
|
7688
|
+
ttl_seconds: 7200,
|
|
7689
|
+
});
|
|
7690
|
+
expect(parsed.error).toBe("invalid_request");
|
|
7571
7691
|
expect(hubCalls.length).toBe(0);
|
|
7572
7692
|
closeAllStores();
|
|
7573
7693
|
});
|