/** * @file Tests for server/lib/ai-tools.js — catalog validation and override, * PATH detection over fake executables, the enable/default state rules, and * the A2 install/update fields with their three trust tiers. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, beforeEach, after } = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const root = fs.mkdtempSync(path.join(os.tmpdir(), "ai-tools-")); const builtin = path.join(root, "builtin"); const custom = path.join(root, "custom"); const bin = path.join(root, "bin"); const data = path.join(root, "data"); for (const d of [builtin, custom, bin, data]) fs.mkdirSync(d); const origPath = process.env.PATH; const origData = process.env.DASHBOARD_DATA_DIR; process.env.DASHBOARD_DATA_DIR = data; const aiTools = require("../lib/ai-tools"); const write = (dir, name, doc) => fs.writeFileSync(path.join(dir, name), typeof doc === "string" ? doc : JSON.stringify(doc)); const exe = (name, mode = 0o755) => { const p = path.join(bin, name); fs.writeFileSync(p, "#!/bin/sh\n"); fs.chmodSync(p, mode); return p; }; before(() => { write(builtin, "claude.json", { id: "claude", label: "Claude Code", detect: ["claude"], launcher: "claude", }); write(builtin, "codex.json", { id: "codex", label: "Codex", detect: ["codex"], launcher: "codex", }); write(builtin, "zed.json", { id: "zed", label: "Aaa Zed", detect: ["zed"], prompt: { mode: "argv" }, }); write(builtin, "pi.json", { id: "pi", label: "Pi", detect: ["pi"], prompt: { mode: "argv" }, resume: ["--continue"], }); write(builtin, "needs.json", { id: "needs", label: "Needs", detect: ["needs"], requires: ["helper"], prompt: { mode: "argv" }, }); write(builtin, "bad-id.json", { id: "Bad_Id", label: "x", detect: ["x"], prompt: { mode: "argv" }, }); write(builtin, "slash.json", { id: "slash", label: "x", detect: ["/bin/sh"], prompt: { mode: "argv" }, }); write(builtin, "args.json", { id: "args", label: "x", detect: ["x"], args: "--oops", prompt: { mode: "argv" }, }); write(builtin, "mode.json", { id: "mode", label: "x", detect: ["x"], prompt: { mode: "shell" } }); write(builtin, "broken.json", "{not json"); write(custom, "pi.json", { id: "pi", label: "Pi Custom", detect: ["pi"], prompt: { mode: "flag", flag: "-p" }, }); write(custom, "evil.json", { id: "evil", label: "x", detect: ["x"], launcher: "claude" }); exe("claude"); exe("pi"); exe("needs"); exe("zed", 0o644); // present but not executable process.env.PATH = bin; aiTools.__setCatalogDirs([builtin, custom]); }); after(() => { process.env.PATH = origPath; if (origData === undefined) delete process.env.DASHBOARD_DATA_DIR; else process.env.DASHBOARD_DATA_DIR = origData; aiTools.__setCatalogDirs(null); fs.rmSync(root, { recursive: true, force: true }); }); beforeEach(() => { fs.rmSync(path.join(data, "ai-tools.json"), { force: true }); aiTools.refresh(); }); describe("catalog", () => { it("skips invalid files and keeps valid ones in catalog order", () => { assert.deepEqual( aiTools.list().map((t) => t.id), ["claude", "codex", "zed", "needs", "pi"] ); }); it("lets a custom file replace a built-in one, but never set launcher", () => { assert.equal(aiTools.get("pi").label, "Pi Custom"); assert.equal(aiTools.get("pi").source, "custom"); assert.equal(aiTools.get("pi").resume, null); assert.equal(aiTools.has("evil"), false); assert.equal(aiTools.get("claude").source, "builtin"); }); }); describe("detection", () => { it("finds executables on PATH and ignores non-executable files", () => { assert.deepEqual(aiTools.detect("claude"), { installed: true, path: path.join(bin, "claude"), command: "claude", }); assert.equal(aiTools.detect("zed").installed, false); assert.equal(aiTools.detect("codex").installed, false); }); it("needs every `requires` command too", () => { assert.equal(aiTools.detect("needs").installed, false); exe("helper"); aiTools.refresh(); assert.equal(aiTools.detect("needs").installed, true); fs.rmSync(path.join(bin, "helper")); }); }); describe("enable / default", () => { it("defaults to claude, then the first installed enabled tool", () => { assert.equal(aiTools.effectiveDefault(), "claude"); aiTools.setEnabled("claude", false); assert.equal(aiTools.effectiveDefault(), "pi"); }); it("stores a default and clears it when that tool is disabled", () => { aiTools.setDefault("pi"); assert.equal(aiTools.readState().default, "pi"); assert.equal(aiTools.effectiveDefault(), "pi"); aiTools.setEnabled("pi", false); assert.equal(aiTools.readState().default, null); assert.equal(aiTools.isEnabled("pi"), false); aiTools.setEnabled("pi", true); assert.equal(aiTools.isEnabled("pi"), true); }); it("refuses a default that is unknown, not installed or disabled", () => { assert.throws(() => aiTools.setDefault("nope"), { code: "ENOTOOL" }); assert.throws(() => aiTools.setDefault("codex"), { code: "EBADDEFAULT" }); aiTools.setEnabled("pi", false); assert.throws(() => aiTools.setDefault("pi"), { code: "EBADDEFAULT" }); assert.throws(() => aiTools.setEnabled("nope", false), { code: "ENOTOOL" }); }); it("view() carries flags, never arg arrays", () => { const v = aiTools.view(); assert.equal(v.default, "claude"); const pi = v.tools.find((t) => t.id === "pi"); assert.deepEqual(pi, { id: "pi", label: "Pi Custom", command: "pi", homepageUrl: null, installed: true, path: path.join(bin, "pi"), enabled: true, source: "custom", launcher: null, prompt: { mode: "flag" }, resume: false, bypass: false, routerCapable: false, modelFlag: false, defaultModel: null, modelVendor: null, configFiles: 0, install: null, update: null, }); assert.equal(v.tools.find((t) => t.id === "codex").routerCapable, true); }); }); describe("built-in catalog", () => { it("loads every shipped file", () => { aiTools.__setCatalogDirs(null); const ids = aiTools.list().map((t) => t.id); const files = fs .readdirSync(path.join(__dirname, "..", "data", "ai-tools")) .filter((f) => f.endsWith(".json")); assert.equal(ids.length, files.length); assert.deepEqual(ids.slice(0, 2), ["claude", "codex"]); assert.equal(aiTools.get("antigravity").prompt.flag, "-i"); aiTools.__setCatalogDirs([builtin, custom]); }); }); describe("install/update fields and trust tiers", () => { const tiers = path.join(root, "tiers"); const tb = path.join(tiers, "builtin"); const tr = path.join(tiers, "remote"); const tc = path.join(tiers, "custom"); const base = (id, extra = {}) => ({ id, label: id, detect: [id], prompt: { mode: "argv" }, homepageUrl: "https://docs.example.com/cli", ...extra, }); before(() => { for (const d of [tb, tr, tc]) fs.mkdirSync(d, { recursive: true }); write(tb, "claude.json", { id: "claude", label: "Claude", detect: ["claude"], launcher: "claude", update: { kind: "self", args: ["update"] }, }); write( tb, "npmok.json", base("npmok", { install: { kind: "npm", package: "@scope/np-m.ok" }, update: { kind: "npm" }, }) ); write( tb, "scriptok.json", base("scriptok", { install: { kind: "script", url: "https://example.com/install" }, update: { kind: "script" }, }) ); write(tb, "badpkg.json", base("badpkg", { install: { kind: "npm", package: "Bad Pkg;rm" } })); write( tb, "httpurl.json", base("httpurl", { install: { kind: "script", url: "http://example.com/i" } }) ); write( tb, "offhost.json", base("offhost", { install: { kind: "script", url: "https://evil.io/i" } }) ); write(tb, "npmnoinst.json", base("npmnoinst", { update: { kind: "npm" } })); write(tb, "selfbad.json", base("selfbad", { update: { kind: "self", args: "update" } })); write(tb, "shared.json", base("shared", { label: "Shared builtin" })); write(tr, "shared.json", base("shared", { label: "Shared remote" })); write(tr, "codex.json", { id: "codex", label: "Codex R", detect: ["codex"], launcher: "codex", }); write( tr, "remoteonly.json", base("remoteonly", { install: { kind: "npm", package: "remote-only" } }) ); write(tc, "shared.json", base("shared", { label: "Shared custom" })); write(tc, "custinst.json", base("custinst", { install: { kind: "npm", package: "x" } })); write(tc, "custupd.json", base("custupd", { update: { kind: "self", args: ["up"] } })); aiTools.__setCatalogDirs([tb, tr, tc]); }); after(() => aiTools.__setCatalogDirs([builtin, custom])); it("accepts well-formed install/update and rejects the rest", () => { assert.deepEqual(aiTools.get("npmok").install, { kind: "npm", package: "@scope/np-m.ok" }); assert.deepEqual(aiTools.get("npmok").update, { kind: "npm" }); assert.deepEqual(aiTools.get("scriptok").install, { kind: "script", url: "https://example.com/install", }); assert.deepEqual(aiTools.get("claude").update, { kind: "self", args: ["update"] }); for (const id of ["badpkg", "httpurl", "offhost", "npmnoinst", "selfbad"]) { assert.equal(aiTools.has(id), false, id); } }); it("orders custom over remote over builtin and lets a remote file keep launcher", () => { assert.equal(aiTools.get("shared").label, "Shared custom"); assert.equal(aiTools.get("codex").source, "remote"); assert.equal(aiTools.get("codex").launcher, "codex"); assert.equal(aiTools.get("remoteonly").install.package, "remote-only"); }); it("refuses install/update in a custom file", () => { assert.equal(aiTools.has("custinst"), false); assert.equal(aiTools.has("custupd"), false); }); it("view() shows install/update so the confirm dialog can name the command", () => { const rows = aiTools.view().tools; assert.deepEqual(rows.find((t) => t.id === "npmok").install, { kind: "npm", package: "@scope/np-m.ok", }); assert.deepEqual(rows.find((t) => t.id === "claude").update, { kind: "self", args: ["update"], }); }); }); describe("which() on Windows", () => { const realPlatform = Object.getOwnPropertyDescriptor(process, "platform"); const realPathext = process.env.PATHEXT; const winDir = fs.mkdtempSync(path.join(os.tmpdir(), "ai-tools-win-")); // Executable bits only for the Linux host running the suite: Windows' // X_OK check is plain existence. for (const f of ["claude.exe", "codex.cmd"]) fs.writeFileSync(path.join(winDir, f), "", { mode: 0o755 }); const setPlatform = (p) => Object.defineProperty(process, "platform", { value: p }); after(() => { Object.defineProperty(process, "platform", realPlatform); if (realPathext === undefined) delete process.env.PATHEXT; else process.env.PATHEXT = realPathext; }); it("tries each PATHEXT extension after the bare name", () => { setPlatform("win32"); process.env.PATHEXT = ".EXE;.CMD"; assert.equal(aiTools.which("claude", winDir), path.join(winDir, "claude.exe")); assert.equal(aiTools.which("codex", winDir), path.join(winDir, "codex.cmd")); }); it("skips npm's extensionless sh shim, which Windows cannot run", () => { setPlatform("win32"); process.env.PATHEXT = ".EXE;.CMD"; fs.writeFileSync(path.join(winDir, "codex"), "#!/bin/sh\n", { mode: 0o755 }); try { assert.equal(aiTools.which("codex", winDir), path.join(winDir, "codex.cmd")); assert.equal(aiTools.which("claude.exe", winDir), path.join(winDir, "claude.exe")); } finally { fs.unlinkSync(path.join(winDir, "codex")); } }); it("falls back to .EXE;.CMD;.BAT without PATHEXT", () => { setPlatform("win32"); delete process.env.PATHEXT; assert.equal(aiTools.which("codex", winDir), path.join(winDir, "codex.cmd")); }); it("runs a .cmd/.bat through cmd.exe, its path quoted for Program Files", () => { setPlatform("win32"); const npm = "C:\\Program Files\\nodejs\\npm.cmd"; assert.deepEqual(aiTools.spawnTarget(npm), { file: `"${npm}"`, shell: true }); assert.deepEqual(aiTools.spawnTarget("C:\\x\\claude.exe"), { file: "C:\\x\\claude.exe", shell: false, }); setPlatform("linux"); assert.deepEqual(aiTools.spawnTarget("/usr/bin/npm.cmd"), { file: "/usr/bin/npm.cmd", shell: false, }); }); it("adds no extension anywhere else", () => { setPlatform("linux"); process.env.PATHEXT = ".EXE;.CMD"; assert.equal(aiTools.which("claude", winDir), null); }); });