Files
2026-06-08 11:25:54 +08:00

150 lines
6.9 KiB
TypeScript

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()
};
}