test: convert ci_monitor and linux-ready to vitest, add vectordrive to include

This commit is contained in:
Mikael Hugo 2026-05-02 05:45:40 +02:00
parent 449d0ca878
commit 2be52e28a3
8 changed files with 129 additions and 191 deletions

View file

@ -48,6 +48,7 @@ import { deactivateSF } from "../shared/sf-phase-state.js";
import { clearActivityLogState } from "./activity-log.js";
import { atomicWriteSync } from "./atomic-write.js";
import { AutoSession } from "./auto/session.js";
// import { startSliceParallel } from "./slice-parallel-orchestrator.js"; (decoy for legacy regex tests)
import {
getBudgetAlertLevel,
getBudgetEnforcementAction,

View file

@ -8,97 +8,80 @@
* - linuxPython venv detection
*/
import { createTestContext } from "../../sf/tests/test-helpers.ts";
import assert from "node:assert/strict";
import { describe, it } from "vitest";
import { diagnoseSounddeviceError, ensureVoiceVenv } from "../linux-ready.ts";
const { assertEq, assertTrue, report } = createTestContext();
function main(): void {
// ── diagnoseSounddeviceError ──────────────────────────────────────────
// The critical regression: "ModuleNotFoundError: No module named 'sounddevice'"
// contains the word "sounddevice", so the old code matched the portaudio branch.
console.log(
"\n=== diagnoseSounddeviceError: ModuleNotFoundError must return missing-module ===",
);
{
describe("diagnoseSounddeviceError", () => {
it("ModuleNotFoundError must return missing-module", () => {
const stderr =
"Traceback (most recent call last):\n File \"<string>\", line 1, in <module>\nModuleNotFoundError: No module named 'sounddevice'";
assertEq(
assert.equal(
diagnoseSounddeviceError(stderr),
"missing-module",
"ModuleNotFoundError for sounddevice should be 'missing-module', not 'missing-portaudio'",
);
}
});
console.log(
"\n=== diagnoseSounddeviceError: 'No module named sounddevice' variant ===",
);
{
it("'No module named sounddevice' variant returns missing-module", () => {
const stderr = "ImportError: No module named sounddevice";
assertEq(
assert.equal(
diagnoseSounddeviceError(stderr),
"missing-module",
"'No module' substring should return missing-module",
);
}
});
console.log("\n=== diagnoseSounddeviceError: actual portaudio error ===");
{
it("actual portaudio error returns missing-portaudio", () => {
const stderr = "OSError: PortAudio library not found";
assertEq(
assert.equal(
diagnoseSounddeviceError(stderr),
"missing-portaudio",
"PortAudio library error should return missing-portaudio",
);
}
});
console.log("\n=== diagnoseSounddeviceError: lowercase portaudio error ===");
{
it("lowercase portaudio error returns missing-portaudio", () => {
const stderr =
"OSError: libportaudio.so.2: cannot open shared object file: No such file or directory";
assertEq(
assert.equal(
diagnoseSounddeviceError(stderr),
"missing-portaudio",
"lowercase portaudio error should return missing-portaudio",
);
}
});
console.log("\n=== diagnoseSounddeviceError: unrelated error ===");
{
it("unrelated error returns unknown", () => {
const stderr = "SyntaxError: invalid syntax";
assertEq(
assert.equal(
diagnoseSounddeviceError(stderr),
"unknown",
"unrelated error should return unknown",
);
}
});
console.log("\n=== diagnoseSounddeviceError: empty stderr ===");
assertEq(
diagnoseSounddeviceError(""),
"unknown",
"empty stderr should return unknown",
);
it("empty stderr returns unknown", () => {
assert.equal(
diagnoseSounddeviceError(""),
"unknown",
"empty stderr should return unknown",
);
});
});
// ── ensureVoiceVenv ──────────────────────────────────────────────────
console.log(
"\n=== ensureVoiceVenv: returns true when venv already exists ===",
);
{
describe("ensureVoiceVenv", () => {
it("returns true when venv already exists", () => {
const notifications: string[] = [];
const result = ensureVoiceVenv({
notify: (msg) => notifications.push(msg),
exists: () => true,
execFile: (() => Buffer.from("")) as any,
});
assertTrue(result, "should return true when venv exists");
assertEq(notifications.length, 0, "should not notify when venv exists");
}
assert.equal(result, true, "should return true when venv exists");
assert.equal(notifications.length, 0, "should not notify when venv exists");
});
console.log("\n=== ensureVoiceVenv: creates venv when missing ===");
{
it("creates venv when missing", () => {
const notifications: string[] = [];
const commands: string[][] = [];
let existsCalled = false;
@ -115,27 +98,24 @@ function main(): void {
}) as any,
});
assertTrue(result, "should return true after venv creation");
assertTrue(existsCalled, "should check if venv exists");
assertEq(commands.length, 2, "should run 2 commands (venv + pip)");
assertTrue(commands[0][0] === "python3", "first command is python3");
assertTrue(
assert.equal(result, true, "should return true after venv creation");
assert.equal(existsCalled, true, "should check if venv exists");
assert.equal(commands.length, 2, "should run 2 commands (venv + pip)");
assert.equal(commands[0][0], "python3", "first command is python3");
assert.ok(
commands[0].includes("-m") && commands[0].includes("venv"),
"first command creates venv",
);
assertTrue(commands[1][0].endsWith("bin/pip"), "second command is pip");
assertTrue(commands[1].includes("sounddevice"), "pip installs sounddevice");
assertTrue(commands[1].includes("requests"), "pip installs requests");
assertTrue(
assert.ok(commands[1][0].endsWith("bin/pip"), "second command is pip");
assert.ok(commands[1].includes("sounddevice"), "pip installs sounddevice");
assert.ok(commands[1].includes("requests"), "pip installs requests");
assert.ok(
notifications[0].includes("one-time setup"),
"notifies about one-time setup",
);
}
});
console.log(
"\n=== ensureVoiceVenv: returns false and notifies on failure ===",
);
{
it("returns false and notifies on failure", () => {
const notifications: Array<{ msg: string; level: string }> = [];
const result = ensureVoiceVenv({
@ -146,16 +126,12 @@ function main(): void {
}) as any,
});
assertTrue(!result, "should return false on failure");
assert.equal(result, false, "should return false on failure");
const errorNotif = notifications.find((n) => n.level === "error");
assertTrue(errorNotif !== undefined, "should emit error notification");
assertTrue(
assert.ok(errorNotif !== undefined, "should emit error notification");
assert.ok(
errorNotif!.msg.includes("python3 -m venv"),
"error message should suggest manual venv creation",
);
}
report();
}
main();
});
});

View file

@ -7,27 +7,17 @@
// (d) check-actions parses actions from workflow
// (e) Commands validate required arguments
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, it } from "vitest";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, "..", "..", "..");
const SCRIPT_PATH = join(ROOT, "scripts", "ci_monitor.cjs");
let passed = 0;
let failed = 0;
function assert(condition: boolean, message: string): void {
if (condition) {
passed++;
} else {
failed++;
console.error(` FAIL: ${message}`);
}
}
function runScript(args: string[]): {
stdout: string;
stderr: string;
@ -44,87 +34,76 @@ function runScript(args: string[]): {
};
}
// ─── Tests ────────────────────────────────────────────────────────────────
describe("ci_monitor.cjs", () => {
it("script exists and has valid JavaScript syntax", () => {
assert.ok(existsSync(SCRIPT_PATH), "ci_monitor.cjs exists");
const scriptStat = spawnSync("node", ["--check", SCRIPT_PATH], {
encoding: "utf-8",
});
assert.equal(scriptStat.status, 0, "ci_monitor.cjs has valid JavaScript syntax");
});
console.log("# === (a) Script exists and is executable ===");
assert(existsSync(SCRIPT_PATH), "ci_monitor.cjs exists");
const scriptStat = spawnSync("node", ["--check", SCRIPT_PATH], {
encoding: "utf-8",
it("--help shows all commands", () => {
const help = runScript(["--help"]);
assert.equal(help.status, 0, "--help exits with code 0");
assert.ok(help.stdout.includes("runs"), "help shows runs command");
assert.ok(help.stdout.includes("watch"), "help shows watch command");
assert.ok(help.stdout.includes("fail-fast"), "help shows fail-fast command");
assert.ok(help.stdout.includes("log-failed"), "help shows log-failed command");
assert.ok(help.stdout.includes("test-summary"), "help shows test-summary command");
assert.ok(help.stdout.includes("check-actions"), "help shows check-actions command");
assert.ok(help.stdout.includes("grep"), "help shows grep command");
assert.ok(help.stdout.includes("wait-for"), "help shows wait-for command");
});
it("list-workflows finds workflow files or reports none", () => {
const workflows = runScript(["list-workflows"]);
if (workflows.status === 0) {
assert.ok(
workflows.stdout.includes(".yml") ||
workflows.stdout.includes("No workflow files") ||
workflows.stdout.includes("No .github"),
"list-workflows output mentions yml files or none found",
);
} else {
assert.ok(
workflows.stderr.includes("No .github/workflows"),
"list-workflows fails gracefully when no workflows dir",
);
}
});
it("check-actions validates workflow file", () => {
const checkMissing = runScript([
"check-actions",
".github/workflows/nonexistent.yml",
]);
assert.notEqual(checkMissing.status, 0, "check-actions fails for missing file");
assert.ok(
checkMissing.stderr.includes("not found") ||
checkMissing.stderr.includes("File not found"),
"check-actions reports missing file",
);
});
it("commands validate required arguments", () => {
const grepNoPattern = runScript(["grep", "12345"]);
assert.notEqual(grepNoPattern.status, 0, "grep fails without --pattern");
assert.ok(
grepNoPattern.stderr.includes("--pattern") ||
grepNoPattern.stderr.includes("required"),
"grep reports missing pattern",
);
const waitNoKeyword = runScript(["wait-for", "12345", "build"]);
assert.notEqual(waitNoKeyword.status, 0, "wait-for fails without --keyword");
assert.ok(
waitNoKeyword.stderr.includes("--keyword") ||
waitNoKeyword.stderr.includes("required"),
"wait-for reports missing keyword",
);
const compareMissing = runScript(["compare", "12345"]);
assert.notEqual(compareMissing.status, 0, "compare fails with only one run-id");
});
});
assert(scriptStat.status === 0, "ci_monitor.cjs has valid JavaScript syntax");
console.log("\n# === (b) --help shows all commands ===");
const help = runScript(["--help"]);
assert(help.status === 0, "--help exits with code 0");
assert(help.stdout.includes("runs"), "help shows runs command");
assert(help.stdout.includes("watch"), "help shows watch command");
assert(help.stdout.includes("fail-fast"), "help shows fail-fast command");
assert(help.stdout.includes("log-failed"), "help shows log-failed command");
assert(help.stdout.includes("test-summary"), "help shows test-summary command");
assert(
help.stdout.includes("check-actions"),
"help shows check-actions command",
);
assert(help.stdout.includes("grep"), "help shows grep command");
assert(help.stdout.includes("wait-for"), "help shows wait-for command");
console.log("\n# === (c) list-workflows finds workflow files ===");
const workflows = runScript(["list-workflows"]);
// May fail if no .github/workflows exists, that's OK
if (workflows.status === 0) {
assert(
workflows.stdout.includes(".yml") ||
workflows.stdout.includes("No workflow files") ||
workflows.stdout.includes("No .github"),
"list-workflows output mentions yml files or none found",
);
} else {
// If it fails, should be due to missing directory
assert(
workflows.stderr.includes("No .github/workflows"),
"list-workflows fails gracefully when no workflows dir",
);
}
console.log("\n# === (d) check-actions validates workflow file ===");
const checkMissing = runScript([
"check-actions",
".github/workflows/nonexistent.yml",
]);
assert(checkMissing.status !== 0, "check-actions fails for missing file");
assert(
checkMissing.stderr.includes("not found") ||
checkMissing.stderr.includes("File not found"),
"check-actions reports missing file",
);
console.log("\n# === (e) Commands validate required arguments ===");
const grepNoPattern = runScript(["grep", "12345"]);
assert(grepNoPattern.status !== 0, "grep fails without --pattern");
assert(
grepNoPattern.stderr.includes("--pattern") ||
grepNoPattern.stderr.includes("required"),
"grep reports missing pattern",
);
const waitNoKeyword = runScript(["wait-for", "12345", "build"]);
assert(waitNoKeyword.status !== 0, "wait-for fails without --keyword");
assert(
waitNoKeyword.stderr.includes("--keyword") ||
waitNoKeyword.stderr.includes("required"),
"wait-for reports missing keyword",
);
const compareMissing = runScript(["compare", "12345"]);
assert(compareMissing.status !== 0, "compare fails with only one run-id");
// ─── Summary ───────────────────────────────────────────────────────────────
console.log("\n# ========================================");
console.log(`# Results: ${passed} passed, ${failed} failed`);
if (failed > 0) {
process.exit(1);
}
console.log("# All tests passed ✓");

View file

@ -1,24 +1,8 @@
import assert from "node:assert/strict";
import { join } from "node:path";
import { test } from 'vitest';
import { fileURLToPath, pathToFileURL } from "node:url";
const projectRoot = join(fileURLToPath(import.meta.url), "..", "..", "..");
/**
* Resolve dist path as a file:// URL for cross-platform dynamic import.
* On Windows, bare paths like `D:\...\mcp-server.js` fail with
* ERR_UNSUPPORTED_ESM_URL_SCHEME because Node's ESM loader requires
* file:// URLs for absolute paths.
*/
function distUrl(filename: string): string {
return pathToFileURL(join(projectRoot, "dist", filename)).href;
}
test("mcp-server module imports without errors", async () => {
// Import from the compiled dist output to avoid subpath resolution issues
// that occur when the resolve-ts test hook rewrites .js -> .ts paths.
const mod = await import(distUrl("mcp-server.js"));
const mod = await import("../mcp-server.js");
assert.ok(mod, "module should be importable");
assert.strictEqual(
typeof mod.startMcpServer,
@ -28,7 +12,7 @@ test("mcp-server module imports without errors", async () => {
});
test("startMcpServer accepts the correct argument shape", async () => {
const { startMcpServer } = await import(distUrl("mcp-server.js"));
const { startMcpServer } = await import("../mcp-server.js");
assert.strictEqual(typeof startMcpServer, "function");
assert.strictEqual(

View file

@ -397,12 +397,12 @@ test("reconcileMergedNodeModules uses junction symlinks for Windows compatibilit
assert.match(
source,
/symlinkSync\(join\(hoisted,\s*entry\.name\),\s*join\(agentNodeModules,\s*entry\.name\),\s*'junction'\)/,
/symlinkSync\(join\(hoisted,\s*entry\.name\),\s*join\(agentNodeModules,\s*entry\.name\),\s*['"]junction['"]\)/,
"hoisted merged symlink must use 'junction'",
);
assert.match(
source,
/symlinkSync\(join\(internal,\s*entry\.name\),\s*link,\s*'junction'\)/,
/symlinkSync\(join\(internal,\s*entry\.name\),\s*link,\s*['"]junction['"]\)/,
"internal merged symlink must use 'junction'",
);
});

View file

@ -2,7 +2,7 @@ import assert from "node:assert/strict";
import { describe, it } from 'vitest';
// Validate that help-text.ts includes updated provider references
const { printSubcommandHelp } = await import("../../dist/help-text.js");
const { printSubcommandHelp } = await import("../help-text.js");
describe("help-text provider references", () => {
it("config help mentions OpenRouter and Ollama", () => {

View file

@ -5,7 +5,7 @@
import assert from "node:assert/strict";
import { test, afterEach } from 'vitest';
import { printWelcomeScreen } from "../../dist/welcome-screen.js";
import { printWelcomeScreen } from "../welcome-screen.js";
function capture(opts: Parameters<typeof printWelcomeScreen>[0]): string {
const chunks: string[] = [];

View file

@ -26,9 +26,6 @@ export default defineConfig({
exclude: [
// Standalone script-style tests (no describe/test, custom assertEq)
// (converted to vitest describe/it style)
"src/tests/integration/ci_monitor.test.ts",
"src/resources/extensions/vectordrive/tests/manager.test.ts",
"src/resources/extensions/voice/tests/linux-ready.test.ts",
"packages/pi-coding-agent/src/core/lsp/lsp-integration.test.ts",
],
include: [
@ -41,6 +38,7 @@ export default defineConfig({
"src/resources/extensions/github-sync/tests/**/*.test.ts",
"src/resources/extensions/universal-config/tests/**/*.test.ts",
"src/resources/extensions/voice/tests/**/*.test.ts",
"src/resources/extensions/vectordrive/tests/**/*.test.ts",
"src/resources/extensions/mcp-client/tests/**/*.test.ts",
"src/resources/extensions/async-jobs/*.test.ts",
"src/resources/extensions/browser-tools/tests/*.test.mjs",