forked from EduCraft/curriculum-project-hub
feat: make agent roles and skills dynamic
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { mkdir, mkdtemp, realpath, rm } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { runAgent } from "../../src/agent/runner.js";
|
||||
import { importSkillDirectory } from "../../src/agent/skillStore.js";
|
||||
|
||||
const queryMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
@@ -76,7 +77,7 @@ describe("runAgent", () => {
|
||||
workspaceRoot = await realpath(workspaceRoot);
|
||||
workspace = await realpath(workspace);
|
||||
previousSecrets = Object.fromEntries(
|
||||
["DATABASE_URL", "FEISHU_APP_SECRET", "HUB_SESSION_SECRET"].map((name) => [name, process.env[name]]),
|
||||
["DATABASE_URL", "FEISHU_APP_SECRET", "HUB_SESSION_SECRET", "HUB_SKILL_STORE_ROOT"].map((name) => [name, process.env[name]]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -113,8 +114,7 @@ describe("runAgent", () => {
|
||||
allowDangerouslySkipPermissions: true,
|
||||
settingSources: [],
|
||||
settings: { disableBundledSkills: true },
|
||||
plugins: [expect.objectContaining({ type: "local", skipMcpDiscovery: true })],
|
||||
skills: ["cph-curated:outline", "cph-curated:lesson-project", "cph-curated:data-processing-spec"],
|
||||
skills: [],
|
||||
strictMcpConfig: true,
|
||||
sandbox: expect.objectContaining({
|
||||
enabled: true,
|
||||
@@ -149,7 +149,7 @@ describe("runAgent", () => {
|
||||
|
||||
it("returns the skills actually reported by SDK initialization", async () => {
|
||||
queryMock.mockReturnValue(messages(
|
||||
initMessage(["cph-curated:outline"]),
|
||||
initMessage(["cph-runtime:outline"]),
|
||||
assistantMessage("fresh"),
|
||||
resultMessage("sdk-session-1"),
|
||||
));
|
||||
@@ -164,7 +164,7 @@ describe("runAgent", () => {
|
||||
prisma: stubPrisma,
|
||||
});
|
||||
|
||||
expect(result.initializedSkillIds).toEqual(["cph-curated:outline"]);
|
||||
expect(result.initializedSkillIds).toEqual(["cph-runtime:outline"]);
|
||||
});
|
||||
|
||||
it("maps role tool ids to the Claude SDK tool whitelist", async () => {
|
||||
@@ -183,13 +183,13 @@ describe("runAgent", () => {
|
||||
|
||||
expect(queryMock.mock.calls[0]?.[0]).toMatchObject({
|
||||
options: {
|
||||
tools: ["Read", "Bash", "Skill"],
|
||||
tools: ["Read", "Bash"],
|
||||
allowedTools: ["Read", "Bash", "mcp__cph_hub__send_file"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps only the curated Skill dispatcher for an empty role tool whitelist", async () => {
|
||||
it("disables SDK tools for an empty role tool and skill selection", async () => {
|
||||
queryMock.mockReturnValue(messages(assistantMessage("ok"), resultMessage("sdk-session-1")));
|
||||
|
||||
await runAgent({
|
||||
@@ -205,12 +205,42 @@ describe("runAgent", () => {
|
||||
|
||||
expect(queryMock.mock.calls[0]?.[0]).toMatchObject({
|
||||
options: {
|
||||
tools: ["Skill"],
|
||||
tools: [],
|
||||
allowedTools: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("loads only the dynamic skills selected by the role", async () => {
|
||||
const source = join(root, "skill-source");
|
||||
const storeRoot = join(root, "skill-store");
|
||||
await mkdir(source);
|
||||
await writeFile(join(source, "SKILL.md"), "---\nname: typst\ndescription: Typst\n---\n");
|
||||
const installed = await importSkillDirectory({ sourceDir: source, storeRoot });
|
||||
process.env["HUB_SKILL_STORE_ROOT"] = storeRoot;
|
||||
queryMock.mockReturnValue(messages(assistantMessage("ok"), resultMessage("sdk-session-1")));
|
||||
|
||||
await runAgent({
|
||||
prompt: "排版",
|
||||
model: undefined,
|
||||
project: { projectId: "p", boundChatId: "c", workspaceRoot, workspaceDir: workspace },
|
||||
systemPrompt: undefined,
|
||||
tools: [],
|
||||
skills: [{ name: "typst", version: "0.15.0", contentDigest: installed.contentDigest }],
|
||||
runId: "run-skill",
|
||||
sessionId: "hub-session-1",
|
||||
prisma: stubPrisma,
|
||||
});
|
||||
|
||||
expect(queryMock.mock.calls[0]?.[0]).toMatchObject({
|
||||
options: {
|
||||
tools: ["Skill"],
|
||||
plugins: [expect.objectContaining({ type: "local", skipMcpDiscovery: true })],
|
||||
skills: ["cph-runtime:typst"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("returns SDK-reported cost when present", async () => {
|
||||
queryMock.mockReturnValue(messages(assistantMessage("ok"), resultMessage("sdk-session-1", 0.0042)));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user