gsd-pi 2.45.0-dev.6b9da3e → 2.45.0-dev.e0ee972
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/dist/help-text.js +1 -1
- package/dist/loader.js +34 -0
- package/dist/resources/extensions/gsd/auto/phases.js +16 -10
- package/dist/resources/extensions/gsd/auto/run-unit.js +6 -3
- package/dist/resources/extensions/gsd/auto-worktree.js +5 -4
- package/dist/resources/extensions/gsd/auto.js +4 -5
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +13 -12
- package/dist/resources/extensions/gsd/db-writer.js +9 -9
- package/dist/resources/extensions/gsd/doctor-checks.js +1 -1
- package/dist/resources/extensions/gsd/doctor.js +2 -2
- package/dist/resources/extensions/gsd/gsd-db.js +5 -1
- package/dist/resources/extensions/gsd/preferences-types.js +2 -2
- package/dist/resources/extensions/gsd/preferences.js +8 -4
- package/dist/resources/extensions/gsd/workflow-logger.js +138 -0
- package/dist/resources/extensions/gsd/worktree-manager.js +4 -3
- package/dist/resources/extensions/gsd/worktree-resolver.js +37 -0
- package/dist/resources/extensions/voice/index.js +11 -16
- package/dist/resources/extensions/voice/linux-ready.js +67 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +19 -19
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +19 -19
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.js +2 -0
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lifecycle-hooks.d.ts +4 -0
- package/packages/pi-coding-agent/dist/core/lifecycle-hooks.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lifecycle-hooks.js +10 -5
- package/packages/pi-coding-agent/dist/core/lifecycle-hooks.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lifecycle-hooks.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/lifecycle-hooks.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lifecycle-hooks.test.js +185 -0
- package/packages/pi-coding-agent/dist/core/lifecycle-hooks.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js +239 -10
- package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +2 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +20 -2
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/package-commands.test.js +206 -195
- package/packages/pi-coding-agent/dist/core/package-commands.test.js.map +1 -1
- package/packages/pi-coding-agent/src/core/compaction-orchestrator.ts +2 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -1
- package/packages/pi-coding-agent/src/core/lifecycle-hooks.test.ts +227 -0
- package/packages/pi-coding-agent/src/core/lifecycle-hooks.ts +11 -5
- package/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts +297 -11
- package/packages/pi-coding-agent/src/core/model-registry.ts +30 -3
- package/packages/pi-coding-agent/src/core/package-commands.test.ts +227 -205
- package/src/resources/extensions/gsd/auto/phases.ts +16 -12
- package/src/resources/extensions/gsd/auto/run-unit.ts +6 -3
- package/src/resources/extensions/gsd/auto-worktree.ts +8 -5
- package/src/resources/extensions/gsd/auto.ts +3 -3
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +13 -12
- package/src/resources/extensions/gsd/db-writer.ts +9 -17
- package/src/resources/extensions/gsd/doctor-checks.ts +1 -1
- package/src/resources/extensions/gsd/doctor.ts +2 -2
- package/src/resources/extensions/gsd/gsd-db.ts +5 -1
- package/src/resources/extensions/gsd/journal.ts +6 -1
- package/src/resources/extensions/gsd/preferences-types.ts +2 -2
- package/src/resources/extensions/gsd/preferences.ts +7 -3
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +42 -3
- package/src/resources/extensions/gsd/tests/preferences.test.ts +7 -9
- package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +275 -0
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +220 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +193 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +4 -9
- package/src/resources/extensions/gsd/worktree-resolver.ts +37 -0
- package/src/resources/extensions/voice/index.ts +11 -21
- package/src/resources/extensions/voice/linux-ready.ts +87 -0
- package/src/resources/extensions/voice/tests/linux-ready.test.ts +124 -0
- /package/dist/web/standalone/.next/static/{rzO54ZboyINyEt7cVM_uS → dFMji9G1LZ-Tv36el9pRT}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{rzO54ZboyINyEt7cVM_uS → dFMji9G1LZ-Tv36el9pRT}/_ssgManifest.js +0 -0
|
@@ -19,6 +19,7 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from "node:
|
|
|
19
19
|
import { execFileSync } from "node:child_process";
|
|
20
20
|
import { join, resolve, sep } from "node:path";
|
|
21
21
|
import { GSDError, GSD_PARSE_ERROR, GSD_STALE_STATE, GSD_LOCK_HELD, GSD_GIT_ERROR, GSD_MERGE_CONFLICT } from "./errors.js";
|
|
22
|
+
import { logWarning } from "./workflow-logger.js";
|
|
22
23
|
import {
|
|
23
24
|
nativeBranchDelete,
|
|
24
25
|
nativeBranchExists,
|
|
@@ -136,9 +137,7 @@ export function createWorktree(basePath: string, name: string, opts: { branch?:
|
|
|
136
137
|
// worktree can be created in its place.
|
|
137
138
|
const gitFilePath = join(wtPath, ".git");
|
|
138
139
|
if (!existsSync(gitFilePath)) {
|
|
139
|
-
|
|
140
|
-
`[GSD] Removing stale worktree directory (no .git file): ${wtPath}`,
|
|
141
|
-
);
|
|
140
|
+
logWarning("reconcile", `Removing stale worktree directory (no .git file): ${wtPath}`, { worktree: name });
|
|
142
141
|
rmSync(wtPath, { recursive: true, force: true });
|
|
143
142
|
} else {
|
|
144
143
|
throw new GSDError(GSD_STALE_STATE, `Worktree "${name}" already exists at ${wtPath}`);
|
|
@@ -345,14 +344,10 @@ export function removeWorktree(
|
|
|
345
344
|
"git", ["stash", "push", "-m", "gsd: auto-stash submodule changes before worktree teardown"],
|
|
346
345
|
{ cwd: resolvedWtPath, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" },
|
|
347
346
|
);
|
|
348
|
-
|
|
349
|
-
`[GSD] WARNING: Stashed uncommitted submodule changes in ${resolvedWtPath} before worktree teardown.\n`,
|
|
350
|
-
);
|
|
347
|
+
logWarning("reconcile", `Stashed uncommitted submodule changes before worktree teardown`, { worktree: name, path: resolvedWtPath });
|
|
351
348
|
} catch {
|
|
352
349
|
// Stash failed — warn the user that submodule changes may be lost
|
|
353
|
-
|
|
354
|
-
`[GSD] WARNING: Submodule changes detected in ${resolvedWtPath} — stash failed, changes may be lost during force removal.\n`,
|
|
355
|
-
);
|
|
350
|
+
logWarning("reconcile", `Submodule changes detected — stash failed, changes may be lost during force removal`, { worktree: name, path: resolvedWtPath });
|
|
356
351
|
}
|
|
357
352
|
}
|
|
358
353
|
} catch {
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { existsSync, unlinkSync } from "node:fs";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
17
18
|
import { join } from "node:path";
|
|
18
19
|
import type { AutoSession } from "./auto/session.js";
|
|
19
20
|
import { debugLog } from "./debug-logger.js";
|
|
20
21
|
import { MergeConflictError } from "./git-service.js";
|
|
22
|
+
import { emitJournalEvent } from "./journal.js";
|
|
21
23
|
|
|
22
24
|
// ─── Dependency Interface ──────────────────────────────────────────────────
|
|
23
25
|
|
|
@@ -155,6 +157,13 @@ export class WorktreeResolver {
|
|
|
155
157
|
skipped: true,
|
|
156
158
|
reason: "isolation-disabled",
|
|
157
159
|
});
|
|
160
|
+
emitJournalEvent(this.s.originalBasePath || this.s.basePath, {
|
|
161
|
+
ts: new Date().toISOString(),
|
|
162
|
+
flowId: randomUUID(),
|
|
163
|
+
seq: 0,
|
|
164
|
+
eventType: "worktree-skip",
|
|
165
|
+
data: { milestoneId, reason: "isolation-disabled" },
|
|
166
|
+
});
|
|
158
167
|
return;
|
|
159
168
|
}
|
|
160
169
|
|
|
@@ -184,6 +193,13 @@ export class WorktreeResolver {
|
|
|
184
193
|
result: "success",
|
|
185
194
|
wtPath,
|
|
186
195
|
});
|
|
196
|
+
emitJournalEvent(this.s.originalBasePath || this.s.basePath, {
|
|
197
|
+
ts: new Date().toISOString(),
|
|
198
|
+
flowId: randomUUID(),
|
|
199
|
+
seq: 0,
|
|
200
|
+
eventType: "worktree-enter",
|
|
201
|
+
data: { milestoneId, wtPath, created: !existingPath },
|
|
202
|
+
});
|
|
187
203
|
ctx.notify(`Entered worktree for ${milestoneId} at ${wtPath}`, "info");
|
|
188
204
|
} catch (err) {
|
|
189
205
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -193,6 +209,13 @@ export class WorktreeResolver {
|
|
|
193
209
|
result: "error",
|
|
194
210
|
error: msg,
|
|
195
211
|
});
|
|
212
|
+
emitJournalEvent(this.s.originalBasePath || this.s.basePath, {
|
|
213
|
+
ts: new Date().toISOString(),
|
|
214
|
+
flowId: randomUUID(),
|
|
215
|
+
seq: 0,
|
|
216
|
+
eventType: "worktree-create-failed",
|
|
217
|
+
data: { milestoneId, error: msg, fallback: "project-root" },
|
|
218
|
+
});
|
|
196
219
|
ctx.notify(
|
|
197
220
|
`Auto-worktree creation for ${milestoneId} failed: ${msg}. Continuing in project root.`,
|
|
198
221
|
"warning",
|
|
@@ -288,6 +311,13 @@ export class WorktreeResolver {
|
|
|
288
311
|
mode,
|
|
289
312
|
basePath: this.s.basePath,
|
|
290
313
|
});
|
|
314
|
+
emitJournalEvent(this.s.originalBasePath || this.s.basePath, {
|
|
315
|
+
ts: new Date().toISOString(),
|
|
316
|
+
flowId: randomUUID(),
|
|
317
|
+
seq: 0,
|
|
318
|
+
eventType: "worktree-merge-start",
|
|
319
|
+
data: { milestoneId, mode },
|
|
320
|
+
});
|
|
291
321
|
|
|
292
322
|
if (mode === "none") {
|
|
293
323
|
debugLog("WorktreeResolver", {
|
|
@@ -408,6 +438,13 @@ export class WorktreeResolver {
|
|
|
408
438
|
error: msg,
|
|
409
439
|
fallback: "chdir-to-project-root",
|
|
410
440
|
});
|
|
441
|
+
emitJournalEvent(this.s.originalBasePath || this.s.basePath, {
|
|
442
|
+
ts: new Date().toISOString(),
|
|
443
|
+
flowId: randomUUID(),
|
|
444
|
+
seq: 0,
|
|
445
|
+
eventType: "worktree-merge-failed",
|
|
446
|
+
data: { milestoneId, error: msg },
|
|
447
|
+
});
|
|
411
448
|
// Surface a clear, actionable error. The worktree and milestone branch are
|
|
412
449
|
// intentionally preserved — nothing has been deleted. The user can retry
|
|
413
450
|
// /gsd dispatch complete-milestone or merge manually once the underlying issue is fixed
|
|
@@ -4,9 +4,9 @@ import type { AssistantMessage } from "@gsd/pi-ai";
|
|
|
4
4
|
import { isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@gsd/pi-tui";
|
|
5
5
|
import { spawn, execFileSync, type ChildProcess } from "node:child_process";
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
|
-
import * as os from "node:os";
|
|
8
7
|
import * as path from "node:path";
|
|
9
8
|
import * as readline from "node:readline";
|
|
9
|
+
import { linuxPython, diagnoseSounddeviceError, ensureVoiceVenv, VOICE_VENV_PYTHON } from "./linux-ready.js";
|
|
10
10
|
|
|
11
11
|
const __extensionDir = import.meta.dirname!;
|
|
12
12
|
const SWIFT_SRC = path.join(__extensionDir, "speech-recognizer.swift");
|
|
@@ -15,19 +15,6 @@ const PYTHON_SCRIPT = path.join(__extensionDir, "speech-recognizer.py");
|
|
|
15
15
|
|
|
16
16
|
const IS_DARWIN = process.platform === "darwin";
|
|
17
17
|
const IS_LINUX = process.platform === "linux";
|
|
18
|
-
const VOICE_VENV_PYTHON = path.join(
|
|
19
|
-
process.env.HOME || process.env.USERPROFILE || os.homedir(),
|
|
20
|
-
".gsd",
|
|
21
|
-
"voice-venv",
|
|
22
|
-
"bin",
|
|
23
|
-
"python3",
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
/** Return the python3 binary path — prefer venv if it exists, else system. */
|
|
27
|
-
function linuxPython(): string {
|
|
28
|
-
if (fs.existsSync(VOICE_VENV_PYTHON)) return VOICE_VENV_PYTHON;
|
|
29
|
-
return "python3";
|
|
30
|
-
}
|
|
31
18
|
|
|
32
19
|
function ensureBinary(): boolean {
|
|
33
20
|
if (fs.existsSync(RECOGNIZER_BIN)) return true;
|
|
@@ -69,17 +56,20 @@ function ensureLinuxReady(ctx: ExtensionContext): boolean {
|
|
|
69
56
|
});
|
|
70
57
|
} catch (err: unknown) {
|
|
71
58
|
const stderr = (err as { stderr?: Buffer })?.stderr?.toString() ?? "";
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
ctx.ui.notify(
|
|
59
|
+
const diagnosis = diagnoseSounddeviceError(stderr);
|
|
60
|
+
|
|
61
|
+
if (diagnosis === "missing-module") {
|
|
62
|
+
// Module not installed — auto-create venv (handles PEP 668 systems
|
|
63
|
+
// where system pip is blocked). See #2403.
|
|
64
|
+
if (!ensureVoiceVenv({ notify: (msg, level) => ctx.ui.notify(msg, level) })) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
78
67
|
linuxReady = true;
|
|
79
68
|
return true;
|
|
69
|
+
} else if (diagnosis === "missing-portaudio") {
|
|
70
|
+
ctx.ui.notify("Voice: install libportaudio2 with: sudo apt install libportaudio2", "error");
|
|
80
71
|
} else {
|
|
81
72
|
ctx.ui.notify(`Voice: dependency check failed — ${stderr.split("\n")[0] || "unknown error"}`, "error");
|
|
82
|
-
return false;
|
|
83
73
|
}
|
|
84
74
|
return false;
|
|
85
75
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* linux-ready.ts — Linux voice readiness logic (extracted for testability).
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Detecting system vs venv python3
|
|
6
|
+
* - Diagnosing sounddevice import errors (portaudio vs missing module)
|
|
7
|
+
* - Auto-creating venv on PEP 668 systems
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as os from "node:os";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
|
|
15
|
+
export const VOICE_VENV_DIR = path.join(
|
|
16
|
+
process.env.HOME || process.env.USERPROFILE || os.homedir(),
|
|
17
|
+
".gsd",
|
|
18
|
+
"voice-venv",
|
|
19
|
+
);
|
|
20
|
+
export const VOICE_VENV_PYTHON = path.join(VOICE_VENV_DIR, "bin", "python3");
|
|
21
|
+
|
|
22
|
+
/** Return the python3 binary path — prefer venv if it exists, else system. */
|
|
23
|
+
export function linuxPython(): string {
|
|
24
|
+
if (fs.existsSync(VOICE_VENV_PYTHON)) return VOICE_VENV_PYTHON;
|
|
25
|
+
return "python3";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Diagnose a sounddevice import error from its stderr output.
|
|
30
|
+
*
|
|
31
|
+
* Returns:
|
|
32
|
+
* - "missing-module" — sounddevice python package not installed
|
|
33
|
+
* - "missing-portaudio" — libportaudio2 native library not found
|
|
34
|
+
* - "unknown" — unrecognized error
|
|
35
|
+
*
|
|
36
|
+
* IMPORTANT: Check "No module" / "ModuleNotFoundError" BEFORE checking for the
|
|
37
|
+
* word "sounddevice", because `ModuleNotFoundError: No module named 'sounddevice'`
|
|
38
|
+
* contains both strings. The more specific check must come first.
|
|
39
|
+
*/
|
|
40
|
+
export function diagnoseSounddeviceError(stderr: string): "missing-module" | "missing-portaudio" | "unknown" {
|
|
41
|
+
// Check for missing Python module FIRST — the error message
|
|
42
|
+
// "ModuleNotFoundError: No module named 'sounddevice'" contains the word
|
|
43
|
+
// "sounddevice", so the old order (checking "sounddevice" first) was wrong.
|
|
44
|
+
if (stderr.includes("No module") || stderr.includes("ModuleNotFoundError")) {
|
|
45
|
+
return "missing-module";
|
|
46
|
+
}
|
|
47
|
+
// Now check for native portaudio library issues.
|
|
48
|
+
if (stderr.includes("PortAudio") || stderr.includes("portaudio")) {
|
|
49
|
+
return "missing-portaudio";
|
|
50
|
+
}
|
|
51
|
+
return "unknown";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ReadinessCallbacks {
|
|
55
|
+
notify: (message: string, level: "info" | "error") => void;
|
|
56
|
+
/** Override for execFileSync — for testing. Uses execFileSync (safe, no shell). */
|
|
57
|
+
execFile?: typeof execFileSync;
|
|
58
|
+
/** Override for fs.existsSync — for testing */
|
|
59
|
+
exists?: typeof fs.existsSync;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Auto-create the voice venv if it doesn't exist.
|
|
64
|
+
* Uses execFileSync internally (no shell, safe from injection).
|
|
65
|
+
*
|
|
66
|
+
* Returns true on success, false on failure.
|
|
67
|
+
*/
|
|
68
|
+
export function ensureVoiceVenv(cb: ReadinessCallbacks): boolean {
|
|
69
|
+
const exists = cb.exists ?? fs.existsSync;
|
|
70
|
+
const execFile = cb.execFile ?? execFileSync;
|
|
71
|
+
|
|
72
|
+
if (exists(VOICE_VENV_PYTHON)) return true;
|
|
73
|
+
|
|
74
|
+
cb.notify("Voice: setting up Python environment — one-time setup", "info");
|
|
75
|
+
try {
|
|
76
|
+
execFile("python3", ["-m", "venv", VOICE_VENV_DIR], { timeout: 30000 });
|
|
77
|
+
execFile(
|
|
78
|
+
path.join(VOICE_VENV_DIR, "bin", "pip"),
|
|
79
|
+
["install", "sounddevice", "requests", "--quiet"],
|
|
80
|
+
{ timeout: 120000 },
|
|
81
|
+
);
|
|
82
|
+
return true;
|
|
83
|
+
} catch {
|
|
84
|
+
cb.notify("Voice: failed to create Python venv — run: python3 -m venv ~/.gsd/voice-venv", "error");
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* linux-ready.test.ts — Tests for Linux voice readiness logic (#2403).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - diagnoseSounddeviceError branch ordering (ModuleNotFoundError must NOT
|
|
6
|
+
* match the portaudio branch, even though it contains "sounddevice")
|
|
7
|
+
* - ensureVoiceVenv auto-creation
|
|
8
|
+
* - linuxPython venv detection
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createTestContext } from "../../gsd/tests/test-helpers.ts";
|
|
12
|
+
import { diagnoseSounddeviceError, ensureVoiceVenv } from "../linux-ready.ts";
|
|
13
|
+
|
|
14
|
+
const { assertEq, assertTrue, report } = createTestContext();
|
|
15
|
+
|
|
16
|
+
function main(): void {
|
|
17
|
+
// ── diagnoseSounddeviceError ──────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
// The critical regression: "ModuleNotFoundError: No module named 'sounddevice'"
|
|
20
|
+
// contains the word "sounddevice", so the old code matched the portaudio branch.
|
|
21
|
+
console.log("\n=== diagnoseSounddeviceError: ModuleNotFoundError must return missing-module ===");
|
|
22
|
+
{
|
|
23
|
+
const stderr = "Traceback (most recent call last):\n File \"<string>\", line 1, in <module>\nModuleNotFoundError: No module named 'sounddevice'";
|
|
24
|
+
assertEq(diagnoseSounddeviceError(stderr), "missing-module",
|
|
25
|
+
"ModuleNotFoundError for sounddevice should be 'missing-module', not 'missing-portaudio'");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log("\n=== diagnoseSounddeviceError: 'No module named sounddevice' variant ===");
|
|
29
|
+
{
|
|
30
|
+
const stderr = "ImportError: No module named sounddevice";
|
|
31
|
+
assertEq(diagnoseSounddeviceError(stderr), "missing-module",
|
|
32
|
+
"'No module' substring should return missing-module");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log("\n=== diagnoseSounddeviceError: actual portaudio error ===");
|
|
36
|
+
{
|
|
37
|
+
const stderr = "OSError: PortAudio library not found";
|
|
38
|
+
assertEq(diagnoseSounddeviceError(stderr), "missing-portaudio",
|
|
39
|
+
"PortAudio library error should return missing-portaudio");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log("\n=== diagnoseSounddeviceError: lowercase portaudio error ===");
|
|
43
|
+
{
|
|
44
|
+
const stderr = "OSError: libportaudio.so.2: cannot open shared object file: No such file or directory";
|
|
45
|
+
assertEq(diagnoseSounddeviceError(stderr), "missing-portaudio",
|
|
46
|
+
"lowercase portaudio error should return missing-portaudio");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log("\n=== diagnoseSounddeviceError: unrelated error ===");
|
|
50
|
+
{
|
|
51
|
+
const stderr = "SyntaxError: invalid syntax";
|
|
52
|
+
assertEq(diagnoseSounddeviceError(stderr), "unknown",
|
|
53
|
+
"unrelated error should return unknown");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log("\n=== diagnoseSounddeviceError: empty stderr ===");
|
|
57
|
+
{
|
|
58
|
+
assertEq(diagnoseSounddeviceError(""), "unknown",
|
|
59
|
+
"empty stderr should return unknown");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── ensureVoiceVenv ──────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
console.log("\n=== ensureVoiceVenv: returns true when venv already exists ===");
|
|
65
|
+
{
|
|
66
|
+
const notifications: string[] = [];
|
|
67
|
+
const result = ensureVoiceVenv({
|
|
68
|
+
notify: (msg) => notifications.push(msg),
|
|
69
|
+
exists: () => true,
|
|
70
|
+
execFile: (() => Buffer.from("")) as any,
|
|
71
|
+
});
|
|
72
|
+
assertTrue(result, "should return true when venv exists");
|
|
73
|
+
assertEq(notifications.length, 0, "should not notify when venv exists");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log("\n=== ensureVoiceVenv: creates venv when missing ===");
|
|
77
|
+
{
|
|
78
|
+
const notifications: string[] = [];
|
|
79
|
+
const commands: string[][] = [];
|
|
80
|
+
let existsCalled = false;
|
|
81
|
+
|
|
82
|
+
const result = ensureVoiceVenv({
|
|
83
|
+
notify: (msg) => notifications.push(msg),
|
|
84
|
+
exists: () => { existsCalled = true; return false; },
|
|
85
|
+
execFile: ((cmd: string, args: string[]) => {
|
|
86
|
+
commands.push([cmd, ...args]);
|
|
87
|
+
return Buffer.from("");
|
|
88
|
+
}) as any,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
assertTrue(result, "should return true after venv creation");
|
|
92
|
+
assertTrue(existsCalled, "should check if venv exists");
|
|
93
|
+
assertEq(commands.length, 2, "should run 2 commands (venv + pip)");
|
|
94
|
+
assertTrue(commands[0][0] === "python3", "first command is python3");
|
|
95
|
+
assertTrue(commands[0].includes("-m") && commands[0].includes("venv"),
|
|
96
|
+
"first command creates venv");
|
|
97
|
+
assertTrue(commands[1][0].endsWith("bin/pip"), "second command is pip");
|
|
98
|
+
assertTrue(commands[1].includes("sounddevice"), "pip installs sounddevice");
|
|
99
|
+
assertTrue(commands[1].includes("requests"), "pip installs requests");
|
|
100
|
+
assertTrue(notifications[0].includes("one-time setup"),
|
|
101
|
+
"notifies about one-time setup");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log("\n=== ensureVoiceVenv: returns false and notifies on failure ===");
|
|
105
|
+
{
|
|
106
|
+
const notifications: Array<{ msg: string; level: string }> = [];
|
|
107
|
+
|
|
108
|
+
const result = ensureVoiceVenv({
|
|
109
|
+
notify: (msg, level) => notifications.push({ msg, level }),
|
|
110
|
+
exists: () => false,
|
|
111
|
+
execFile: (() => { throw new Error("externally-managed-environment"); }) as any,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
assertTrue(!result, "should return false on failure");
|
|
115
|
+
const errorNotif = notifications.find(n => n.level === "error");
|
|
116
|
+
assertTrue(errorNotif !== undefined, "should emit error notification");
|
|
117
|
+
assertTrue(errorNotif!.msg.includes("python3 -m venv"),
|
|
118
|
+
"error message should suggest manual venv creation");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
report();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
main();
|
|
File without changes
|
|
File without changes
|