forked from bai/curriculum-project-hub
a2c8fa8eaf
将 provider/runner/tools 三件手搓轮子替换为 AI SDK v7 的 generateText + tool:
- 删 openrouter-provider.ts(翻译层消失)、provider.ts 自定义消息类型
- runner.ts 循环体改为 generateText({stopWhen: stepCountIs}),保留 RunRequest/RunResult 公共签名
- tools.ts ToolRegistry 改为 ToolFactory + build(ctx, names?) 按 role 白名单构建 tools record
- workspace/cph/feishu 工具改写为 tool() 定义,execute 闭包捕获 per-run ToolContext
- transcript.ts 改为 ModelMessage[] JSONL(0.0.0 cutover,无迁移)
- server.ts/trigger.ts 接 modelFactory + toolWhitelist
- 测试:helpers 写 MockLanguageModel implements LanguageModelV4;5 个测试改写 + 新增 runner.test.ts
- 移除未使用的 openai 依赖
- ADR-0017 Consequences 同步:loop 委托给 AI SDK,provider seam 是 LanguageModel
tsc 干净,54 tests 全过。
65 lines
2.7 KiB
TypeScript
65 lines
2.7 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
import { cp, mkdtemp, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { ToolRegistry, type ToolContext } from "../../src/agent/tools.js";
|
|
import { cphCheckTool, cphBuildTool } from "../../src/agent/cph.js";
|
|
|
|
const EXAMPLES_DIR = join(process.cwd(), "..", "examples", "TH-141");
|
|
|
|
describe("cph subprocess tools (integration, real cph binary)", () => {
|
|
let ws: string;
|
|
let tools: ToolRegistry;
|
|
|
|
beforeEach(async () => {
|
|
// Use a writable copy of the real TH-141 example as the workspace.
|
|
ws = await mkdtemp(join(tmpdir(), "hub-cph-example-"));
|
|
await cp(EXAMPLES_DIR, ws, { recursive: true });
|
|
tools = new ToolRegistry();
|
|
tools.register("cph_check", cphCheckTool);
|
|
tools.register("cph_build", cphBuildTool);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await rm(ws, { recursive: true, force: true });
|
|
});
|
|
|
|
it("cph_check runs on the TH-141 example and returns diagnostics", async () => {
|
|
const ctx = { runId: "r", projectId: "p", boundChatId: "c", workspaceDir: ws };
|
|
const out = await executeTool(tools, "cph_check", {}, ctx);
|
|
const result = JSON.parse(out) as { exitCode: number; stdout: string; stderr: string };
|
|
|
|
// cph check exits 0 on a legal lesson (no error diagnostics, ADR-0010).
|
|
// If the example has warnings, exit is still 0.
|
|
expect(result.exitCode).toBe(0);
|
|
}, 30000);
|
|
|
|
it("cph_build renders the student target PDF", async () => {
|
|
const ctx = { runId: "r", projectId: "p", boundChatId: "c", workspaceDir: ws };
|
|
const out = await executeTool(tools, "cph_build", { target: "student", output: "build/test-student.pdf" }, ctx);
|
|
const result = JSON.parse(out) as { exitCode: number; stdout: string; stderr: string; output: string };
|
|
|
|
expect(result.exitCode).toBe(0);
|
|
}, 30000);
|
|
|
|
it("cph_check on a temp dir with no engineering file returns non-zero", async () => {
|
|
const empty = await mkdtemp(join(tmpdir(), "hub-cph-empty-"));
|
|
try {
|
|
const ctx = { runId: "r", projectId: "p", boundChatId: "c", workspaceDir: empty };
|
|
const out = await executeTool(tools, "cph_check", {}, ctx);
|
|
const result = JSON.parse(out) as { exitCode: number };
|
|
expect(result.exitCode).not.toBe(0);
|
|
} finally {
|
|
await rm(empty, { recursive: true, force: true });
|
|
}
|
|
}, 15000);
|
|
});
|
|
|
|
async function executeTool(tools: ToolRegistry, name: string, args: unknown, ctx: ToolContext): Promise<string> {
|
|
const def = tools.build(ctx)[name];
|
|
if (def?.execute === undefined) {
|
|
throw new Error(`missing executable tool: ${name}`);
|
|
}
|
|
return def.execute(args, { toolCallId: "call", messages: [], context: undefined }) as Promise<string>;
|
|
}
|