feat(ai-tools): install/update AI CLIs, remote catalog sync, cached favicons (#40 A2)
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @file Tests for server/lib/ai-tools-sync.js — the daily copy of the newest
|
||||
* release's AI Tools catalog: newer-only, validated per file, replaced whole,
|
||||
* kept on failure, and ignored once the running build catches up.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
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 data = fs.mkdtempSync(path.join(os.tmpdir(), "ai-sync-"));
|
||||
const origData = process.env.DASHBOARD_DATA_DIR;
|
||||
process.env.DASHBOARD_DATA_DIR = data;
|
||||
|
||||
const sync = require("../lib/ai-tools-sync");
|
||||
|
||||
const REPO = "https://forge.example.com/Owner/Repo.git";
|
||||
const API = "https://forge.example.com/api/v1/repos/Owner/Repo";
|
||||
const tool = (id, extra = {}) =>
|
||||
JSON.stringify({ id, label: id, detect: [id], prompt: { mode: "argv" }, ...extra });
|
||||
|
||||
/** A fake fetch over a map of URL → body (string or object) or a status number. */
|
||||
function fakeFetch(routes) {
|
||||
const calls = [];
|
||||
const fn = async (url) => {
|
||||
calls.push(String(url));
|
||||
const r = routes[String(url)];
|
||||
if (r === undefined || typeof r === "number") {
|
||||
return { ok: false, status: r || 404, json: async () => ({}), text: async () => "" };
|
||||
}
|
||||
const text = typeof r === "string" ? r : JSON.stringify(r);
|
||||
return { ok: true, status: 200, json: async () => JSON.parse(text), text: async () => text };
|
||||
};
|
||||
fn.calls = calls;
|
||||
return fn;
|
||||
}
|
||||
|
||||
const listing = (names) =>
|
||||
names.map((n) => ({ type: "file", name: n, download_url: `https://forge.example.com/raw/${n}` }));
|
||||
|
||||
function routesFor(tag, files) {
|
||||
const r = {
|
||||
[`${API}/releases/latest`]: { tag_name: tag },
|
||||
[`${API}/contents/server/data/ai-tools?ref=${tag}`]: listing(Object.keys(files)),
|
||||
};
|
||||
for (const [n, body] of Object.entries(files)) r[`https://forge.example.com/raw/${n}`] = body;
|
||||
return r;
|
||||
}
|
||||
|
||||
let origFetch;
|
||||
before(() => {
|
||||
origFetch = globalThis.fetch;
|
||||
});
|
||||
beforeEach(() => {
|
||||
fs.rmSync(path.join(data, "runtime"), { recursive: true, force: true });
|
||||
delete process.env.AGENTHUB_AI_TOOLS_SYNC;
|
||||
sync.__setVersion("1.8.11");
|
||||
});
|
||||
after(() => {
|
||||
globalThis.fetch = origFetch;
|
||||
sync.__setVersion(null);
|
||||
if (origData === undefined) delete process.env.DASHBOARD_DATA_DIR;
|
||||
else process.env.DASHBOARD_DATA_DIR = origData;
|
||||
fs.rmSync(data, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("syncOnce", () => {
|
||||
it("writes nothing when the newest release is not newer", async () => {
|
||||
globalThis.fetch = fakeFetch(routesFor("v1.8.11", { "a.json": tool("a") }));
|
||||
const r = await sync.syncOnce({ repoUrl: REPO });
|
||||
assert.deepEqual(r, { ok: false, reason: "not_newer" });
|
||||
assert.equal(sync.activeRemoteDir(), null);
|
||||
});
|
||||
|
||||
it("copies valid files of a newer release and drops invalid ones", async () => {
|
||||
globalThis.fetch = fakeFetch(
|
||||
routesFor("v1.9.0", {
|
||||
"a.json": tool("a"),
|
||||
"bad.json": "{nope",
|
||||
"codex.json": JSON.stringify({
|
||||
id: "codex",
|
||||
label: "Codex",
|
||||
detect: ["codex"],
|
||||
launcher: "codex",
|
||||
}),
|
||||
})
|
||||
);
|
||||
const r = await sync.syncOnce({ repoUrl: REPO });
|
||||
assert.equal(r.ok, true);
|
||||
assert.equal(r.tag, "v1.9.0");
|
||||
const dir = sync.activeRemoteDir();
|
||||
assert.ok(dir);
|
||||
assert.deepEqual(fs.readdirSync(dir).sort(), ["a.json", "codex.json", "meta.json"]);
|
||||
assert.deepEqual(sync.catalogMeta().source, "remote");
|
||||
assert.equal(sync.catalogMeta().tag, "v1.9.0");
|
||||
});
|
||||
|
||||
it("keeps the previous set when a download fails", async () => {
|
||||
globalThis.fetch = fakeFetch(routesFor("v1.9.0", { "a.json": tool("a") }));
|
||||
await sync.syncOnce({ repoUrl: REPO });
|
||||
const routes = routesFor("v1.10.0", { "b.json": tool("b") });
|
||||
routes["https://forge.example.com/raw/b.json"] = 500;
|
||||
globalThis.fetch = fakeFetch(routes);
|
||||
const r = await sync.syncOnce({ repoUrl: REPO });
|
||||
assert.equal(r.ok, false);
|
||||
assert.equal(sync.catalogMeta().tag, "v1.9.0");
|
||||
assert.ok(fs.existsSync(path.join(sync.activeRemoteDir(), "a.json")));
|
||||
});
|
||||
|
||||
it("refuses an oversized file, an off-origin download and a huge listing", async () => {
|
||||
const routes = routesFor("v1.9.0", { "big.json": tool("big", { label: "x".repeat(70_000) }) });
|
||||
routes[`${API}/contents/server/data/ai-tools?ref=v1.9.0`].push({
|
||||
type: "file",
|
||||
name: "evil.json",
|
||||
download_url: "https://evil.example.net/evil.json",
|
||||
});
|
||||
routes["https://evil.example.net/evil.json"] = tool("evil");
|
||||
globalThis.fetch = fakeFetch(routes);
|
||||
const r = await sync.syncOnce({ repoUrl: REPO });
|
||||
assert.equal(r.ok, true);
|
||||
assert.deepEqual(fs.readdirSync(sync.activeRemoteDir()), ["meta.json"]);
|
||||
assert.ok(!globalThis.fetch.calls.includes("https://evil.example.net/evil.json"));
|
||||
|
||||
const many = {};
|
||||
for (let i = 0; i < 201; i++) many[`t${i}.json`] = tool(`t${i}`);
|
||||
globalThis.fetch = fakeFetch(routesFor("v1.9.1", many));
|
||||
assert.deepEqual(await sync.syncOnce({ repoUrl: REPO }), { ok: false, reason: "bad_listing" });
|
||||
});
|
||||
|
||||
it("does nothing with AGENTHUB_AI_TOOLS_SYNC=0", async () => {
|
||||
process.env.AGENTHUB_AI_TOOLS_SYNC = "0";
|
||||
globalThis.fetch = fakeFetch({});
|
||||
assert.deepEqual(await sync.syncOnce({ repoUrl: REPO }), { ok: false, reason: "disabled" });
|
||||
assert.equal(globalThis.fetch.calls.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("activeRemoteDir", () => {
|
||||
it("ignores a synced set once the running build is as new", async () => {
|
||||
globalThis.fetch = fakeFetch(routesFor("v1.9.0", { "a.json": tool("a") }));
|
||||
await sync.syncOnce({ repoUrl: REPO });
|
||||
assert.ok(sync.activeRemoteDir());
|
||||
sync.__setVersion("1.9.0");
|
||||
assert.equal(sync.activeRemoteDir(), null);
|
||||
assert.equal(sync.catalogMeta().source, "builtin");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user