From 2be52e28a324dd4bb76e919851c5cda3a22a211a Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sat, 2 May 2026 05:45:40 +0200 Subject: [PATCH] test: convert ci_monitor and linux-ready to vitest, add vectordrive to include --- src/resources/extensions/sf/auto.ts | 1 + .../voice/tests/linux-ready.test.ts | 120 +++++-------- src/tests/integration/ci_monitor.test.ts | 167 ++++++++---------- src/tests/mcp-server.test.ts | 20 +-- src/tests/node-modules-symlink.test.ts | 4 +- src/tests/provider-help-text.test.ts | 2 +- src/tests/welcome-screen.test.ts | 2 +- vitest.config.ts | 4 +- 8 files changed, 129 insertions(+), 191 deletions(-) diff --git a/src/resources/extensions/sf/auto.ts b/src/resources/extensions/sf/auto.ts index 0374ec196..7483beb25 100644 --- a/src/resources/extensions/sf/auto.ts +++ b/src/resources/extensions/sf/auto.ts @@ -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, diff --git a/src/resources/extensions/voice/tests/linux-ready.test.ts b/src/resources/extensions/voice/tests/linux-ready.test.ts index 72364b254..391e51f54 100644 --- a/src/resources/extensions/voice/tests/linux-ready.test.ts +++ b/src/resources/extensions/voice/tests/linux-ready.test.ts @@ -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 \"\", line 1, in \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(); + }); +}); diff --git a/src/tests/integration/ci_monitor.test.ts b/src/tests/integration/ci_monitor.test.ts index a9dbe10dd..24ca69892 100644 --- a/src/tests/integration/ci_monitor.test.ts +++ b/src/tests/integration/ci_monitor.test.ts @@ -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 ✓"); diff --git a/src/tests/mcp-server.test.ts b/src/tests/mcp-server.test.ts index 48b598c12..d4f8fd464 100644 --- a/src/tests/mcp-server.test.ts +++ b/src/tests/mcp-server.test.ts @@ -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( diff --git a/src/tests/node-modules-symlink.test.ts b/src/tests/node-modules-symlink.test.ts index 6c89d3d82..a64a704c6 100644 --- a/src/tests/node-modules-symlink.test.ts +++ b/src/tests/node-modules-symlink.test.ts @@ -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'", ); }); diff --git a/src/tests/provider-help-text.test.ts b/src/tests/provider-help-text.test.ts index 85ea1076f..7efb8e01a 100644 --- a/src/tests/provider-help-text.test.ts +++ b/src/tests/provider-help-text.test.ts @@ -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", () => { diff --git a/src/tests/welcome-screen.test.ts b/src/tests/welcome-screen.test.ts index a9d8caaf4..2c13a1833 100644 --- a/src/tests/welcome-screen.test.ts +++ b/src/tests/welcome-screen.test.ts @@ -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[0]): string { const chunks: string[] = []; diff --git a/vitest.config.ts b/vitest.config.ts index e8ad68e6e..bc99b3b9e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -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",