chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* Tests for CLI tool detection: cross-platform known paths, size threshold,
|
||||
* npm prefix deduplication, and env var overrides.
|
||||
*/
|
||||
|
||||
import { describe, it, before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const { getCliRuntimeStatus, CLI_TOOL_IDS } =
|
||||
await import("../../src/shared/services/cliRuntime.ts");
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────
|
||||
|
||||
function createTempDir() {
|
||||
const testRoot = path.join(os.tmpdir(), "omniroute-test-tmp");
|
||||
if (!fs.existsSync(testRoot)) {
|
||||
fs.mkdirSync(testRoot, { recursive: true });
|
||||
}
|
||||
return fs.mkdtempSync(path.join(testRoot, "cli-test-"));
|
||||
}
|
||||
|
||||
function createFile(dir, name, content) {
|
||||
const filePath = path.join(dir, name);
|
||||
fs.writeFileSync(filePath, content);
|
||||
if (process.platform !== "win32") {
|
||||
fs.chmodSync(filePath, 0o755);
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
// ─── CLI_TOOL_IDS ─────────────────────────────────────────────
|
||||
|
||||
describe("CLI_TOOL_IDS", () => {
|
||||
it("should include all expected tools from cliRuntime.ts (separate from CLI_TOOLS catalog)", () => {
|
||||
// CLI_TOOL_IDS comes from cliRuntime.ts — a runtime-detection catalog that
|
||||
// is SEPARATE from the UI catalog CLI_TOOLS in cliTools.ts.
|
||||
// windsurf was removed from CLI_TOOLS (plan 14 D17) but may still be in
|
||||
// cliRuntime.ts for binary detection purposes.
|
||||
const expected = [
|
||||
"claude",
|
||||
"codex",
|
||||
"droid",
|
||||
"openclaw",
|
||||
"cursor",
|
||||
"cline",
|
||||
"kilo",
|
||||
"continue",
|
||||
"opencode",
|
||||
"qoder",
|
||||
"qwen",
|
||||
];
|
||||
for (const id of expected) {
|
||||
assert.ok(CLI_TOOL_IDS.includes(id), `Missing tool: ${id}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Size Threshold (30 bytes) ────────────────────────────────
|
||||
|
||||
describe("Size threshold — checkKnownPath", () => {
|
||||
let tmpDir;
|
||||
|
||||
before(() => {
|
||||
tmpDir = createTempDir();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("should detect files >= 30 bytes via env var", async () => {
|
||||
const prev = process.env.CLI_DROID_BIN;
|
||||
// Create a valid 30-byte+ script (using spaces/comments for padding, NO \r on linux)
|
||||
const content =
|
||||
process.platform === "win32"
|
||||
? "@echo off\r\necho 1.0.0\r\nREM PADDING_PADDIN\r\nexit 0\r\n"
|
||||
: "#!/bin/sh\necho 1.0.0\n# PADDING_PADDING_PAD\nexit 0\n";
|
||||
const script = createFile(tmpDir, "droid-valid", content);
|
||||
// Verify it's at least 30 bytes
|
||||
const stat = fs.statSync(script);
|
||||
assert.ok(stat.size >= 30, `File should be >= 30 bytes, got ${stat.size}`);
|
||||
|
||||
process.env.CLI_DROID_BIN = script;
|
||||
try {
|
||||
const result = await getCliRuntimeStatus("droid");
|
||||
assert.ok(result.installed, `Expected installed=true, got reason=${result.reason}`);
|
||||
assert.ok(result.commandPath === script, `Expected commandPath=${script}`);
|
||||
} finally {
|
||||
if (prev !== undefined) process.env.CLI_DROID_BIN = prev;
|
||||
else delete process.env.CLI_DROID_BIN;
|
||||
}
|
||||
});
|
||||
|
||||
it("should detect a valid CLI script (>= 30 bytes) via env var", async () => {
|
||||
const prev = process.env.CLI_DROID_BIN;
|
||||
// Ensure the size stays > 30 bytes without \r\n on bash
|
||||
const content =
|
||||
process.platform === "win32"
|
||||
? "@echo off\r\necho 1.0.0\r\nREM PADDING_PAD\r\n"
|
||||
: "#!/bin/sh\necho 1.0.0\n# PADDING_PADDING_PAD\n";
|
||||
const script =
|
||||
process.platform === "win32"
|
||||
? createFile(tmpDir, "droid.cmd", content)
|
||||
: createFile(tmpDir, "droid", content);
|
||||
|
||||
process.env.CLI_DROID_BIN = script;
|
||||
try {
|
||||
const result = await getCliRuntimeStatus("droid");
|
||||
assert.ok(result.installed, `Expected installed=true, got reason=${result.reason}`);
|
||||
assert.ok(
|
||||
result.commandPath === script,
|
||||
`Expected commandPath=${script}, got ${result.commandPath}`
|
||||
);
|
||||
} finally {
|
||||
if (prev !== undefined) process.env.CLI_DROID_BIN = prev;
|
||||
else delete process.env.CLI_DROID_BIN;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Healthcheck with --version ───────────────────────────────
|
||||
|
||||
describe("Healthcheck — checkRunnable", () => {
|
||||
let tmpDir;
|
||||
|
||||
before(() => {
|
||||
tmpDir = createTempDir();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("should report runnable=true for a script that outputs version", async () => {
|
||||
const prev = process.env.CLI_CLINE_BIN;
|
||||
const script =
|
||||
process.platform === "win32"
|
||||
? createFile(tmpDir, "good.cmd", "@echo off\necho 1.0.0\n")
|
||||
: createFile(tmpDir, "good", "#!/bin/sh\necho 1.0.0\n");
|
||||
|
||||
process.env.CLI_CLINE_BIN = script;
|
||||
try {
|
||||
const result = await getCliRuntimeStatus("cline");
|
||||
assert.ok(result.installed, `Expected installed=true, got reason=${result.reason}`);
|
||||
if (result.runnable) {
|
||||
assert.ok(result.reason === null, `Expected no reason, got ${result.reason}`);
|
||||
}
|
||||
} finally {
|
||||
if (prev !== undefined) process.env.CLI_CLINE_BIN = prev;
|
||||
else delete process.env.CLI_CLINE_BIN;
|
||||
}
|
||||
});
|
||||
|
||||
it("should detect qodercli via env override and mark it runnable", async () => {
|
||||
const prev = process.env.CLI_QODER_BIN;
|
||||
const script =
|
||||
process.platform === "win32"
|
||||
? createFile(tmpDir, "qoder.cmd", "@echo off\necho qodercli 0.1.37\n")
|
||||
: createFile(tmpDir, "qodercli", "#!/bin/sh\necho qodercli 0.1.37\n");
|
||||
|
||||
process.env.CLI_QODER_BIN = script;
|
||||
try {
|
||||
const result = await getCliRuntimeStatus("qoder");
|
||||
assert.equal(result.installed, true);
|
||||
assert.equal(result.runnable, true);
|
||||
assert.equal(result.commandPath, script);
|
||||
assert.equal(result.reason, null);
|
||||
} finally {
|
||||
if (prev !== undefined) process.env.CLI_QODER_BIN = prev;
|
||||
else delete process.env.CLI_QODER_BIN;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Unknown tool ─────────────────────────────────────────────
|
||||
|
||||
describe("Unknown tool", () => {
|
||||
it("should return unknown_tool for non-existent tool", async () => {
|
||||
const result = await getCliRuntimeStatus("nonexistent-tool-xyz");
|
||||
assert.equal(result.installed, false);
|
||||
assert.equal(result.reason, "unknown_tool");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── continue tool (requiresBinary: false) ────────────────────
|
||||
|
||||
describe("continue tool — no binary required", () => {
|
||||
it("should report installed=true without checking binary", async () => {
|
||||
const result = await getCliRuntimeStatus("continue");
|
||||
assert.equal(result.installed, true);
|
||||
assert.equal(result.reason, "not_required");
|
||||
});
|
||||
});
|
||||
|
||||
// Note: windsurf was removed from CLI_TOOLS in plan 14 D17 (MITM backlog plan 11).
|
||||
// cliRuntime.ts may still have windsurf for binary detection (separate catalog).
|
||||
// This test is skipped if windsurf is not registered in cliRuntime.ts.
|
||||
describe("windsurf tool — guide-only integration (cliRuntime.ts)", () => {
|
||||
it("should handle getCliRuntimeStatus for windsurf if it exists in cliRuntime catalog", async () => {
|
||||
if (!CLI_TOOL_IDS.includes("windsurf")) {
|
||||
// windsurf removed from runtime detection catalog too — skip
|
||||
return;
|
||||
}
|
||||
const result = await getCliRuntimeStatus("windsurf");
|
||||
assert.equal(result.installed, true);
|
||||
assert.equal(result.runnable, true);
|
||||
assert.equal(result.reason, "not_required");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── resolveOpencodeConfigPath — cross-platform ─────────────────
|
||||
|
||||
const { resolveOpencodeConfigPath: resolveOpencodeConfigPathFn } =
|
||||
await import("../../src/shared/services/cliRuntime.ts");
|
||||
|
||||
describe("resolveOpencodeConfigPath — cross-platform", () => {
|
||||
it("should resolve on Linux with XDG_CONFIG_HOME", () => {
|
||||
const result = resolveOpencodeConfigPathFn(
|
||||
"linux",
|
||||
{ XDG_CONFIG_HOME: "/tmp/xdg" },
|
||||
"/home/dev"
|
||||
);
|
||||
assert.equal(result, path.join("/tmp/xdg", "opencode", "opencode.json"));
|
||||
});
|
||||
|
||||
it("should resolve on Linux with default .config", () => {
|
||||
const result = resolveOpencodeConfigPathFn("linux", {}, "/home/dev");
|
||||
assert.equal(result, path.join("/home/dev", ".config", "opencode", "opencode.json"));
|
||||
});
|
||||
|
||||
it("should resolve on Windows under ~/.config (XDG, NOT %APPDATA% — #3330)", () => {
|
||||
// #3330: OpenCode reads its config from ~/.config/opencode on every
|
||||
// platform, including Windows (%USERPROFILE%\.config). %APPDATA% is ignored.
|
||||
const result = resolveOpencodeConfigPathFn(
|
||||
"win32",
|
||||
{ APPDATA: "C:\\Users\\dev\\AppData\\Roaming" },
|
||||
"C:\\Users\\dev"
|
||||
);
|
||||
assert.equal(result, path.join("C:\\Users\\dev", ".config", "opencode", "opencode.json"));
|
||||
});
|
||||
|
||||
it("should resolve on Windows under ~/.config without APPDATA (#3330)", () => {
|
||||
const result = resolveOpencodeConfigPathFn("win32", {}, "C:\\Users\\dev");
|
||||
assert.equal(result, path.join("C:\\Users\\dev", ".config", "opencode", "opencode.json"));
|
||||
});
|
||||
|
||||
it("should honor XDG_CONFIG_HOME on Windows too (#3330)", () => {
|
||||
const result = resolveOpencodeConfigPathFn(
|
||||
"win32",
|
||||
{ XDG_CONFIG_HOME: "D:\\xdg" },
|
||||
"C:\\Users\\dev"
|
||||
);
|
||||
assert.equal(result, path.join("D:\\xdg", "opencode", "opencode.json"));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user