Files
AgentHub/server/__tests__/ai-tools-route.test.js
T
nntrivi2001 77917203fd feat(ai-tools): catalog of AI coding CLIs, detection, enable/default, lanes for any tool (#40 A1)
server/lib/ai-tools.js loads server/data/ai-tools/*.json (38 tools, data from
Orca, MIT) plus DASHBOARD_AI_TOOLS_DIR, detects each CLI by walking PATH, and
keeps enable/default in <data dir>/ai-tools.json. shell-path.js merges the
login shell's PATH at startup and on Refresh. /api/ai-tools lists, refreshes
and toggles; /api/run/binary resolves through the catalog.

Lanes accept any catalog id (EDISABLED for a disabled one on creation, ENOTOOL
at start once a tool left the catalog). pty-run keeps the Claude/Codex
builders and builds every other tool's argv from its catalog file, pasting an
after-start prompt through a tmux buffer; every pane gets the hydrated PATH.
A per-run bypass switch appends the catalog's bypass args (Codex included).
POST /api/run now forwards sandbox/approval/resumeLast, which it dropped.
agenthub lanes add gains --tool.
2026-09-29 08:18:40 +07:00

136 lines
5.6 KiB
JavaScript

/**
* @file Route tests for /api/ai-tools: list shape, enable/disable, default
* errors, the same-origin guard, and /api/run/binary resolving through the
* catalog.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, before, 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 http = require("node:http");
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ai-tools-route-"));
process.env.DASHBOARD_DB_PATH = path.join(ROOT, "t.db");
process.env.DASHBOARD_DATA_DIR = path.join(ROOT, "data");
process.env.CLAUDE_HOME = path.join(ROOT, ".claude");
process.env.AGENTHUB_SHELL_PATH = "0";
const BIN = path.join(ROOT, "bin");
fs.mkdirSync(BIN);
for (const name of ["claude", "agy"]) {
fs.writeFileSync(path.join(BIN, name), "#!/bin/sh\n");
fs.chmodSync(path.join(BIN, name), 0o755);
}
const origPath = process.env.PATH;
process.env.PATH = BIN;
const { createApp } = require("../index");
require("../lib/ai-tools").refresh();
let app;
let base;
const request = (method, p, body, headers = {}) =>
fetch(`${base}${p}`, {
method,
headers: { "Content-Type": "application/json", ...headers },
body: body === undefined ? undefined : JSON.stringify(body),
}).then(async (r) => ({ status: r.status, body: await r.json().catch(() => null) }));
before(async () => {
app = http.createServer(createApp());
await new Promise((r) => app.listen(0, "127.0.0.1", r));
base = `http://127.0.0.1:${app.address().port}`;
});
after(async () => {
process.env.PATH = origPath;
await new Promise((r) => app.close(r));
fs.rmSync(ROOT, { recursive: true, force: true });
});
describe("/api/ai-tools", () => {
it("lists the catalog with detection and the default", async () => {
const { status, body } = await request("GET", "/api/ai-tools");
assert.equal(status, 200);
assert.equal(body.default, "claude");
const agy = body.tools.find((t) => t.id === "antigravity");
assert.equal(agy.installed, true);
assert.equal(agy.path, path.join(BIN, "agy"));
assert.equal(agy.resume, true);
assert.equal(agy.bypass, true);
assert.equal(body.tools.find((t) => t.id === "gemini").installed, false);
assert.equal("args" in agy, false);
});
it("disables and re-enables a tool, and sets the default", async () => {
let r = await request("PUT", "/api/ai-tools/claude", { enabled: false });
assert.equal(r.status, 200);
assert.equal(r.body.tools.find((t) => t.id === "claude").enabled, false);
assert.equal(r.body.default, "antigravity");
r = await request("PUT", "/api/ai-tools/claude", { enabled: true });
assert.equal(r.body.default, "claude");
r = await request("PUT", "/api/ai-tools/antigravity", { default: true });
assert.equal(r.body.default, "antigravity");
});
it("answers ENOTOOL / EBADDEFAULT / EBADFIELD", async () => {
assert.equal((await request("PUT", "/api/ai-tools/nope", { enabled: false })).status, 404);
const bad = await request("PUT", "/api/ai-tools/gemini", { default: true });
assert.equal(bad.status, 400);
assert.equal(bad.body.error.code, "EBADDEFAULT");
assert.equal((await request("PUT", "/api/ai-tools/claude", { enabled: "no" })).status, 400);
});
it("refuses a cross-origin write", async () => {
const r = await request(
"PUT",
"/api/ai-tools/claude",
{ enabled: false },
{ Origin: "https://evil.test" }
);
assert.equal(r.status, 403);
});
it("refresh re-detects", async () => {
fs.writeFileSync(path.join(BIN, "gemini"), "#!/bin/sh\n");
fs.chmodSync(path.join(BIN, "gemini"), 0o755);
const r = await request("POST", "/api/ai-tools/refresh");
assert.equal(r.body.tools.find((t) => t.id === "gemini").installed, true);
});
it("/api/run/binary resolves through the catalog", async () => {
const r = await request("GET", "/api/run/binary?tool=antigravity");
assert.deepEqual(r.body, { found: true, path: path.join(BIN, "agy") });
assert.equal((await request("GET", "/api/run/binary?tool=rm")).status, 400);
});
});
describe("lanes on catalog tools", () => {
it("creates a lane for an installed or not-installed tool, refuses unknown/disabled", async () => {
const dir = (n) => {
const d = path.join(ROOT, n);
fs.mkdirSync(d, { recursive: true });
return d;
};
let r = await request("POST", "/api/lanes", { cwd: dir("a"), title: "a", tool: "gemini" });
assert.equal(r.status, 201);
assert.equal(r.body.lane.tool, "gemini");
r = await request("POST", "/api/lanes", { cwd: dir("b"), title: "b", tool: "aider" });
assert.equal(r.status, 201, "a tool that is not installed may still get a lane");
r = await request("POST", "/api/lanes", { cwd: dir("c"), title: "c", tool: "nope" });
assert.equal(r.body.error.code, "EBADTOOL");
await request("PUT", "/api/ai-tools/aider", { enabled: false });
r = await request("POST", "/api/lanes", { cwd: dir("d"), title: "d", tool: "aider" });
assert.equal(r.status, 400);
assert.equal(r.body.error.code, "EDISABLED");
r = await request("POST", "/api/lanes/ensure", { cwd: dir("e"), tool: "aider" });
assert.equal(r.body.error.code, "EDISABLED");
r = await request("POST", "/api/lanes/ensure", { cwd: dir("b"), tool: "aider" });
assert.equal(r.status, 200, "an existing lane on a disabled tool is still found");
r = await request("POST", "/api/lanes/worktree", { sourceRepo: dir("f"), tool: "aider" });
assert.equal(r.body.error.code, "EDISABLED");
await request("PUT", "/api/ai-tools/aider", { enabled: true });
});
});