150 lines
6.9 KiB
TypeScript
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()
|
|
};
|
|
}
|