/** * @file Tests for the Artifacts hub (server/lib/artifacts.js, routes/artifacts.js): * claude.ai pages (link allowlist, hook capture, one row per URL), workspace * file candidates and pins (a snapshot, served sandboxed), skills (snapshot, * symlink/secret refusal, install into the caller's Claude home with * replace moving the old one aside), plugins (marketplace allowlist, install * argv; a marketplace link read into its plugin list, the same plugin added * twice kept as one row, key/market edited), skills uploaded (one Markdown * file, a folder, a zip, several skills at once — name and frontmatter set, * junk skipped, nothing stored when one fails) or written in the dialog, a * file uploaded, sharing refused in local mode, visibility for a server-mode * developer, the admin's installs switch, and same-origin writes. * @author Nguyễn Ngọc Trí Vĩ */ 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.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "artifacts-"))); process.env.DASHBOARD_DB_PATH = path.join(ROOT, "t.db"); process.env.CLAUDE_HOME = path.join(ROOT, "claude"); process.env.DASHBOARD_DATA_DIR = path.join(ROOT, "data"); process.env.DASHBOARD_REMOTE_SYNC_MS = "0"; process.env.DASHBOARD_LIVENESS_PROBE = "0"; const { createApp } = require("../index"); const { db } = require("../db"); const lanesLib = require("../lib/lanes"); const artifacts = require("../lib/artifacts"); const ws = path.join(ROOT, "ws"); let lane; let server; let base; function request(method, p, body, headers = {}) { return fetch(`${base}${p}`, { method, headers: { "Content-Type": "application/json", ...headers }, body: body === undefined ? undefined : JSON.stringify(body), }) .then(async (r) => ({ status: r.status, headers: r.headers, text: await r.text(), })) .then((r) => { let json = null; try { json = JSON.parse(r.text); } catch { /* not JSON */ } return { ...r, body: json }; }); } before(async () => { fs.mkdirSync(path.join(ws, "out"), { recursive: true }); fs.writeFileSync(path.join(ws, "out", "report.html"), "

hi

"); fs.writeFileSync(path.join(ws, "main.js"), "code"); const skill = path.join(ws, ".claude", "skills", "deploy"); fs.mkdirSync(path.join(skill, "scripts"), { recursive: true }); fs.writeFileSync( path.join(skill, "SKILL.md"), "---\nname: deploy\ndescription: Ship it\n---\nDo it.\n" ); fs.writeFileSync(path.join(skill, "scripts", "run.sh"), "echo hi\n"); fs.mkdirSync(process.env.CLAUDE_HOME, { recursive: true }); lane = lanesLib.createLane({ cwd: ws, title: "ws" }); db.prepare( "INSERT INTO sessions (id, name, status, cwd, lane_id) VALUES ('s1', 's1', 'active', ?, ?)" ).run(ws, lane.id); const ev = db.prepare( "INSERT INTO events (session_id, event_type, tool_name, data) VALUES ('s1', 'PostToolUse', ?, ?)" ); ev.run( "Write", JSON.stringify({ tool_input: { file_path: path.join(ws, "out", "report.html") } }) ); ev.run("Edit", JSON.stringify({ tool_input: { file_path: path.join(ws, "main.js") } })); ev.run("Write", JSON.stringify({ tool_input: { file_path: "/etc/passwd.md" } })); server = http.createServer(createApp()); await new Promise((r) => server.listen(0, "127.0.0.1", r)); base = `http://127.0.0.1:${server.address().port}`; }); after(async () => { await new Promise((r) => server.close(r)); fs.rmSync(ROOT, { recursive: true, force: true }); }); describe("pages", () => { it("accepts only a claude.ai link, and keeps one row per link", async () => { for (const url of ["http://claude.ai/x", "https://evil.example/claude.ai/", "javascript:x"]) { const r = await request("POST", "/api/artifacts/page", { url }); assert.equal(r.status, 400, url); assert.equal(r.body.error.code, "EBADURL"); } const url = "https://claude.ai/code/artifact/abc-123"; const a = await request("POST", "/api/artifacts/page", { url, title: "One" }); assert.equal(a.status, 201); const b = await request("POST", "/api/artifacts/page", { url, title: "Two" }); assert.equal(b.body.item.id, a.body.item.id); assert.equal(b.body.item.title, "Two"); }); it("captures a publish from the Artifact tool's hook, owned by the session's workspace", () => { const row = artifacts.captureFromHook("s1", { tool_input: { action: "publish", file_path: "/x/plan.html" }, tool_response: "Published: https://claude.ai/artifact/zz9-Q\nWatching it.", }); assert.equal(row.url, "https://claude.ai/artifact/zz9-Q"); assert.equal(row.title, "plan.html"); assert.equal(row.lane_id, lane.id); assert.equal(artifacts.captureFromHook("s1", { tool_input: { action: "read" } }), null); assert.equal(artifacts.captureFromHook("s1", { tool_response: "no link" }), null); }); }); describe("files", () => { it("lists only documents a session wrote inside the workspace", async () => { const r = await request("GET", `/api/artifacts/candidates?laneId=${lane.id}`); assert.equal(r.status, 200); assert.deepEqual( r.body.items.map((i) => i.path), ["out/report.html"] ); }); it("pins a snapshot and serves it sandboxed, never a path outside it", async () => { const r = await request("POST", "/api/artifacts/file", { laneId: lane.id, path: "out/report.html", }); assert.equal(r.status, 201, r.text); const id = r.body.item.id; // The live file changes; the artifact does not. fs.writeFileSync(path.join(ws, "out", "report.html"), "changed"); const raw = await request("GET", `/api/artifacts/${id}/raw`); assert.equal(raw.status, 200); assert.match(raw.text, /

hi<\/h1>/); assert.match(raw.headers.get("content-security-policy"), /sandbox/); assert.equal(raw.headers.get("x-content-type-options"), "nosniff"); const other = await request("GET", `/api/artifacts/${id}/raw?path=../../t.db`); assert.equal(other.status, 404); const escape = await request("POST", "/api/artifacts/file", { laneId: lane.id, path: "../t.db", }); assert.equal(escape.status, 400); const del = await request("DELETE", `/api/artifacts/${id}`); assert.equal(del.status, 200); assert.equal((await request("GET", `/api/artifacts/${id}`)).status, 404); }); }); describe("skills", () => { it("snapshots a workspace skill and installs it into the caller's Claude home", async () => { const list = await request("GET", `/api/artifacts/skills?laneId=${lane.id}`); assert.deepEqual(list.body.items, [{ name: "deploy", from: "workspace" }]); const r = await request("POST", "/api/artifacts/skill", { laneId: lane.id, name: "deploy" }); assert.equal(r.status, 201, r.text); assert.equal(r.body.item.description, "Ship it"); assert.deepEqual(r.body.item.files.map((f) => f.path).sort(), ["SKILL.md", "scripts/run.sh"]); const id = r.body.item.id; const first = await request("POST", `/api/artifacts/${id}/install`, {}); assert.equal(first.status, 200, first.text); const target = path.join(process.env.CLAUDE_HOME, "skills", "deploy"); assert.equal(fs.readFileSync(path.join(target, "scripts", "run.sh"), "utf8"), "echo hi\n"); const again = await request("POST", `/api/artifacts/${id}/install`, {}); assert.equal(again.status, 409); assert.equal(again.body.error.code, "EEXISTS"); fs.writeFileSync(path.join(target, "mine.txt"), "keep me"); const replaced = await request("POST", `/api/artifacts/${id}/install`, { replace: true }); assert.equal(replaced.status, 200, replaced.text); assert.ok(!fs.existsSync(path.join(target, "mine.txt"))); const aside = path.join(process.env.CLAUDE_HOME, "agenthub-replaced-skills"); const [old] = fs.readdirSync(aside); assert.equal(fs.readFileSync(path.join(aside, old, "mine.txt"), "utf8"), "keep me"); }); it("refuses a skill holding a symlink or a secret-looking file", async () => { const bad = path.join(ws, ".claude", "skills", "leaky"); fs.mkdirSync(bad, { recursive: true }); fs.writeFileSync(path.join(bad, "SKILL.md"), "x"); fs.symlinkSync("/etc/passwd", path.join(bad, "link")); const r = await request("POST", "/api/artifacts/skill", { laneId: lane.id, name: "leaky" }); assert.equal(r.body.error.code, "ESYMLINK"); fs.unlinkSync(path.join(bad, "link")); fs.writeFileSync(path.join(bad, ".env"), "KEY=1"); const s = await request("POST", "/api/artifacts/skill", { laneId: lane.id, name: "leaky" }); assert.equal(s.body.error.code, "ESECRET"); }); it("is not installed while an admin has turned installs off", async () => { const item = (await request("GET", "/api/artifacts?kind=skill")).body.items[0]; const off = await request("PUT", "/api/artifacts/settings", { installsEnabled: false }); assert.equal(off.body.installsEnabled, false); const r = await request("POST", `/api/artifacts/${item.id}/install`, { replace: true }); assert.equal(r.status, 403); assert.equal(r.body.error.code, "EINSTALLSOFF"); await request("PUT", "/api/artifacts/settings", { installsEnabled: true }); }); }); describe("plugins", () => { it("takes only name@marketplace from owner/repo or a remote URL, and installs without -y", async () => { const bad = await request("POST", "/api/artifacts/plugin", { key: "x@y", market: "/etc" }); assert.equal(bad.body.error.code, "EBADMARKET"); const dash = await request("POST", "/api/artifacts/plugin", { key: "-x@y", market: "o/r" }); assert.equal(dash.body.error.code, "EBADPLUGIN"); const r = await request("POST", "/api/artifacts/plugin", { key: "tool@market", market: "o/r" }); assert.equal(r.status, 201, r.text); const calls = []; artifacts.__setExecImpl(async (cmd, args) => { calls.push([cmd, ...args]); return { stdout: "" }; }); const inst = await request("POST", `/api/artifacts/${r.body.item.id}/install`, {}); assert.equal(inst.status, 200, inst.text); assert.deepEqual(calls, [ ["claude", "plugin", "marketplace", "add", "o/r"], ["claude", "plugin", "install", "--scope", "user", "tool@market"], ]); }); }); function upload(p, files, fields = {}) { const form = new FormData(); const paths = []; for (const [name, content] of files) { form.append("files", new Blob([content]), name.split("/").pop()); paths.push(name); } form.append("paths", JSON.stringify(paths)); for (const [k, v] of Object.entries(fields)) form.append(k, v); return fetch(`${base}${p}`, { method: "POST", body: form }).then(async (r) => ({ status: r.status, body: await r.json(), })); } const snapText = (item, file = "SKILL.md") => fs.readFileSync( path.join( process.env.DASHBOARD_DATA_DIR, "artifacts", db.prepare("SELECT snapshot FROM artifacts WHERE id = ?").get(item.id).snapshot, file ), "utf8" ); describe("plugins from a marketplace link", () => { const MARKET = JSON.stringify({ name: "sg", description: "Smartgift skills", plugins: [ { name: "sg-dev", source: "./plugins/dev", description: "Dev team" }, { name: "sg-common", source: "./plugins/common" }, { name: "-bad", source: "./x" }, ], }); it("normalizes what a user pastes into a marketplace source", () => { const n = artifacts.normalizeMarket; assert.equal(n("https://github.com/o/r"), "o/r"); assert.equal(n("https://github.com/o/r.git/"), "o/r"); assert.equal( n("https://git.example.vn/Org/skills/src/branch/main/"), "https://git.example.vn/Org/skills" ); assert.equal( n("https://git.example.vn/Org/skills.git"), "https://git.example.vn/Org/skills.git" ); assert.throws(() => n("file:///etc"), { code: "EBADMARKET" }); assert.throws(() => n("http://x/y.git"), { code: "EBADMARKET" }); }); it("lists a marketplace's plugins and adds a picked one once", async () => { let asked = null; artifacts.__setInspectImpl(async (m) => { asked = m; return MARKET; }); const r = await request("POST", "/api/artifacts/plugin/inspect", { market: "https://git.example.vn/Org/skills.git", }); artifacts.__setInspectImpl(null); assert.equal(r.status, 200, r.text); assert.equal(asked, "https://git.example.vn/Org/skills.git"); assert.equal(r.body.name, "sg"); assert.deepEqual( r.body.plugins.map((p) => p.key), ["sg-dev@sg", "sg-common@sg"] ); const add = (title) => request("POST", "/api/artifacts/plugin", { key: "sg-dev@sg", market: r.body.market, title }); const a = await add("Dev"); const b = await add("Dev team"); assert.equal(a.status, 201, a.text); assert.equal(b.body.item.id, a.body.item.id); assert.equal(b.body.item.title, "Dev team"); const edit = await request("PATCH", `/api/artifacts/${a.body.item.id}`, { source: "sg-common@sg", market: "https://github.com/o/r", }); assert.equal(edit.status, 200, edit.text); assert.equal(edit.body.item.source, "sg-common@sg"); assert.equal(edit.body.item.market, "o/r"); const bad = await request("PATCH", `/api/artifacts/${a.body.item.id}`, { source: "nope" }); assert.equal(bad.body.error.code, "EBADPLUGIN"); }); it("refuses a marketplace.json with no usable name", async () => { artifacts.__setInspectImpl(async () => "{}"); const r = await request("POST", "/api/artifacts/plugin/inspect", { market: "o/r" }); artifacts.__setInspectImpl(null); assert.equal(r.body.error.code, "EBADMARKET"); }); }); describe("skills from files or written", () => { it("makes one Markdown file the SKILL.md, named by the user, with a frontmatter", async () => { const r = await upload( "/api/artifacts/skill/upload", [["Notes cho đội.md", "# Do this\nStep 1\n"]], { name: "Hướng dẫn deploy", description: "Use when deploying", } ); assert.equal(r.status, 201, JSON.stringify(r.body)); const [item] = r.body.items; assert.equal(item.source, "Huong-dan-deploy"); assert.deepEqual( item.files.map((f) => f.path), ["SKILL.md"] ); const text = snapText(item); assert.match(text, /^---\nname: Huong-dan-deploy\ndescription: Use when deploying\n---\n/); assert.match(text, /# Do this/); }); it("refuses a skill with no description anywhere", async () => { const r = await upload("/api/artifacts/skill/upload", [["x.md", "body"]], { name: "x" }); assert.equal(r.body.error.code, "ENODESCRIPTION"); }); it("takes a picked folder: the top folder is stripped, junk skipped, the name from the frontmatter", async () => { const r = await upload("/api/artifacts/skill/upload", [ ["my-skill/SKILL.md", "---\nname: release-notes\ndescription: Write notes\n---\nGo."], ["my-skill/scripts/run.sh", "echo"], ["my-skill/.DS_Store", "junk"], ]); assert.equal(r.status, 201, JSON.stringify(r.body)); const [item] = r.body.items; assert.equal(item.source, "release-notes"); assert.equal(item.description, "Write notes"); assert.deepEqual(item.files.map((f) => f.path).sort(), ["SKILL.md", "scripts/run.sh"]); }); it("unpacks a zip holding several skills into one artifact each", async () => { const AdmZip = require("adm-zip"); const zip = new AdmZip(); zip.addFile("pack/alpha/SKILL.md", Buffer.from("---\nname: alpha\ndescription: A\n---\n")); zip.addFile("pack/alpha/ref/more.md", Buffer.from("x")); zip.addFile("pack/beta/skill.md", Buffer.from("---\ndescription: B\n---\nb")); zip.addFile("__MACOSX/pack/._alpha", Buffer.from("junk")); const r = await upload("/api/artifacts/skill/upload", [["pack.zip", zip.toBuffer()]]); assert.equal(r.status, 201, JSON.stringify(r.body)); assert.deepEqual(r.body.items.map((i) => i.source).sort(), ["alpha", "beta"]); const beta = r.body.items.find((i) => i.source === "beta"); assert.match(snapText(beta), /^---\nname: beta\ndescription: B\n---/); }); it("stores nothing when one skill of an upload is bad, and refuses a secret or a path out", async () => { const before = db.prepare("SELECT COUNT(*) AS n FROM artifacts WHERE kind = 'skill'").get().n; const r = await upload("/api/artifacts/skill/upload", [ ["a/SKILL.md", "---\ndescription: A\n---\n"], ["b/SKILL.md", "no frontmatter"], ]); assert.equal(r.body.error.code, "ENODESCRIPTION"); assert.equal( db.prepare("SELECT COUNT(*) AS n FROM artifacts WHERE kind = 'skill'").get().n, before ); const secret = await upload("/api/artifacts/skill/upload", [ ["s/SKILL.md", "---\ndescription: S\n---\n"], ["s/.env", "K=1"], ]); assert.equal(secret.body.error.code, "ESECRET"); const out = await upload("/api/artifacts/skill/upload", [["../SKILL.md", "x"]], { description: "d", }); assert.equal(out.body.error.code, "EBADPATH"); const none = await upload("/api/artifacts/skill/upload", [ ["a.png", "x"], ["b.png", "y"], ]); assert.equal(none.body.error.code, "ENOSKILLMD"); }); it("writes a skill from the dialog", async () => { const r = await request("POST", "/api/artifacts/skill/new", { name: "standup", description: "Use for the daily standup: summarise yesterday", content: "Summarise.", }); assert.equal(r.status, 201, r.text); assert.match( snapText(r.body.item), /^---\nname: standup\ndescription: "Use for the daily standup: summarise yesterday"\n---\n\nSummarise\./ ); const bad = await request("POST", "/api/artifacts/skill/new", { name: "!!!", description: "d", content: "c", }); assert.equal(bad.body.error.code, "EBADNAME"); }); it("keeps an uploaded file as a snapshot, by its own base name", async () => { const r = await upload("/api/artifacts/file/upload", [["report.html", "

x

"]], { title: "Report", }); assert.equal(r.status, 201, JSON.stringify(r.body)); assert.equal(r.body.item.kind, "file"); assert.equal(r.body.item.title, "Report"); const raw = await request("GET", `/api/artifacts/${r.body.item.id}/raw`); assert.equal(raw.text, "

x

"); assert.match(raw.headers.get("content-security-policy"), /sandbox/); }); }); describe("sharing and visibility", () => { it("refuses to share in local mode", async () => { const item = (await request("GET", "/api/artifacts")).body.items[0]; const r = await request("PATCH", `/api/artifacts/${item.id}`, { shared: true }); assert.equal(r.status, 400); assert.equal(r.body.error.code, "ELOCALMODE"); }); it("shows a server-mode developer their own rows and shared ones only", () => { db.prepare( "INSERT INTO artifacts (kind, title, owner_id, url) VALUES ('page', 'a', 7, 'https://claude.ai/a')" ).run(); db.prepare( "INSERT INTO artifacts (kind, title, owner_id, url, shared) VALUES ('page', 'b', 8, 'https://claude.ai/b', 1)" ).run(); db.prepare( "INSERT INTO artifacts (kind, title, owner_id, url) VALUES ('page', 'c', 8, 'https://claude.ai/c')" ).run(); const dev = { method: "GET", user: { id: 7, role: "developer", username: "d" } }; const titles = artifacts .list(dev) .map((i) => i.title) .sort(); assert.deepEqual(titles, ["a", "b"]); assert.deepEqual( artifacts.list(dev, { scope: "hub" }).map((i) => i.title), ["b"] ); const hidden = db.prepare("SELECT id FROM artifacts WHERE title = 'c'").get().id; assert.equal(artifacts.canSeeId(dev, hidden), false); const shared = db.prepare("SELECT id FROM artifacts WHERE title = 'b'").get().id; assert.throws(() => artifacts.update(dev, shared, { title: "x" }), { code: "EFORBIDDEN" }); }); it("refuses a cross-origin write", async () => { const r = await request( "POST", "/api/artifacts/page", { url: "https://claude.ai/artifact/q" }, { Origin: "https://evil.example" } ); assert.equal(r.status, 403); }); }); describe("search, frontmatter and marketplace limits", () => { it("finds a title holding `_` or `%` literally, and only that one", () => { db.prepare( "INSERT INTO artifacts (kind, title, url) VALUES ('page', 'my_skill', 'https://claude.ai/u1')" ).run(); db.prepare( "INSERT INTO artifacts (kind, title, url) VALUES ('page', 'myXskill', 'https://claude.ai/u2')" ).run(); db.prepare( "INSERT INTO artifacts (kind, title, url) VALUES ('page', '100% done', 'https://claude.ai/u3')" ).run(); assert.deepEqual( artifacts.list(null, { q: "my_skill" }).map((i) => i.title), ["my_skill"] ); assert.deepEqual( artifacts.list(null, { q: "100%" }).map((i) => i.title), ["100% done"] ); }); it("replaces a multi-line description whole, and quotes one YAML would misread", () => { const src = "---\nname: x\ndescription: >\n old line one\n old line two\nallowed-tools: Read\n---\nbody\n"; assert.equal( artifacts.withFrontmatter(src, "x", "new desc"), "---\nname: x\ndescription: new desc\nallowed-tools: Read\n---\nbody\n" ); assert.match( artifacts.withFrontmatter("body", "x", "- Use for reports"), /^description: "- Use for reports"$/m ); assert.match( artifacts.withFrontmatter("body", "x", "Use for reports"), /^description: Use for reports$/m ); }); it("stops reading a marketplace.json link past 1 MB", async () => { const real = globalThis.fetch; let pulled = 0; globalThis.fetch = async () => { const body = new ReadableStream({ pull(c) { pulled += 1; if (pulled > 100) return c.close(); c.enqueue(new Uint8Array(64 * 1024)); }, }); const res = new Response(body); Object.defineProperty(res, "url", { value: "https://x.example/m.json" }); return res; }; try { await assert.rejects(artifacts.inspectMarket("https://x.example/m.json"), { code: "ETOOBIG", }); } finally { globalThis.fetch = real; } assert.ok(pulled < 30, `read ${pulled} chunks`); }); });