@openparachute/vault 0.7.9-rc.6 → 0.7.9-rc.8
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/core/src/history-capture.test.ts +9 -1
- package/core/src/history-visibility.test.ts +11 -0
- package/core/src/history-visibility.ts +9 -0
- package/core/src/history.test.ts +57 -0
- package/core/src/history.ts +12 -1
- package/core/src/store.ts +2 -0
- package/package.json +1 -1
- package/src/attachment-tickets.test.ts +49 -27
- package/src/auto-transcribe-integration.test.ts +82 -0
- package/src/auto-transcribe.test.ts +126 -374
- package/src/auto-transcribe.ts +43 -215
- package/src/history-import-routes.test.ts +23 -1
- package/src/history-import.test.ts +34 -2
- package/src/history-import.ts +4 -1
- package/src/mcp-tools.ts +8 -2
- package/src/routes.ts +5 -3
- package/src/server.ts +7 -7
- package/src/transcription-registry.ts +9 -7
- package/src/vault-compact-routes.test.ts +1 -1
- package/src/vault-history-routes.test.ts +50 -1
- package/src/vault.test.ts +40 -17
|
@@ -1,388 +1,140 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auto-transcribe gating decisions (vault#353).
|
|
3
|
-
*
|
|
4
|
-
* Three independent guards: mime-type prefix, enabled toggle, scribe URL
|
|
5
|
-
* present. Pure function — exercise the truth table.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { describe, test, expect } from "bun:test";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe("shouldAutoTranscribe", () => {
|
|
26
|
-
const scribePresent = () => "http://127.0.0.1:1943";
|
|
27
|
-
const scribeAbsent = () => undefined;
|
|
28
|
-
|
|
29
|
-
test("triggers on audio/* mime-type when enabled + scribe reachable", () => {
|
|
30
|
-
expect(shouldAutoTranscribe("audio/wav", {
|
|
31
|
-
readGlobalConfigImpl: readGlobalConfig(true),
|
|
32
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
33
|
-
})).toBe(true);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("triggers on audio/mp4 (m4a)", () => {
|
|
37
|
-
expect(shouldAutoTranscribe("audio/mp4", {
|
|
38
|
-
readGlobalConfigImpl: readGlobalConfig(true),
|
|
39
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
40
|
-
})).toBe(true);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("triggers on audio/webm", () => {
|
|
44
|
-
expect(shouldAutoTranscribe("audio/webm", {
|
|
45
|
-
readGlobalConfigImpl: readGlobalConfig(true),
|
|
46
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
47
|
-
})).toBe(true);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("triggers case-insensitively (AUDIO/WAV)", () => {
|
|
51
|
-
expect(shouldAutoTranscribe("AUDIO/WAV", {
|
|
52
|
-
readGlobalConfigImpl: readGlobalConfig(true),
|
|
53
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
54
|
-
})).toBe(true);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("skips non-audio mime-types (image/png, application/pdf, video/mp4)", () => {
|
|
58
|
-
expect(shouldAutoTranscribe("image/png", {
|
|
59
|
-
readGlobalConfigImpl: readGlobalConfig(true),
|
|
60
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
61
|
-
})).toBe(false);
|
|
62
|
-
expect(shouldAutoTranscribe("application/pdf", {
|
|
63
|
-
readGlobalConfigImpl: readGlobalConfig(true),
|
|
64
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
65
|
-
})).toBe(false);
|
|
66
|
-
expect(shouldAutoTranscribe("video/mp4", {
|
|
67
|
-
readGlobalConfigImpl: readGlobalConfig(true),
|
|
68
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
69
|
-
})).toBe(false);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("skips when enabled is false (default off)", () => {
|
|
73
|
-
expect(shouldAutoTranscribe("audio/wav", {
|
|
74
|
-
readGlobalConfigImpl: readGlobalConfig(false),
|
|
75
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
76
|
-
})).toBe(false);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("fires when enabled is unset — unset config means ON", () => {
|
|
80
|
-
// Default behavior (no `auto_transcribe` block in config) is opt-out:
|
|
81
|
-
// once an operator has scribe reachable, audio attachments transcribe
|
|
82
|
-
// automatically. Operators wanting it OFF set
|
|
83
|
-
// `auto_transcribe.enabled: false` explicitly. Previously default-off;
|
|
84
|
-
// flipped to default-on so installing scribe is the only opt-in signal.
|
|
85
|
-
expect(shouldAutoTranscribe("audio/wav", {
|
|
86
|
-
readGlobalConfigImpl: readGlobalConfig(undefined),
|
|
87
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
88
|
-
})).toBe(true);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("skips when scribe URL is undefined (no services.json entry, no env)", () => {
|
|
92
|
-
expect(shouldAutoTranscribe("audio/wav", {
|
|
93
|
-
readGlobalConfigImpl: readGlobalConfig(true),
|
|
94
|
-
getCachedScribeUrlImpl: scribeAbsent,
|
|
95
|
-
})).toBe(false);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("skips when scribe URL is empty string", () => {
|
|
99
|
-
expect(shouldAutoTranscribe("audio/wav", {
|
|
100
|
-
readGlobalConfigImpl: readGlobalConfig(true),
|
|
101
|
-
getCachedScribeUrlImpl: () => "",
|
|
102
|
-
})).toBe(false);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("skips on garbage mime-type input", () => {
|
|
106
|
-
expect(shouldAutoTranscribe("", {
|
|
107
|
-
readGlobalConfigImpl: readGlobalConfig(true),
|
|
108
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
109
|
-
})).toBe(false);
|
|
110
|
-
expect(shouldAutoTranscribe("not-a-mime", {
|
|
111
|
-
readGlobalConfigImpl: readGlobalConfig(true),
|
|
112
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
113
|
-
})).toBe(false);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("respects enabledOverride when present", () => {
|
|
117
|
-
expect(shouldAutoTranscribe("audio/wav", {
|
|
118
|
-
readGlobalConfigImpl: readGlobalConfig(false),
|
|
119
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
120
|
-
enabledOverride: true,
|
|
121
|
-
})).toBe(true);
|
|
122
|
-
expect(shouldAutoTranscribe("audio/wav", {
|
|
123
|
-
readGlobalConfigImpl: readGlobalConfig(true),
|
|
124
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
125
|
-
enabledOverride: false,
|
|
126
|
-
})).toBe(false);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe("per-vault precedence (per-vault → global → true)", () => {
|
|
130
|
-
test("per-vault true wins even when global is false", () => {
|
|
131
|
-
expect(shouldAutoTranscribe("audio/wav", {
|
|
132
|
-
readGlobalConfigImpl: readGlobalConfig(false),
|
|
133
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
134
|
-
perVaultEnabled: true,
|
|
135
|
-
})).toBe(true);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("per-vault false wins even when global is true", () => {
|
|
139
|
-
// The whole point: linking scribe to vault X (perVault true) elsewhere
|
|
140
|
-
// must not force-on a vault that set its own false.
|
|
141
|
-
expect(shouldAutoTranscribe("audio/wav", {
|
|
142
|
-
readGlobalConfigImpl: readGlobalConfig(true),
|
|
143
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
144
|
-
perVaultEnabled: false,
|
|
145
|
-
})).toBe(false);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("per-vault unset falls back to global", () => {
|
|
149
|
-
expect(shouldAutoTranscribe("audio/wav", {
|
|
150
|
-
readGlobalConfigImpl: readGlobalConfig(true),
|
|
151
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
152
|
-
perVaultEnabled: undefined,
|
|
153
|
-
})).toBe(true);
|
|
154
|
-
expect(shouldAutoTranscribe("audio/wav", {
|
|
155
|
-
readGlobalConfigImpl: readGlobalConfig(false),
|
|
156
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
157
|
-
perVaultEnabled: undefined,
|
|
158
|
-
})).toBe(false);
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { findModel } from "./transcription/models.ts";
|
|
6
|
+
import { classifyAutoTranscribe, shouldAutoTranscribe, noProviderErrorFor,
|
|
7
|
+
NO_PROVIDER_ERROR, warnNoTranscriptionProvider, _resetNoProviderWarnForTest } from "./auto-transcribe.ts";
|
|
8
|
+
import { getTranscriptionWorker, getTranscriptionWorkerProvider, setTranscriptionWorker } from "./transcription-registry.ts";
|
|
9
|
+
|
|
10
|
+
const config = (enabled?: boolean) => () => ({ port: 1940, auto_transcribe: { enabled } }) as any;
|
|
11
|
+
const ready = () => ({ ready: true, localProvider: "whisper-cpp" });
|
|
12
|
+
const absent = () => ({ ready: false, localProvider: null });
|
|
13
|
+
|
|
14
|
+
describe("automatic transcription eligibility", () => {
|
|
15
|
+
for (const mime of ["audio/wav", "audio/mp4", "audio/webm", "AUDIO/WAV"]) {
|
|
16
|
+
test(`ready worker accepts ${mime}`, () => {
|
|
17
|
+
expect(shouldAutoTranscribe(mime, { readGlobalConfigImpl: config(), providerStateImpl: ready })).toBe(true);
|
|
159
18
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
getCachedScribeUrlImpl: scribePresent,
|
|
165
|
-
perVaultEnabled: undefined,
|
|
166
|
-
})).toBe(true);
|
|
19
|
+
}
|
|
20
|
+
for (const mime of ["image/png", "video/mp4", "application/pdf", "", null as any]) {
|
|
21
|
+
test(`non-audio ${mime} never probes readiness`, () => {
|
|
22
|
+
expect(classifyAutoTranscribe(mime, { providerStateImpl: () => { throw Error("unexpected probe"); } }).kind).toBe("not-audio");
|
|
167
23
|
});
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
24
|
+
}
|
|
25
|
+
for (const global of [undefined, true, false]) {
|
|
26
|
+
for (const vault of [undefined, true, false]) {
|
|
27
|
+
for (const override of [undefined, true, false]) {
|
|
28
|
+
test(`precedence global=${global} vault=${vault} override=${override}`, () => {
|
|
29
|
+
const enabled = override ?? vault ?? global ?? true;
|
|
30
|
+
const d = classifyAutoTranscribe("audio/wav", { readGlobalConfigImpl: config(global),
|
|
31
|
+
perVaultEnabled: vault, enabledOverride: override, providerStateImpl: ready });
|
|
32
|
+
expect(d.kind).toBe(enabled ? "transcribe" : "disabled");
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
test("disabled bypasses readiness entirely", () => {
|
|
38
|
+
expect(classifyAutoTranscribe("audio/wav", { perVaultEnabled: false,
|
|
39
|
+
providerStateImpl: () => { throw Error("unexpected probe"); } }).kind).toBe("disabled");
|
|
40
|
+
});
|
|
41
|
+
for (const local of [null, "whisper-cpp"]) {
|
|
42
|
+
test(`not ready remains unavailable with installed=${local}`, () => {
|
|
43
|
+
expect(classifyAutoTranscribe("audio/wav", { readGlobalConfigImpl: config(),
|
|
44
|
+
providerStateImpl: () => ({ ready: false, localProvider: local }) })).toEqual({ kind: "unavailable", localProvider: local });
|
|
177
45
|
});
|
|
46
|
+
}
|
|
47
|
+
test("unavailable is false, not an enqueue", () => {
|
|
48
|
+
expect(shouldAutoTranscribe("audio/wav", { readGlobalConfigImpl: config(), providerStateImpl: absent })).toBe(false);
|
|
178
49
|
});
|
|
179
50
|
});
|
|
180
51
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
).
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
test("operator turned it off → disabled (silence is CORRECT here)", () => {
|
|
207
|
-
expect(
|
|
208
|
-
classifyAutoTranscribe(audio, {
|
|
209
|
-
perVaultEnabled: false,
|
|
210
|
-
getCachedScribeUrlImpl: withProvider,
|
|
211
|
-
}).kind,
|
|
212
|
-
).toBe("disabled");
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("enabled + no provider → unavailable, NOT disabled — the whole point", () => {
|
|
216
|
-
expect(
|
|
217
|
-
classifyAutoTranscribe(audio, {
|
|
218
|
-
perVaultEnabled: true,
|
|
219
|
-
getCachedScribeUrlImpl: noProvider,
|
|
220
|
-
}).kind,
|
|
221
|
-
).toBe("unavailable");
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
test("a blank provider URL is unavailable, not a provider", () => {
|
|
225
|
-
expect(
|
|
226
|
-
classifyAutoTranscribe(audio, {
|
|
227
|
-
perVaultEnabled: true,
|
|
228
|
-
getCachedScribeUrlImpl: () => " ",
|
|
229
|
-
}).kind,
|
|
230
|
-
).toBe("unavailable");
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test("the default (no toggle set) with no provider is unavailable — the fresh-install case", () => {
|
|
234
|
-
expect(
|
|
235
|
-
classifyAutoTranscribe(audio, {
|
|
236
|
-
readGlobalConfigImpl: (() => ({})) as never,
|
|
237
|
-
getCachedScribeUrlImpl: noProvider,
|
|
238
|
-
}).kind,
|
|
239
|
-
).toBe("unavailable");
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test("enabled + provider → transcribe", () => {
|
|
243
|
-
expect(
|
|
244
|
-
classifyAutoTranscribe(audio, {
|
|
245
|
-
perVaultEnabled: true,
|
|
246
|
-
getCachedScribeUrlImpl: withProvider,
|
|
247
|
-
}).kind,
|
|
248
|
-
).toBe("transcribe");
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
test("shouldAutoTranscribe stays a faithful boolean view of the classifier", () => {
|
|
252
|
-
for (const opts of [
|
|
253
|
-
{ perVaultEnabled: false, getCachedScribeUrlImpl: withProvider },
|
|
254
|
-
{ perVaultEnabled: true, getCachedScribeUrlImpl: noProvider },
|
|
255
|
-
{ perVaultEnabled: true, getCachedScribeUrlImpl: withProvider },
|
|
256
|
-
]) {
|
|
257
|
-
expect(shouldAutoTranscribe(audio, opts)).toBe(
|
|
258
|
-
classifyAutoTranscribe(audio, opts).kind === "transcribe",
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
});
|
|
52
|
+
test("real gate requires a matching registered worker; stale URL cannot enable a missing local worker", () => {
|
|
53
|
+
const previous = { provider: process.env.TRANSCRIPTION_PROVIDER, url: process.env.SCRIBE_URL };
|
|
54
|
+
const worker = getTranscriptionWorker();
|
|
55
|
+
const provider = getTranscriptionWorkerProvider();
|
|
56
|
+
const fake = { stop: async () => {}, tick: async () => 0, kick: async () => {} };
|
|
57
|
+
try {
|
|
58
|
+
process.env.TRANSCRIPTION_PROVIDER = "transcribe-cpp";
|
|
59
|
+
process.env.SCRIBE_URL = "http://stale.invalid";
|
|
60
|
+
setTranscriptionWorker(null);
|
|
61
|
+
expect(classifyAutoTranscribe("audio/wav", { perVaultEnabled: true }).kind).toBe("unavailable");
|
|
62
|
+
setTranscriptionWorker(fake, "scribe-http");
|
|
63
|
+
expect(classifyAutoTranscribe("audio/wav", { perVaultEnabled: true }).kind).toBe("unavailable");
|
|
64
|
+
setTranscriptionWorker(fake, "transcribe-cpp");
|
|
65
|
+
expect(classifyAutoTranscribe("audio/wav", { perVaultEnabled: true }).kind).toBe("transcribe");
|
|
66
|
+
process.env.TRANSCRIPTION_PROVIDER = "scribe-http";
|
|
67
|
+
expect(classifyAutoTranscribe("audio/wav", { perVaultEnabled: true }).kind).toBe("unavailable");
|
|
68
|
+
setTranscriptionWorker(fake, "scribe-http");
|
|
69
|
+
expect(classifyAutoTranscribe("audio/wav", { perVaultEnabled: true }).kind).toBe("transcribe");
|
|
70
|
+
} finally {
|
|
71
|
+
setTranscriptionWorker(worker, provider);
|
|
72
|
+
if (previous.provider === undefined) delete process.env.TRANSCRIPTION_PROVIDER;
|
|
73
|
+
else process.env.TRANSCRIPTION_PROVIDER = previous.provider;
|
|
74
|
+
if (previous.url === undefined) delete process.env.SCRIBE_URL;
|
|
75
|
+
else process.env.SCRIBE_URL = previous.url;
|
|
76
|
+
}
|
|
262
77
|
});
|
|
263
78
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
expect(seen.length).toBe(1);
|
|
275
|
-
expect(seen[0]).toMatch(/no transcription provider configured/i);
|
|
276
|
-
// Past the window → one more.
|
|
277
|
-
now += 61_000;
|
|
278
|
-
warnNoTranscriptionProvider(clock);
|
|
279
|
-
expect(seen.length).toBe(2);
|
|
280
|
-
} finally {
|
|
281
|
-
console.warn = orig;
|
|
282
|
-
_resetNoProviderWarnForTest();
|
|
283
|
-
}
|
|
284
|
-
});
|
|
79
|
+
test("diagnostics distinguish installed from ready without reviving Scribe", () => {
|
|
80
|
+
expect(noProviderErrorFor(null)).toBe(NO_PROVIDER_ERROR);
|
|
81
|
+
expect(NO_PROVIDER_ERROR).toContain("transcription status");
|
|
82
|
+
expect(NO_PROVIDER_ERROR).toContain("standalone Scribe service is retired");
|
|
83
|
+
const message = noProviderErrorFor("whisper-cpp");
|
|
84
|
+
expect(message).toContain("binary is installed");
|
|
85
|
+
expect(message).toContain("not active and ready");
|
|
86
|
+
expect(message).toContain("transcription status");
|
|
87
|
+
expect(message).not.toContain("point SCRIBE_URL");
|
|
88
|
+
expect(message).not.toContain("resolves only a scribe URL");
|
|
285
89
|
});
|
|
286
90
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
test("a runnable local provider → a message that does not contradict the box", () => {
|
|
320
|
-
const d = classifyAutoTranscribe(audio, {
|
|
321
|
-
perVaultEnabled: true,
|
|
322
|
-
getCachedScribeUrlImpl: noScribe,
|
|
323
|
-
localProviderImpl: () => "whisper-cpp",
|
|
324
|
-
});
|
|
325
|
-
expect(d.kind).toBe("unavailable");
|
|
326
|
-
const msg = noProviderErrorFor(d.kind === "unavailable" ? d.localProvider : null);
|
|
327
|
-
|
|
328
|
-
// The three false claims the old string made on such a box.
|
|
329
|
-
expect(msg).not.toContain("no transcription provider configured");
|
|
330
|
-
expect(msg).not.toContain("set TRANSCRIPTION_PROVIDER to a local provider");
|
|
331
|
-
expect(msg).not.toContain("transcription install");
|
|
332
|
-
// And what it must say instead: the local install is fine, this path just
|
|
333
|
-
// doesn't read it, and don't go reinstalling anything.
|
|
334
|
-
expect(msg).toContain("whisper-cpp");
|
|
335
|
-
expect(msg).toContain("SCRIBE_URL");
|
|
336
|
-
expect(msg).toMatch(/does not consult it/);
|
|
337
|
-
expect(msg).toMatch(/Reinstalling whisper-cpp will not change this/);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
test("the decision itself is untouched — this diff moves no behaviour", () => {
|
|
341
|
-
// Same inputs, both local-provider states: still `unavailable` either way.
|
|
342
|
-
// If this ever diverges, the change stopped being diagnostic-only and
|
|
343
|
-
// started deciding what gets transcribed.
|
|
344
|
-
for (const local of [null, "whisper-cpp"]) {
|
|
345
|
-
expect(
|
|
346
|
-
classifyAutoTranscribe(audio, {
|
|
347
|
-
perVaultEnabled: true,
|
|
348
|
-
getCachedScribeUrlImpl: noScribe,
|
|
349
|
-
localProviderImpl: () => local,
|
|
350
|
-
}).kind,
|
|
351
|
-
).toBe("unavailable");
|
|
91
|
+
test("whisper-cpp real readiness requires model plus matching active worker, without Scribe", () => {
|
|
92
|
+
const keys = ["PARACHUTE_HOME", "WHISPER_CPP_BIN_DIR", "TRANSCRIPTION_PROVIDER", "TRANSCRIPTION_MODEL", "SCRIBE_URL"] as const;
|
|
93
|
+
const original = Object.fromEntries(keys.map((key) => [key, process.env[key]]));
|
|
94
|
+
const home = mkdtempSync(join(tmpdir(), "auto-whisper-"));
|
|
95
|
+
const oldWorker = getTranscriptionWorker();
|
|
96
|
+
const oldProvider = getTranscriptionWorkerProvider();
|
|
97
|
+
try {
|
|
98
|
+
process.env.PARACHUTE_HOME = home;
|
|
99
|
+
process.env.WHISPER_CPP_BIN_DIR = join(home, "bin");
|
|
100
|
+
process.env.TRANSCRIPTION_PROVIDER = "whisper-cpp";
|
|
101
|
+
process.env.TRANSCRIPTION_MODEL = "whisper-base.en";
|
|
102
|
+
delete process.env.SCRIBE_URL;
|
|
103
|
+
mkdirSync(join(home, "bin"));
|
|
104
|
+
// Snapshot checks presence only; these are never executed.
|
|
105
|
+
writeFileSync(join(home, "bin", "whisper-cli"), "fixture");
|
|
106
|
+
writeFileSync(join(home, "bin", "ffmpeg"), "fixture");
|
|
107
|
+
const worker = { stop: async () => {}, tick: async () => 0, kick: async () => {} };
|
|
108
|
+
setTranscriptionWorker(worker, "whisper-cpp");
|
|
109
|
+
expect(classifyAutoTranscribe("audio/wav", { perVaultEnabled: true }).kind).toBe("unavailable");
|
|
110
|
+
const models = join(home, "transcription", "models");
|
|
111
|
+
mkdirSync(models, { recursive: true });
|
|
112
|
+
writeFileSync(join(models, findModel("whisper-base.en")!.filename), "fixture");
|
|
113
|
+
expect(classifyAutoTranscribe("audio/wav", { perVaultEnabled: true }).kind).toBe("transcribe");
|
|
114
|
+
setTranscriptionWorker(null);
|
|
115
|
+
expect(classifyAutoTranscribe("audio/wav", { perVaultEnabled: true }).kind).toBe("unavailable");
|
|
116
|
+
} finally {
|
|
117
|
+
setTranscriptionWorker(oldWorker, oldProvider);
|
|
118
|
+
for (const key of keys) {
|
|
119
|
+
if (original[key] === undefined) delete process.env[key]; else process.env[key] = original[key];
|
|
352
120
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
perVaultEnabled: true,
|
|
357
|
-
getCachedScribeUrlImpl: () => "http://127.0.0.1:1943",
|
|
358
|
-
localProviderImpl: () => "whisper-cpp",
|
|
359
|
-
}).kind,
|
|
360
|
-
).toBe("transcribe");
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
test("an installed provider with ffmpeg missing STILL gets the honest message", () => {
|
|
364
|
-
// The trap that cost a rewrite: `snap.ready` also demands ffmpeg, so it
|
|
365
|
-
// varies box to box. A machine with the provider installed and ffmpeg
|
|
366
|
-
// missing is still one where "no transcription provider configured" is
|
|
367
|
-
// false, so the discriminator is INSTALLED, not READY — it answers the
|
|
368
|
-
// question the sentence asks, identically everywhere.
|
|
369
|
-
const msg = noProviderErrorFor("whisper-cpp");
|
|
370
|
-
expect(msg).not.toBe(NO_PROVIDER_ERROR);
|
|
371
|
-
// And it must not over-claim in the other direction: with ffmpeg missing
|
|
372
|
-
// the install is present but not necessarily working, so the message says
|
|
373
|
-
// requests are ROUTED to it and points at status for the real state.
|
|
374
|
-
expect(msg).toMatch(/routed to it/);
|
|
375
|
-
expect(msg).toContain("transcription status");
|
|
376
|
-
expect(msg).not.toMatch(/is fine/);
|
|
377
|
-
});
|
|
121
|
+
rmSync(home, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
378
124
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
125
|
+
test("missing-provider warning is throttled and resets", () => {
|
|
126
|
+
const previous = console.warn;
|
|
127
|
+
const messages: unknown[] = [];
|
|
128
|
+
console.warn = (...args) => { messages.push(args); };
|
|
129
|
+
try {
|
|
130
|
+
_resetNoProviderWarnForTest();
|
|
131
|
+
warnNoTranscriptionProvider(() => 60_000);
|
|
132
|
+
warnNoTranscriptionProvider(() => 60_001);
|
|
133
|
+
expect(messages).toHaveLength(1);
|
|
134
|
+
warnNoTranscriptionProvider(() => 120_000);
|
|
135
|
+
expect(messages).toHaveLength(2);
|
|
136
|
+
_resetNoProviderWarnForTest();
|
|
137
|
+
warnNoTranscriptionProvider(() => 120_001);
|
|
138
|
+
expect(messages).toHaveLength(3);
|
|
139
|
+
} finally { console.warn = previous; _resetNoProviderWarnForTest(); }
|
|
388
140
|
});
|