import assert from "node:assert/strict"; import { lstat, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { test } from "bun:test"; import { createCloudApiServer } from "./server.ts"; import { discoverSkillsForStdio } from "./codex-stdio-session-helpers.ts"; test("cloud api skills upload/list/tree/preview keeps preinstalled and uploaded skill roots separate", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-skills-api-")); const preinstalledDir = path.join(root, "app-skills"); const userSkillsDir = path.join(root, "user-skills"); const workspace = path.join(root, "workspace", "hwlab"); await mkdir(path.join(preinstalledDir, "same-name"), { recursive: true }); await writeFile(path.join(preinstalledDir, "same-name", "SKILL.md"), skillMarkdown("same-name", "Preinstalled skill.")); const server = createCloudApiServer({ env: { PATH: process.env.PATH, HWLAB_PREINSTALLED_SKILLS_DIR: preinstalledDir, HWLAB_USER_SKILLS_DIR: userSkillsDir, HWLAB_CODE_AGENT_WORKSPACE: workspace, HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace, HWLAB_CODE_AGENT_CODEX_HOME: path.join(root, "codex-home") } }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); try { const baseUrl = `http://127.0.0.1:${server.address().port}`; const created = await fetchJson(`${baseUrl}/v1/skills/uploads`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ files: [ { relativePath: "uploaded-same-name/SKILL.md", contentBase64: Buffer.from(skillMarkdown("same-name", "Uploaded skill."), "utf8").toString("base64") }, { relativePath: "uploaded-same-name/references/note.txt", contentBase64: Buffer.from("uploaded note", "utf8").toString("base64") } ] }) }); assert.equal(created.status, 201); assert.equal(created.body.accepted, true); assert.equal(created.body.skill.source, "uploaded"); assert.equal(created.body.skill.name, "same-name"); assert.equal(created.body.skill.rootDir.startsWith(userSkillsDir), true); assert.equal(created.body.skill.rootDir.startsWith(preinstalledDir), false); assert.equal(created.body.skill.manifestPath, path.join(created.body.skill.rootDir, "SKILL.md")); const list = await fetchJson(`${baseUrl}/v1/skills`); assert.equal(list.status, 200); assert.equal(list.body.roots.preinstalled, preinstalledDir); assert.equal(list.body.roots.uploaded, userSkillsDir); assert.equal(list.body.agentRunAssembly.status, "ready"); assert.equal(list.body.agentRunAssembly.resourceBundle.kind, "gitbundle"); assert.equal(list.body.agentRunAssembly.resourceBundle.skillsDir, ".agents/skills"); assert.deepEqual(list.body.agentRunAssembly.bundles, [ { name: "hwlab-tools", subpath: "tools", target_path: "tools" }, { name: "hwlab-agent-skills", subpath: "skills", target_path: ".agents/skills" } ]); assert.deepEqual(list.body.agentRunAssembly.promptRefs.map((item) => item.name), ["hwlab-v02-runtime"]); assert.equal(list.body.agentRunAssembly.promptAssembly.status, "ready"); assert.equal(list.body.agentRunAssembly.promptAssembly.valuesPrinted, false); assert.equal(list.body.agentRunAssembly.promptAssembly.requiredCount, 1); assert.deepEqual(list.body.agentRunAssembly.promptAssembly.injectStages, ["thread-start"]); assert.deepEqual(list.body.agentRunAssembly.promptAssembly.refs, [{ name: "hwlab-v02-runtime", path: "internal/agent/prompts/hwlab-v02-runtime.md", inject: "thread-start", stage: "thread-start initial prompt", required: true, source: "ResourceBundleRef.promptRefs" }]); assert.equal(Object.hasOwn(list.body.agentRunAssembly, "toolAliases"), false); assert.equal(Object.hasOwn(list.body.agentRunAssembly, "skillRefs"), false); const sameName = list.body.skills.filter((skill) => skill.name === "same-name"); assert.equal(sameName.length, 2); assert.deepEqual(sameName.map((skill) => skill.source).sort(), ["preinstalled", "uploaded"]); const tree = await fetchJson(`${baseUrl}/v1/skills/${encodeURIComponent(created.body.skill.id)}/tree`); assert.equal(tree.status, 200); assert.ok(tree.body.tree.some((entry) => entry.path === "SKILL.md" && entry.type === "file")); assert.ok(tree.body.tree.some((entry) => entry.path === "references/note.txt" && entry.type === "file")); const preview = await fetchJson(`${baseUrl}/v1/skills/${encodeURIComponent(created.body.skill.id)}/files?path=SKILL.md`); assert.equal(preview.status, 200); assert.match(preview.body.file.content, /Uploaded skill/u); assert.equal(await readFile(path.join(preinstalledDir, "same-name", "SKILL.md"), "utf8"), skillMarkdown("same-name", "Preinstalled skill.")); const aggregation = path.join(workspace, ".agents", "skills"); assert.equal((await lstat(path.join(aggregation, "preinstalled-same-name"))).isSymbolicLink(), true); assert.equal((await lstat(path.join(aggregation, `uploaded-${created.body.skill.id}`))).isSymbolicLink(), true); } finally { await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))); await rm(root, { recursive: true, force: true }); } }); test("codex stdio skills discovery keeps same-name manifests from different roots", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-skills-discovery-")); const preinstalledDir = path.join(root, "app-skills"); const userSkillsDir = path.join(root, "user-skills"); await mkdir(path.join(preinstalledDir, "same-name"), { recursive: true }); await mkdir(path.join(userSkillsDir, "skill-uploaded"), { recursive: true }); await writeFile(path.join(preinstalledDir, "same-name", "SKILL.md"), skillMarkdown("same-name", "Preinstalled skill.")); await writeFile(path.join(userSkillsDir, "skill-uploaded", "SKILL.md"), skillMarkdown("same-name", "Uploaded skill.")); try { const discovered = await discoverSkillsForStdio({ env: { HWLAB_CODE_AGENT_SKILLS_DIRS: `${preinstalledDir}${path.delimiter}${userSkillsDir}` } }); assert.equal(discovered.status, "ready"); const sameName = discovered.items.filter((skill) => skill.name === "same-name"); assert.equal(sameName.length, 2); assert.deepEqual(sameName.map((skill) => skill.sourceRoot).sort(), [preinstalledDir, userSkillsDir].sort()); } finally { await rm(root, { recursive: true, force: true }); } }); function skillMarkdown(name, description) { return [ "---", `name: ${name}`, `description: ${description}`, "version: v-test", "---", "", `# ${name}`, "", description ].join("\n"); } async function fetchJson(url, options = {}) { const response = await fetch(url, options); return { status: response.status, body: await response.json() }; }