Files
AgentHub/server/__tests__/ai-tools-sync.test.js
T

151 lines
5.7 KiB
JavaScript

/**
* @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");
});
});