gentle-pi 4.3.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/assets/agents/gentle-ai-worker.md +1 -1
- package/assets/design-canvas/canvas.css +648 -0
- package/assets/design-canvas/canvas.js +1497 -0
- package/assets/design-canvas/index.html +88 -0
- package/assets/design-canvas/inspector.js +290 -0
- package/assets/orchestrator-delegation-reference.md +2 -2
- package/assets/orchestrator-verification.md +1 -1
- package/assets/orchestrator.md +2 -2
- package/bin/engram-guard +1 -1
- package/bin/gentle-shell.mjs +138 -155
- package/docs/bundled-install.md +47 -42
- package/docs/delegated-verification.md +6 -0
- package/docs/gentle-agents-activity.md +5 -1
- package/docs/gentle-music.md +82 -0
- package/docs/gentle-shell.md +80 -10
- package/docs/install-wizard.md +30 -6
- package/docs/permission-modes.md +7 -2
- package/docs/readme-reference.md +72 -55
- package/docs/session-profile-format.md +26 -0
- package/docs/voice.md +259 -0
- package/docs/yolo-mode.md +17 -2
- package/extensions/ask-user-choice.ts +4 -1
- package/extensions/gentle-agents.ts +187 -40
- package/extensions/gentle-ai.ts +1765 -1185
- package/extensions/gentle-design.ts +158 -0
- package/extensions/gentle-music.ts +354 -0
- package/extensions/gentle-shell.ts +418 -79
- package/extensions/gentle-voice.ts +11 -0
- package/extensions/session-scratch.ts +41 -22
- package/extensions/skill-registry.ts +63 -8
- package/lib/agent-profiles.ts +26 -0
- package/lib/agents-config.ts +35 -0
- package/lib/agents-protocol.ts +57 -1
- package/lib/agents-runner.ts +52 -3
- package/lib/agents-session-transport.ts +101 -6
- package/lib/agents-widget.ts +14 -1
- package/lib/auto-permission-memory.ts +48 -18
- package/lib/auto-permission-runtime.ts +21 -6
- package/lib/clipboard-image.ts +147 -0
- package/lib/command-palette-catalog.ts +3 -2
- package/lib/commit-slice-signal.ts +1 -1
- package/lib/context-backstop.ts +36 -0
- package/lib/design-canvas-server.ts +947 -0
- package/lib/design-comments.ts +437 -0
- package/lib/design-handoffs.ts +313 -0
- package/lib/design-open-browser.ts +58 -0
- package/lib/design-sources.ts +120 -0
- package/lib/external-editor.ts +31 -0
- package/lib/gentle-design.ts +513 -0
- package/lib/gentle-shell-bundled.ts +141 -57
- package/lib/gentle-shell-launcher.ts +11 -4
- package/lib/gentle-shell-startup-update.ts +61 -9
- package/lib/gentle-shell-uninstall.ts +33 -1
- package/lib/image-attachments.ts +150 -0
- package/lib/inprocess-reviewer.ts +33 -2
- package/lib/jobs-sidebar-state.ts +22 -3
- package/lib/jobs-widget.ts +112 -0
- package/lib/nan-provider.ts +3 -0
- package/lib/native-review-cli.ts +11 -4
- package/lib/notification-audio-native.ts +149 -13
- package/lib/notification-audio.ts +4 -0
- package/lib/notification-pulse-worker.ts +82 -18
- package/lib/notification-service.ts +5 -1
- package/lib/orchestrator-presence.ts +78 -8
- package/lib/orchestrator-scope.ts +10 -0
- package/lib/orchestrator-state.ts +11 -3
- package/lib/permission-mode-policy.ts +2 -2
- package/lib/review-host-relay.ts +11 -2
- package/lib/review-reminder-receipt.ts +24 -1
- package/lib/session-profile-append-controller.ts +713 -0
- package/lib/session-profile-disk-reader.ts +182 -0
- package/lib/session-profile-freeze.ts +2 -2
- package/lib/session-profile-persistence.ts +15 -7
- package/lib/session-scratch.ts +52 -3
- package/lib/session-worktree-registry.ts +7 -0
- package/lib/shell-bar.ts +6 -7
- package/lib/shell-changes.ts +81 -5
- package/lib/shell-prompt.ts +4 -4
- package/lib/shell-sidebar-layout.ts +27 -7
- package/lib/shell-todo.ts +1 -1
- package/lib/verifier-admission.ts +5 -4
- package/lib/vim-editor-adapter.ts +14 -10
- package/lib/voice-artifacts.ts +188 -0
- package/lib/voice-command.ts +576 -0
- package/lib/voice-install.ts +455 -0
- package/lib/voice-platform.ts +99 -0
- package/lib/voice-push-to-talk.ts +262 -0
- package/lib/voice-session.ts +460 -0
- package/lib/voice-settings.ts +50 -0
- package/lib/voice-ui.ts +158 -0
- package/lib/yolo-session-policy.ts +58 -22
- package/package.json +1 -1
- package/runtime/gentle-shell-bundled.mjs +140 -56
- package/runtime/gentle-shell-launcher.mjs +11 -4
- package/runtime/gentle-shell-startup-update.mjs +61 -9
- package/runtime/gentle-shell-uninstall.mjs +33 -1
- package/runtime/native-review-cli.mjs +11 -4
- package/scripts/bundled-engram-migration.mjs +0 -6
- package/scripts/bundled-install.mjs +209 -13
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/installer-downloads.mjs +31 -7
- package/scripts/installer-preflight.mjs +6 -4
- package/scripts/installer-windows.mjs +37 -7
- package/scripts/main-channel.mjs +91 -17
- package/scripts/pi-update.mjs +37 -7
- package/scripts/verify-package-files.mjs +26 -2
- package/scripts/voice-worker.cjs +476 -0
- package/skills/design/SKILL.md +110 -0
- package/tests/agent-profiles.test.ts +24 -0
- package/tests/agents-config.test.ts +61 -0
- package/tests/agents-fake-child.ts +8 -1
- package/tests/agents-protocol.test.ts +32 -0
- package/tests/agents-runner.test.ts +226 -5
- package/tests/agents-session-transport.test.ts +53 -3
- package/tests/ask-user-choice.test.ts +30 -0
- package/tests/auto-mode.test.ts +17 -0
- package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
- package/tests/auto-permission-runtime.test.ts +85 -1
- package/tests/autonomous-guard.test.ts +20 -0
- package/tests/bundled-install.test.ts +276 -5
- package/tests/clipboard-image.test.ts +114 -0
- package/tests/command-palette.test.ts +4 -4
- package/tests/context-backstop.test.ts +70 -4
- package/tests/delegate-for-reason-contract.test.ts +2 -1
- package/tests/design-canvas-assets.test.ts +710 -0
- package/tests/design-canvas-server.test.ts +1096 -0
- package/tests/design-comments.test.ts +201 -0
- package/tests/design-handoffs.test.ts +282 -0
- package/tests/design-open-browser.test.ts +70 -0
- package/tests/design-sources.test.ts +132 -0
- package/tests/engram-safety.test.ts +132 -57
- package/tests/external-editor.test.ts +84 -0
- package/tests/fixtures/primary-session-prompt/gentleman.golden.md +3 -3
- package/tests/gentle-agents.test.ts +369 -24
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +51 -51
- package/tests/gentle-ai.test.ts +928 -448
- package/tests/gentle-design.test.ts +1313 -0
- package/tests/gentle-music.test.ts +452 -0
- package/tests/gentle-shell-bundled.test.ts +321 -248
- package/tests/gentle-shell-launcher.test.ts +29 -2
- package/tests/gentle-shell-startup-update.test.ts +77 -1
- package/tests/gentle-shell-uninstall.test.ts +59 -0
- package/tests/gentle-shell.test.ts +227 -27
- package/tests/gentle-todo.test.ts +11 -0
- package/tests/image-attachments.test.ts +119 -0
- package/tests/image-paste-chips.test.ts +236 -0
- package/tests/inprocess-reviewer.test.ts +55 -2
- package/tests/installer-bundles.test.ts +12 -6
- package/tests/installer-downloads.test.ts +32 -0
- package/tests/installer-posix-bootstrap.test.ts +8 -8
- package/tests/installer-preflight.test.ts +16 -0
- package/tests/installer-server.test.ts +2 -2
- package/tests/installer-windows-bootstrap.test.ts +61 -3
- package/tests/installer-windows-diagnostics.test.ts +111 -0
- package/tests/jobs-widget.test.ts +138 -0
- package/tests/main-channel-upgrade.test.ts +232 -2
- package/tests/main-channel.test.ts +140 -21
- package/tests/model-profiles-preservation.test.ts +2 -2
- package/tests/nan-provider.test.ts +36 -0
- package/tests/native-review-capability-contract.test.ts +11 -4
- package/tests/native-review-parity.test.ts +1 -1
- package/tests/notification-audio-native.test.ts +128 -9
- package/tests/notification-customize.test.ts +17 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +2 -1
- package/tests/orchestrator-budget.test.ts +5 -2
- package/tests/orchestrator-presence.test.ts +100 -2
- package/tests/orchestrator-rdd-ownership.test.ts +2 -2
- package/tests/orchestrator-scope.test.ts +14 -0
- package/tests/orchestrator-state.test.ts +29 -0
- package/tests/package-files.test.ts +34 -0
- package/tests/package-manifest.test.ts +6 -6
- package/tests/permission-mode-policy.test.ts +10 -1
- package/tests/pi-update.test.ts +104 -4
- package/tests/rdd-aware-verification-contract.test.ts +1 -1
- package/tests/review-host-relay-routing.test.ts +88 -1
- package/tests/review-host-relay.test.ts +9 -2
- package/tests/review-json-arguments.test.ts +20 -3
- package/tests/review-loadout-rdd-off-sdk.test.ts +23 -2
- package/tests/review-relay-transport-agent.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +31 -1
- package/tests/runtime-harness.mjs +188 -211
- package/tests/session-changes.test.ts +2 -1
- package/tests/session-profile-append-controller-fork.test.ts +458 -0
- package/tests/session-profile-append-controller-pi.test.ts +290 -0
- package/tests/session-profile-append-controller.test.ts +731 -0
- package/tests/session-profile-disk-reader-pi.test.ts +183 -0
- package/tests/session-profile-disk-reader.test.ts +311 -0
- package/tests/session-profile-freeze.test.ts +6 -4
- package/tests/session-scratch.test.ts +68 -2
- package/tests/session-worktree-registry.test.ts +13 -1
- package/tests/shell-bar.test.ts +18 -6
- package/tests/shell-changes.test.ts +148 -1
- package/tests/shell-prompt.test.ts +3 -3
- package/tests/shell-render-cache.test.ts +134 -1
- package/tests/shell-sidebar-layout.test.ts +67 -0
- package/tests/short-check-routing-contract.test.ts +1 -1
- package/tests/skill-collision-prefixes.test.ts +3 -1
- package/tests/skill-registry-watcher.test.ts +17 -5
- package/tests/skill-registry.test.ts +47 -3
- package/tests/task-size-routing-contract.test.ts +1 -1
- package/tests/test-strength-contract.test.ts +1 -1
- package/tests/verifier-admission.test.ts +18 -1
- package/tests/vim-editor-adapter.test.ts +2 -2
- package/tests/voice-command.test.ts +773 -0
- package/tests/voice-install.test.ts +323 -0
- package/tests/voice-platform.test.ts +176 -0
- package/tests/voice-push-to-talk.test.ts +444 -0
- package/tests/voice-session.test.ts +434 -0
- package/tests/voice-settings.test.ts +49 -0
- package/tests/voice-ui.test.ts +97 -0
- package/tests/voice-worker.test.ts +445 -0
- package/tests/weak-model-economy-prompt.test.ts +32 -0
- package/tests/windows-hidden-processes.test.ts +25 -2
- package/tests/yolo-customize.test.ts +5 -1
- package/tests/yolo-mode.test.ts +1 -1
- package/tests/yolo-session-policy-capture.test.ts +104 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { Readable } from "node:stream";
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
// The voice worker (S1-S3) runs in its own process; here its entry function is
|
|
12
|
+
// driven in-process with fake capture and recognizer modules, so no native code
|
|
13
|
+
// or microphone is ever touched. The installed files it validates are real
|
|
14
|
+
// files in an owned scratch directory under tmp/.
|
|
15
|
+
|
|
16
|
+
const repoRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
17
|
+
const { runVoiceWorker } = createRequire(import.meta.url)("../scripts/voice-worker.cjs");
|
|
18
|
+
|
|
19
|
+
const SCHEMA = "gentle.voice/v1";
|
|
20
|
+
const SHERPA_ENTRY = "node_modules/sherpa-onnx-node/non-streaming-asr.js";
|
|
21
|
+
const CPAL_ENTRY = "node_modules/node-cpal/index.js";
|
|
22
|
+
const NATIVE = "node_modules/sherpa-onnx-linux-x64/sherpa-onnx.node";
|
|
23
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
24
|
+
|
|
25
|
+
interface Install {
|
|
26
|
+
runtimeRoot: string;
|
|
27
|
+
modelRoot: string;
|
|
28
|
+
modelFiles: Record<string, { name: string; size: number; sha256: string }>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function install(t: test.TestContext): Install {
|
|
32
|
+
const base = join(repoRoot, "tmp");
|
|
33
|
+
mkdirSync(base, { recursive: true });
|
|
34
|
+
const dir = mkdtempSync(join(base, "voice-worker-"));
|
|
35
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
36
|
+
const runtimeRoot = join(dir, "runtime");
|
|
37
|
+
for (const file of [SHERPA_ENTRY, CPAL_ENTRY, NATIVE]) {
|
|
38
|
+
mkdirSync(dirname(join(runtimeRoot, file)), { recursive: true });
|
|
39
|
+
writeFileSync(join(runtimeRoot, file), `// ${file}\n`);
|
|
40
|
+
}
|
|
41
|
+
const modelRoot = join(dir, "model");
|
|
42
|
+
mkdirSync(modelRoot);
|
|
43
|
+
const modelFiles: Install["modelFiles"] = {};
|
|
44
|
+
for (const [role, name] of [["encoder", "encoder.int8.onnx"], ["decoder", "decoder.int8.onnx"], ["joiner", "joiner.int8.onnx"], ["tokens", "tokens.txt"]]) {
|
|
45
|
+
const data = Buffer.from(`${role} weights`);
|
|
46
|
+
writeFileSync(join(modelRoot, name), data);
|
|
47
|
+
modelFiles[role] = { name, size: data.length, sha256: createHash("sha256").update(data).digest("hex") };
|
|
48
|
+
}
|
|
49
|
+
return { runtimeRoot, modelRoot, modelFiles };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function startRequest(paths: Install, overrides: Record<string, unknown> = {}) {
|
|
53
|
+
return {
|
|
54
|
+
schema: SCHEMA,
|
|
55
|
+
type: "start",
|
|
56
|
+
target: "linux-x64",
|
|
57
|
+
maxRecordingMs: 120_000,
|
|
58
|
+
numThreads: 2,
|
|
59
|
+
runtime: { root: paths.runtimeRoot, files: [SHERPA_ENTRY, CPAL_ENTRY, NATIVE] },
|
|
60
|
+
model: { root: paths.modelRoot, files: paths.modelFiles },
|
|
61
|
+
...overrides,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface FakeStream {
|
|
66
|
+
options: { config: { sampleRate: number; channels: number; sampleFormat: string }; onData(data: ArrayBufferView): void; onError(error: unknown): void; autoStart: boolean };
|
|
67
|
+
closes: number;
|
|
68
|
+
close(): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface FakeOptions {
|
|
72
|
+
device?: unknown;
|
|
73
|
+
defaults?: { sampleRate: number; channels: number; sampleFormat: string };
|
|
74
|
+
formats?: string[];
|
|
75
|
+
loadError?: string;
|
|
76
|
+
recognizerError?: string;
|
|
77
|
+
text?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function harness(t: test.TestContext, fake: FakeOptions = {}, io: Record<string, unknown> = {}) {
|
|
81
|
+
const paths = install(t);
|
|
82
|
+
const input = new EventEmitter();
|
|
83
|
+
const messages: Array<Record<string, unknown>> = [];
|
|
84
|
+
const raw: string[] = [];
|
|
85
|
+
const loaded: string[] = [];
|
|
86
|
+
const streams: FakeStream[] = [];
|
|
87
|
+
const recognizers: Array<{ config: any; waves: Array<{ sampleRate: number; samples: Float32Array }>; decodes: number }> = [];
|
|
88
|
+
let exitCode: number | undefined;
|
|
89
|
+
const convenience = {
|
|
90
|
+
getDefaultInputDevice: () => ("device" in fake ? fake.device : { deviceId: "default-input" }),
|
|
91
|
+
getDefaultInputConfig: () => fake.defaults ?? { sampleRate: 48000, channels: 2, sampleFormat: "i16" },
|
|
92
|
+
getSupportedFormats: () => fake.formats ?? ["i16"],
|
|
93
|
+
createInputStream: async (options: FakeStream["options"]) => {
|
|
94
|
+
const stream: FakeStream = { options, closes: 0, close: async () => void stream.closes++ };
|
|
95
|
+
streams.push(stream);
|
|
96
|
+
return stream;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
class OfflineRecognizer {
|
|
100
|
+
config: any;
|
|
101
|
+
waves: Array<{ sampleRate: number; samples: Float32Array }> = [];
|
|
102
|
+
decodes = 0;
|
|
103
|
+
constructor(config: unknown) {
|
|
104
|
+
if (fake.recognizerError) throw new Error(fake.recognizerError);
|
|
105
|
+
this.config = config;
|
|
106
|
+
recognizers.push(this);
|
|
107
|
+
}
|
|
108
|
+
createStream() {
|
|
109
|
+
return { acceptWaveform: (wave: { sampleRate: number; samples: Float32Array }) => this.waves.push(wave) };
|
|
110
|
+
}
|
|
111
|
+
decode() {
|
|
112
|
+
this.decodes++;
|
|
113
|
+
}
|
|
114
|
+
getResult() {
|
|
115
|
+
return { text: fake.text ?? " hello world " };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const done: Promise<void> = runVoiceWorker({
|
|
119
|
+
input,
|
|
120
|
+
write: (line: string) => {
|
|
121
|
+
raw.push(line);
|
|
122
|
+
messages.push(JSON.parse(line));
|
|
123
|
+
},
|
|
124
|
+
exit: (code: number) => {
|
|
125
|
+
exitCode = code;
|
|
126
|
+
},
|
|
127
|
+
loadModule: (file: string) => {
|
|
128
|
+
loaded.push(file);
|
|
129
|
+
if (fake.loadError) throw new Error(fake.loadError);
|
|
130
|
+
if (file === join(paths.runtimeRoot, ...SHERPA_ENTRY.split("/"))) return { OfflineRecognizer };
|
|
131
|
+
if (file === join(paths.runtimeRoot, ...CPAL_ENTRY.split("/"))) return { convenience };
|
|
132
|
+
throw new Error(`unexpected module ${file}`);
|
|
133
|
+
},
|
|
134
|
+
platform: "linux",
|
|
135
|
+
arch: "x64",
|
|
136
|
+
nodeVersion: "22.12.0",
|
|
137
|
+
uid,
|
|
138
|
+
...io,
|
|
139
|
+
});
|
|
140
|
+
const send = (message: unknown) => input.emit("data", `${typeof message === "string" ? message : JSON.stringify(message)}\n`);
|
|
141
|
+
const waitFor = async (predicate: () => boolean, label = "condition") => {
|
|
142
|
+
for (let i = 0; i < 500 && !predicate(); i++) await new Promise((resolve) => setTimeout(resolve, 2));
|
|
143
|
+
assert.ok(predicate(), `timed out waiting for ${label}; messages: ${JSON.stringify(messages)}`);
|
|
144
|
+
};
|
|
145
|
+
const types = () => messages.map((message) => message.type);
|
|
146
|
+
return { paths, input, messages, raw, loaded, streams, recognizers, done, send, waitFor, types, exitCode: () => exitCode };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const interleaved = (frames: number, channels: number, value: (frame: number, channel: number) => number) => {
|
|
150
|
+
const data = new Int16Array(frames * channels);
|
|
151
|
+
for (let frame = 0; frame < frames; frame++) for (let channel = 0; channel < channels; channel++) data[frame * channels + channel] = value(frame, channel);
|
|
152
|
+
return data;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
test("records multichannel audio, resamples it to 16 kHz mono and closes its retained stream once on stop", async (t) => {
|
|
156
|
+
const worker = harness(t);
|
|
157
|
+
worker.send(startRequest(worker.paths));
|
|
158
|
+
await worker.waitFor(() => worker.types().includes("recording"));
|
|
159
|
+
const [stream] = worker.streams;
|
|
160
|
+
assert.deepEqual(stream!.options.config, { sampleRate: 48000, channels: 2, sampleFormat: "i16" });
|
|
161
|
+
assert.equal(stream!.options.autoStart, true);
|
|
162
|
+
|
|
163
|
+
// Left at half scale, right silent: the mono downmix is a quarter scale.
|
|
164
|
+
stream!.options.onData(interleaved(4800, 2, (_frame, channel) => (channel === 0 ? 16384 : 0)));
|
|
165
|
+
stream!.options.onData(interleaved(4800, 2, (_frame, channel) => (channel === 0 ? 16384 : 0)));
|
|
166
|
+
worker.send({ schema: SCHEMA, type: "stop" });
|
|
167
|
+
await worker.done;
|
|
168
|
+
stream!.options.onData(interleaved(4800, 2, () => 32767));
|
|
169
|
+
|
|
170
|
+
assert.deepEqual(worker.types(), ["recording", "transcribing", "result"]);
|
|
171
|
+
assert.deepEqual(worker.messages.at(-1), { schema: SCHEMA, type: "result", text: "hello world", truncated: false });
|
|
172
|
+
assert.equal(stream!.closes, 1);
|
|
173
|
+
assert.equal(worker.exitCode(), 0);
|
|
174
|
+
const config = worker.recognizers[0]!.config;
|
|
175
|
+
assert.equal(config.modelConfig.modelType, "nemo_transducer");
|
|
176
|
+
assert.equal(config.modelConfig.transducer.encoder, join(worker.paths.modelRoot, "encoder.int8.onnx"));
|
|
177
|
+
assert.equal(config.modelConfig.tokens, join(worker.paths.modelRoot, "tokens.txt"));
|
|
178
|
+
assert.deepEqual(config.featConfig, { sampleRate: 16000, featureDim: 80 });
|
|
179
|
+
const [wave] = worker.recognizers[0]!.waves;
|
|
180
|
+
assert.equal(wave!.sampleRate, 16000);
|
|
181
|
+
assert.equal(wave!.samples.length, 3200);
|
|
182
|
+
assert.ok(wave!.samples.every((sample) => Math.abs(sample - 0.25) < 1e-6));
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("prefers float input and transcribes on its own when the recording limit is reached", async (t) => {
|
|
186
|
+
const worker = harness(t, { defaults: { sampleRate: 8000, channels: 1, sampleFormat: "i16" }, formats: ["i16", "f32"] });
|
|
187
|
+
worker.send(startRequest(worker.paths, { maxRecordingMs: 1000 }));
|
|
188
|
+
await worker.waitFor(() => worker.types().includes("recording"));
|
|
189
|
+
const [stream] = worker.streams;
|
|
190
|
+
assert.equal(stream!.options.config.sampleFormat, "f32");
|
|
191
|
+
stream!.options.onData(new Float32Array(6000).fill(0.5));
|
|
192
|
+
stream!.options.onData(new Float32Array(6000).fill(0.5));
|
|
193
|
+
await worker.done;
|
|
194
|
+
assert.deepEqual(worker.types(), ["recording", "transcribing", "result"]);
|
|
195
|
+
assert.equal(worker.messages.at(-1)!.truncated, true);
|
|
196
|
+
// 8000 samples kept at 8 kHz, upsampled to one second at 16 kHz.
|
|
197
|
+
assert.equal(worker.recognizers[0]!.waves[0]!.samples.length, 16000);
|
|
198
|
+
assert.equal(stream!.closes, 1);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("cancel closes capture, discards the audio and reports nothing", async (t) => {
|
|
202
|
+
const worker = harness(t);
|
|
203
|
+
worker.send(startRequest(worker.paths));
|
|
204
|
+
await worker.waitFor(() => worker.types().includes("recording"));
|
|
205
|
+
const [stream] = worker.streams;
|
|
206
|
+
stream!.options.onData(interleaved(9600, 2, () => 1000));
|
|
207
|
+
worker.send({ schema: SCHEMA, type: "cancel" });
|
|
208
|
+
await worker.done;
|
|
209
|
+
stream!.options.onData(interleaved(9600, 2, () => 1000));
|
|
210
|
+
worker.send({ schema: SCHEMA, type: "stop" });
|
|
211
|
+
assert.deepEqual(worker.types(), ["recording"]);
|
|
212
|
+
assert.equal(worker.recognizers.length, 0, "the recognizer is never created for a cancelled session");
|
|
213
|
+
assert.ok(!worker.loaded.some((file) => file.endsWith("non-streaming-asr.js")));
|
|
214
|
+
assert.equal(stream!.closes, 1);
|
|
215
|
+
assert.equal(worker.exitCode(), 0);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("a closed stdin ends the session like a cancel", async (t) => {
|
|
219
|
+
const worker = harness(t);
|
|
220
|
+
worker.send(startRequest(worker.paths));
|
|
221
|
+
await worker.waitFor(() => worker.types().includes("recording"));
|
|
222
|
+
worker.input.emit("end");
|
|
223
|
+
await worker.done;
|
|
224
|
+
assert.deepEqual(worker.types(), ["recording"]);
|
|
225
|
+
assert.equal(worker.streams[0]!.closes, 1);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("rejects malformed and unknown requests before loading anything", async (t) => {
|
|
229
|
+
const cases: Array<(paths: Install) => unknown> = [
|
|
230
|
+
() => "not json",
|
|
231
|
+
(paths) => ({ ...startRequest(paths), extra: true }),
|
|
232
|
+
(paths) => startRequest(paths, { runtime: { root: "relative/runtime", files: [SHERPA_ENTRY, CPAL_ENTRY] } }),
|
|
233
|
+
(paths) => startRequest(paths, { runtime: { root: paths.runtimeRoot, files: [SHERPA_ENTRY] } }),
|
|
234
|
+
(paths) => startRequest(paths, { runtime: { root: paths.runtimeRoot, files: [SHERPA_ENTRY, CPAL_ENTRY, "../outside.js"] } }),
|
|
235
|
+
(paths) => startRequest(paths, { target: "win32-arm64" }),
|
|
236
|
+
(paths) => startRequest(paths, { maxRecordingMs: 10 }),
|
|
237
|
+
() => ({ schema: SCHEMA, type: "stop" }),
|
|
238
|
+
];
|
|
239
|
+
for (const [index, request] of cases.entries()) {
|
|
240
|
+
await t.test(`case ${index}`, async (st) => {
|
|
241
|
+
const worker = harness(st);
|
|
242
|
+
worker.send(request(worker.paths));
|
|
243
|
+
await worker.done;
|
|
244
|
+
assert.deepEqual(worker.messages, [{ schema: SCHEMA, type: "error", code: "invalid-request" }]);
|
|
245
|
+
assert.deepEqual(worker.loaded, []);
|
|
246
|
+
assert.equal(worker.exitCode(), 1);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("refuses runtime links, missing runtime files and a foreign target before native loading", async (t) => {
|
|
252
|
+
const cases: Array<[string, (paths: Install) => Record<string, unknown> | void, string]> = [
|
|
253
|
+
["symlinked runtime file", (paths) => {
|
|
254
|
+
rmSync(join(paths.runtimeRoot, NATIVE));
|
|
255
|
+
symlinkSync(join(paths.modelRoot, "tokens.txt"), join(paths.runtimeRoot, NATIVE));
|
|
256
|
+
}, "runtime-invalid"],
|
|
257
|
+
["missing runtime file", (paths) => rmSync(join(paths.runtimeRoot, CPAL_ENTRY)), "runtime-invalid"],
|
|
258
|
+
["other target", () => ({ target: "darwin-arm64" }), "runtime-invalid"],
|
|
259
|
+
];
|
|
260
|
+
for (const [name, tamper, code] of cases) {
|
|
261
|
+
await t.test(name, async (st) => {
|
|
262
|
+
const worker = harness(st);
|
|
263
|
+
const overrides = (tamper(worker.paths) as Record<string, unknown> | undefined) ?? {};
|
|
264
|
+
worker.send(startRequest(worker.paths, overrides));
|
|
265
|
+
await worker.done;
|
|
266
|
+
assert.deepEqual(worker.messages, [{ schema: SCHEMA, type: "error", code }]);
|
|
267
|
+
assert.deepEqual(worker.loaded, []);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("reports native load failures by code only, never their private detail", async (t) => {
|
|
273
|
+
for (const [fake, code] of [[{ loadError: "dlopen failed: /home/someone/.secret/voice/sherpa-onnx.node TOKEN=abc" }, "runtime-load"], [{ recognizerError: "cannot read /home/someone/model" }, "model-load"]] as const) {
|
|
274
|
+
await t.test(code, async (st) => {
|
|
275
|
+
const worker = harness(st, fake);
|
|
276
|
+
worker.send(startRequest(worker.paths));
|
|
277
|
+
if (code === "model-load") {
|
|
278
|
+
// The recognizer is loaded only after stop.
|
|
279
|
+
await worker.waitFor(() => worker.types().includes("recording"));
|
|
280
|
+
worker.streams[0]!.options.onData(interleaved(4800, 2, () => 1000));
|
|
281
|
+
worker.send({ schema: SCHEMA, type: "stop" });
|
|
282
|
+
}
|
|
283
|
+
await worker.done;
|
|
284
|
+
assert.deepEqual(worker.messages.at(-1), { schema: SCHEMA, type: "error", code });
|
|
285
|
+
assert.deepEqual(worker.types(), code === "model-load" ? ["recording", "transcribing", "error"] : ["error"]);
|
|
286
|
+
assert.ok(!worker.raw.join("").includes("someone"));
|
|
287
|
+
assert.ok(!worker.raw.join("").includes(worker.paths.runtimeRoot));
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("reports a missing microphone, an unusable format and empty audio", async (t) => {
|
|
293
|
+
const cases: Array<[FakeOptions, string, boolean]> = [
|
|
294
|
+
[{ device: null }, "no-input-device", false],
|
|
295
|
+
[{ defaults: { sampleRate: 48000, channels: 1, sampleFormat: "i64" }, formats: ["i64"] }, "unsupported-input", false],
|
|
296
|
+
[{}, "empty-audio", true],
|
|
297
|
+
];
|
|
298
|
+
for (const [fake, code, stop] of cases) {
|
|
299
|
+
await t.test(code, async (st) => {
|
|
300
|
+
const worker = harness(st, fake);
|
|
301
|
+
worker.send(startRequest(worker.paths));
|
|
302
|
+
if (stop) {
|
|
303
|
+
await worker.waitFor(() => worker.types().includes("recording"));
|
|
304
|
+
worker.streams[0]!.options.onData(interleaved(100, 2, () => 1000));
|
|
305
|
+
worker.send({ schema: SCHEMA, type: "stop" });
|
|
306
|
+
}
|
|
307
|
+
await worker.done;
|
|
308
|
+
assert.deepEqual(worker.messages.at(-1), { schema: SCHEMA, type: "error", code });
|
|
309
|
+
assert.equal(worker.exitCode(), 1);
|
|
310
|
+
for (const stream of worker.streams) assert.equal(stream.closes, 1);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("refuses to run on Node older than 22", async (t) => {
|
|
316
|
+
const worker = harness(t, {}, { nodeVersion: "20.18.0" });
|
|
317
|
+
await worker.done;
|
|
318
|
+
assert.deepEqual(worker.messages, [{ schema: SCHEMA, type: "error", code: "node-version" }]);
|
|
319
|
+
assert.deepEqual(worker.loaded, []);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// --- capture before model verification (T4) -----------------------------------
|
|
323
|
+
|
|
324
|
+
const isSherpa = (file: string) => file.endsWith("non-streaming-asr.js");
|
|
325
|
+
const isCpal = (file: string) => file.endsWith(join("node-cpal", "index.js"));
|
|
326
|
+
const settle = () => new Promise((resolve) => setTimeout(resolve, 20));
|
|
327
|
+
|
|
328
|
+
// Reads model files through streams that deliver nothing until released, so a
|
|
329
|
+
// test controls exactly when model verification can finish.
|
|
330
|
+
function gatedReads() {
|
|
331
|
+
let release!: () => void;
|
|
332
|
+
const gate = new Promise<void>((resolve) => { release = resolve; });
|
|
333
|
+
const opened: string[] = [];
|
|
334
|
+
const openRead = (file: string) => {
|
|
335
|
+
opened.push(file);
|
|
336
|
+
return Readable.from((async function* () {
|
|
337
|
+
await gate;
|
|
338
|
+
yield readFileSync(file);
|
|
339
|
+
})());
|
|
340
|
+
};
|
|
341
|
+
return { openRead, opened, release };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
test("capture opens and reports recording before the model is verified; sherpa loads only after stop and verification", async (t) => {
|
|
345
|
+
const reads = gatedReads();
|
|
346
|
+
const worker = harness(t, {}, { openRead: reads.openRead });
|
|
347
|
+
worker.send(startRequest(worker.paths));
|
|
348
|
+
await worker.waitFor(() => worker.types().includes("recording"));
|
|
349
|
+
assert.equal(worker.streams.length, 1, "capture is open");
|
|
350
|
+
assert.equal(worker.loaded.length, 1);
|
|
351
|
+
assert.ok(isCpal(worker.loaded[0]!), "only node-cpal is loaded while recording");
|
|
352
|
+
// Recording is reported before verification starts; the first digest read
|
|
353
|
+
// opens asynchronously while recording, so wait for it instead of racing
|
|
354
|
+
// the model tree walk (#2435).
|
|
355
|
+
await worker.waitFor(() => reads.opened.length >= 1, "model digests are streamed while recording");
|
|
356
|
+
// Audio captured during verification is kept.
|
|
357
|
+
worker.streams[0]!.options.onData(interleaved(4800, 2, (_frame, channel) => (channel === 0 ? 16384 : 0)));
|
|
358
|
+
worker.send({ schema: SCHEMA, type: "stop" });
|
|
359
|
+
await worker.waitFor(() => worker.types().includes("transcribing"));
|
|
360
|
+
await settle();
|
|
361
|
+
assert.equal(worker.streams[0]!.closes, 1, "capture stops at once on stop");
|
|
362
|
+
assert.ok(!worker.loaded.some(isSherpa), "no sherpa load before every digest matched");
|
|
363
|
+
assert.equal(worker.recognizers.length, 0);
|
|
364
|
+
reads.release();
|
|
365
|
+
await worker.done;
|
|
366
|
+
assert.deepEqual(worker.types(), ["recording", "transcribing", "result"]);
|
|
367
|
+
assert.ok(isSherpa(worker.loaded[1]!));
|
|
368
|
+
assert.equal(reads.opened.length, 4, "every model file was hashed");
|
|
369
|
+
assert.equal(worker.recognizers[0]!.waves[0]!.samples.length, 1600);
|
|
370
|
+
assert.equal(worker.exitCode(), 0);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("a model digest mismatch found while recording closes capture, reports model-invalid and never loads sherpa", async (t) => {
|
|
374
|
+
const worker = harness(t);
|
|
375
|
+
writeFileSync(join(worker.paths.modelRoot, "joiner.int8.onnx"), "JOINER weights");
|
|
376
|
+
worker.send(startRequest(worker.paths));
|
|
377
|
+
await worker.done;
|
|
378
|
+
assert.deepEqual(worker.messages, [{ schema: SCHEMA, type: "recording" }, { schema: SCHEMA, type: "error", code: "model-invalid" }]);
|
|
379
|
+
assert.equal(worker.streams[0]!.closes, 1);
|
|
380
|
+
assert.ok(!worker.loaded.some(isSherpa));
|
|
381
|
+
assert.equal(worker.recognizers.length, 0);
|
|
382
|
+
assert.equal(worker.exitCode(), 1);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("an invalid model tree is reported while recording and never loads sherpa", async (t) => {
|
|
386
|
+
const worker = harness(t);
|
|
387
|
+
symlinkSync(worker.paths.runtimeRoot, join(worker.paths.modelRoot, "linked"));
|
|
388
|
+
worker.send(startRequest(worker.paths));
|
|
389
|
+
await worker.done;
|
|
390
|
+
assert.deepEqual(worker.types(), ["recording", "error"]);
|
|
391
|
+
assert.equal(worker.messages.at(-1)!.code, "model-invalid");
|
|
392
|
+
assert.ok(!worker.loaded.some(isSherpa));
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("a mismatch found after stop reports model-invalid instead of a transcript", async (t) => {
|
|
396
|
+
const reads = gatedReads();
|
|
397
|
+
const worker = harness(t, {}, { openRead: reads.openRead });
|
|
398
|
+
writeFileSync(join(worker.paths.modelRoot, "joiner.int8.onnx"), "JOINER weights");
|
|
399
|
+
worker.send(startRequest(worker.paths));
|
|
400
|
+
await worker.waitFor(() => worker.types().includes("recording"));
|
|
401
|
+
worker.streams[0]!.options.onData(interleaved(4800, 2, () => 1000));
|
|
402
|
+
worker.send({ schema: SCHEMA, type: "stop" });
|
|
403
|
+
await worker.waitFor(() => worker.types().includes("transcribing"));
|
|
404
|
+
reads.release();
|
|
405
|
+
await worker.done;
|
|
406
|
+
assert.deepEqual(worker.messages.at(-1), { schema: SCHEMA, type: "error", code: "model-invalid" });
|
|
407
|
+
assert.ok(!worker.loaded.some(isSherpa));
|
|
408
|
+
assert.equal(worker.recognizers.length, 0);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("cancel during model verification discards everything, stops hashing and never loads sherpa", async (t) => {
|
|
412
|
+
const reads = gatedReads();
|
|
413
|
+
const worker = harness(t, {}, { openRead: reads.openRead });
|
|
414
|
+
worker.send(startRequest(worker.paths));
|
|
415
|
+
await worker.waitFor(() => worker.types().includes("recording"));
|
|
416
|
+
worker.streams[0]!.options.onData(interleaved(4800, 2, () => 1000));
|
|
417
|
+
// Cancel must interrupt a digest that is actually in flight: wait for the
|
|
418
|
+
// first model read to open, otherwise an early cancel is indistinguishable
|
|
419
|
+
// from verification that never started (#2435).
|
|
420
|
+
await worker.waitFor(() => reads.opened.length === 1, "the first digest read is in flight");
|
|
421
|
+
worker.send({ schema: SCHEMA, type: "cancel" });
|
|
422
|
+
await worker.done;
|
|
423
|
+
reads.release();
|
|
424
|
+
await settle();
|
|
425
|
+
worker.send({ schema: SCHEMA, type: "stop" });
|
|
426
|
+
await settle();
|
|
427
|
+
assert.deepEqual(worker.types(), ["recording"]);
|
|
428
|
+
assert.equal(worker.streams[0]!.closes, 1);
|
|
429
|
+
assert.ok(!worker.loaded.some(isSherpa));
|
|
430
|
+
assert.equal(reads.opened.length, 1, "no further model file is opened after cancel");
|
|
431
|
+
assert.equal(worker.exitCode(), 0);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("the recording limit during verification transcribes once the model is verified", async (t) => {
|
|
435
|
+
const reads = gatedReads();
|
|
436
|
+
const worker = harness(t, { defaults: { sampleRate: 8000, channels: 1, sampleFormat: "f32" }, formats: ["f32"] }, { openRead: reads.openRead });
|
|
437
|
+
worker.send(startRequest(worker.paths, { maxRecordingMs: 1000 }));
|
|
438
|
+
await worker.waitFor(() => worker.types().includes("recording"));
|
|
439
|
+
worker.streams[0]!.options.onData(new Float32Array(9000).fill(0.5));
|
|
440
|
+
await worker.waitFor(() => worker.types().includes("transcribing"));
|
|
441
|
+
assert.ok(!worker.loaded.some(isSherpa));
|
|
442
|
+
reads.release();
|
|
443
|
+
await worker.done;
|
|
444
|
+
assert.deepEqual(worker.messages.at(-1), { schema: SCHEMA, type: "result", text: "hello world", truncated: true });
|
|
445
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { __testing } from "../extensions/gentle-ai.ts";
|
|
6
|
+
|
|
7
|
+
// odd/tasks/weak-model-economy.md S2-S4: bench C-econ and C-risk wording, asserted in the
|
|
8
|
+
// rendered primary-session prompt (S1, the todo block, is covered in gentle-todo.test.ts).
|
|
9
|
+
|
|
10
|
+
const read = (relative: string): string => readFileSync(join(import.meta.dirname, "..", relative), "utf8");
|
|
11
|
+
|
|
12
|
+
test("S2: the differential check runs only when shared code an unnamed command uses changes", () => {
|
|
13
|
+
const gated = "Only when your diff changes shared code (options, parsers, helpers) that an existing command the request did not name also uses, run one differential check before finishing, as a single script: for each such command,";
|
|
14
|
+
for (const persona of ["gentleman", "neutral"] as const) {
|
|
15
|
+
const prompt = __testing.buildGentlePrompt(persona);
|
|
16
|
+
assert.ok(prompt.includes(gated), `${persona} prompt must gate the differential check`);
|
|
17
|
+
assert.ok(!prompt.includes("Before finishing, run a differential check"), `${persona} prompt keeps the ungated check`);
|
|
18
|
+
}
|
|
19
|
+
const worker = read("assets/agents/gentle-ai-worker.md");
|
|
20
|
+
assert.ok(worker.includes(gated), "worker PRESERVE step must gate the differential check");
|
|
21
|
+
assert.ok(!worker.includes("Before finishing, run a differential check"), "worker keeps the ungated check");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("S3: the small path reads one batch and runs its checks in one command", () => {
|
|
25
|
+
const prompt = __testing.buildGentlePrompt("gentleman");
|
|
26
|
+
assert.ok(prompt.includes("Small path: inline (parent; `bash_background` policy); read only what you change and its helpers, in one batch; observe RED inline before the fix; run the focused test and the suite inline in one command."));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("S4: high-risk item 1 covers only data someone cannot rebuild", () => {
|
|
30
|
+
const prompt = __testing.buildGentlePrompt("gentleman");
|
|
31
|
+
assert.ok(prompt.includes("(1) data or irreversible effects that cannot be rebuilt (migrations, rewriting or deleting stored data, format changes, writing data without validation; not saving new records, nor a dev or test database a seed or backup rebuilds);"));
|
|
32
|
+
});
|
|
@@ -163,6 +163,9 @@ async function captureOwnedRoutes(t: TestContext): Promise<{ captures: Capture[]
|
|
|
163
163
|
const fixtures = await import(`./review-test-fixtures.ts${query}`);
|
|
164
164
|
const publication = await import(`../lib/review-publication-gate.ts${query}`);
|
|
165
165
|
const gentleAi = await import(`../extensions/gentle-ai.ts${query}`);
|
|
166
|
+
const sliceSignal = await import(`../lib/commit-slice-signal.ts${query}`);
|
|
167
|
+
const scratch = await import(`../lib/session-scratch.ts${query}`);
|
|
168
|
+
const skills = await import(`../extensions/skill-registry.ts${query}`);
|
|
166
169
|
|
|
167
170
|
const traces: Trace[] = [];
|
|
168
171
|
const graph = codegraph.createCodeGraphTool();
|
|
@@ -256,6 +259,22 @@ async function captureOwnedRoutes(t: TestContext): Promise<{ captures: Capture[]
|
|
|
256
259
|
{ kind: "spawnSync", command: "gh", args: ["api", "repos/{owner}/{repo}/commits/ffffffffffffffffffffffffffffffffffffffff/status", "--jq", ".state"] },
|
|
257
260
|
]);
|
|
258
261
|
assert.deepEqual(ci, { proven: false, status: null });
|
|
262
|
+
// Routes reached through their default runners, so the launch options under
|
|
263
|
+
// test are the ones production actually uses.
|
|
264
|
+
await trace(captures, traces, "commit slice report", () => sliceSignal.commitSliceReport(cwd), [
|
|
265
|
+
{ kind: "execFile", command: "git", args: ["symbolic-ref", "-q", "--short", "HEAD"] },
|
|
266
|
+
{ kind: "execFile", command: "git", args: ["symbolic-ref", "-q", "--short", "refs/remotes/origin/HEAD"] },
|
|
267
|
+
]);
|
|
268
|
+
const artifact = await trace(captures, traces, "scratch artifact check", () => scratch.checkArtifact({ kind: "clone", path: cwd, command: "git clone", recordedAt: "2026-01-01T00:00:00.000Z" }, []), [
|
|
269
|
+
{ kind: "execFile", command: "git", args: ["rev-parse", "--show-toplevel"] },
|
|
270
|
+
]);
|
|
271
|
+
assert.equal(artifact.state, "kept", "an unrelated top-level keeps the recorded directory");
|
|
272
|
+
await trace(captures, traces, "profile declaration sharing", () => gentleAi.repoDeclarationSharing(cwd, join(cwd, "profile.json")), [
|
|
273
|
+
{ kind: "execFileSync", command: "git", args: ["check-ignore", "--quiet", "profile.json"] },
|
|
274
|
+
]);
|
|
275
|
+
await trace(captures, traces, "skill registry tracking probe", () => skills.automaticWriteWarning(cwd, [".atl/.gitignore"]), [
|
|
276
|
+
{ kind: "execFile", command: "git", args: ["ls-files", "--cached", "-z", "--", ".atl/.gitignore"] },
|
|
277
|
+
]);
|
|
259
278
|
return { captures, traces };
|
|
260
279
|
} finally {
|
|
261
280
|
cp.execFileSync = original.execFileSync;
|
|
@@ -286,7 +305,11 @@ test("owned public adapters pass windowsHide at every mapped Node child-process
|
|
|
286
305
|
"publication advertised object",
|
|
287
306
|
"publication remote head",
|
|
288
307
|
"publication GH CI",
|
|
289
|
-
|
|
308
|
+
"commit slice report",
|
|
309
|
+
"scratch artifact check",
|
|
310
|
+
"profile declaration sharing",
|
|
311
|
+
"skill registry tracking probe",
|
|
312
|
+
], "all sixteen mapped public actions must complete before flag assertions");
|
|
290
313
|
for (const trace of traces) {
|
|
291
314
|
for (const capture of trace.captures) {
|
|
292
315
|
assert.equal(capture.options.windowsHide, true, `${trace.label}: ${capture.kind} ${capture.command} must hide Windows consoles`);
|
|
@@ -302,7 +325,7 @@ test("external editor remains the explicit interactive exemption", async () => {
|
|
|
302
325
|
received = options;
|
|
303
326
|
return { status: 0 };
|
|
304
327
|
}) as never, process.cwd()), true);
|
|
305
|
-
assert.equal(received?.shell,
|
|
328
|
+
assert.equal(received?.shell, false, "interactive editors must not interpret paths through a shell");
|
|
306
329
|
assert.equal(received?.stdio, "inherit");
|
|
307
330
|
assert.equal(received?.windowsHide, undefined, "the interactive editor intentionally remains unhidden");
|
|
308
331
|
});
|
|
@@ -116,12 +116,15 @@ test("Customize selects Auto for this session and saves either elevated default
|
|
|
116
116
|
view.handleInput("j");view.handleInput("j");view.render(140);
|
|
117
117
|
assert.match(text(view),/Session permissions: AUTO/);
|
|
118
118
|
await h.apply("\r");assert.equal(await h.controller().mode(h.ctx),"auto");
|
|
119
|
-
view.handleInput("j");view.handleInput("j");view.handleInput("j");view.render(140);
|
|
119
|
+
view.handleInput("j");view.handleInput("j");view.handleInput("j");view.handleInput("j");view.render(140);
|
|
120
120
|
assert.match(text(view),/Default permissions: AUTO/);
|
|
121
121
|
await h.apply("\r");assert.equal(h.controller().defaultMode(),"auto");assert.equal(await h.controller().mode(h.ctx),"auto");
|
|
122
122
|
view.handleInput("j");view.render(140);await h.apply("\r");
|
|
123
123
|
assert.equal(h.controller().defaultMode(),"yolo");assert.equal(await h.controller().mode(h.ctx),"auto");
|
|
124
124
|
assert.equal(JSON.parse(readFileSync(join(h.home,"permission-mode.json"),"utf8")).mode,"yolo");
|
|
125
|
+
view.handleInput("j");view.render(140);await h.apply("\r");
|
|
126
|
+
assert.equal(h.controller().defaultMode(),"super-yolo");assert.equal(await h.controller().mode(h.ctx),"auto");
|
|
127
|
+
assert.equal(JSON.parse(readFileSync(join(h.home,"permission-mode.json"),"utf8")).mode,"super-yolo");
|
|
125
128
|
for(const width of [1,20,60,140])assert.ok(view.render(width).every(line=>visibleWidth(line)<=width));
|
|
126
129
|
open.close();await open.result;
|
|
127
130
|
});
|
|
@@ -269,3 +272,4 @@ test("real Theme and existing component obey narrow dimensions with invisible co
|
|
|
269
272
|
assert.equal(await h.controller().active(h.ctx), false);
|
|
270
273
|
open.close(); await open.result;
|
|
271
274
|
});
|
|
275
|
+
|
package/tests/yolo-mode.test.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { YOLO_STATUS_TEXT } from "../lib/yolo-session-policy.ts";
|
|
|
11
11
|
const ON_TEXT = "🚀 YOLO ON 🔥 — destructive confirmations remain";
|
|
12
12
|
const USAGE = "Use /gentle:yolo enable|disable|status. State unchanged.";
|
|
13
13
|
const STALE_MENU = "YOLO unchanged — the session changed while the menu was open.";
|
|
14
|
-
const menuTitle = (state: "ON" | "OFF") => `🚀 Gentle YOLO 🔥 —
|
|
14
|
+
const menuTitle = (state: "ON" | "OFF") => `🚀 Gentle YOLO 🔥 — standing permission (current: ${state})`;
|
|
15
15
|
|
|
16
16
|
// An empty env would resolve the developer's real ~/.pi/gentle-ai and inherit its persisted mode default.
|
|
17
17
|
const isolatedConfigHomes: string[] = [];
|