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