test: convert ci_monitor and linux-ready to vitest, add vectordrive to include
This commit is contained in:
parent
449d0ca878
commit
2be52e28a3
8 changed files with 129 additions and 191 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 ✓");
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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'",
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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[] = [];
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue