102 lines
5.7 KiB
TypeScript
102 lines
5.7 KiB
TypeScript
import { describe, expect, it, spyOn } from "bun:test";
|
|
import { tool } from "../../.opencode/tool.js";
|
|
import publishReport from "../../.opencode/tools/publish_task_report.js";
|
|
import { taskReportSchema } from "../../src/product/investigation-report.js";
|
|
|
|
const native = tool.schema.object(publishReport.args);
|
|
const requestId = "60000000-0000-4000-8000-000000000001";
|
|
const report = {
|
|
title: "候选隔离方案", current_conclusion: "已取得候选与隔离分析", conclusion_boundary: "候选尚待现场核验",
|
|
stage: "final", outcome: "concluded", evidence_refs: ["query-1"],
|
|
recommendations: ["核验阀门可用性"], pending_items: ["等待现场回执"], data_gaps: [],
|
|
};
|
|
const block = { block_id: "summary", fallback_text: "已取得有边界的方案。", spec: {
|
|
root: "text", elements: { text: { type: "Text", children: [], props: { text: "已取得有边界的方案。", tone: "neutral" } } },
|
|
} };
|
|
const gap = { gap_id: "sample", scope: "投诉时段", missing_data: "实际采样", required_data: "现场采样结果",
|
|
impact: "无法形成必要判断", evidence_refs: ["query-1"], blocking: true };
|
|
|
|
describe("report publication contract", () => {
|
|
it("advertises outcome-specific constraints without requiring host-owned fields", () => {
|
|
const schema = tool.schema.toJSONSchema(native, { io: "input" });
|
|
const reportSchema = schema.properties!.report as { anyOf: Array<{ properties: Record<string, { const?: string }>; required?: string[] }> };
|
|
const concluded = reportSchema.anyOf.find((variant) => variant.properties.outcome.const === "concluded")!;
|
|
const insufficient = reportSchema.anyOf.find((variant) => variant.properties.outcome.const === "insufficient_data")!;
|
|
expect(concluded.required).not.toContain("stop_reason");
|
|
expect(insufficient.required).toContain("stop_reason");
|
|
expect(insufficient.properties.stage.const).toBe("final");
|
|
});
|
|
it("supplies fixed protocol fields and accepts the known misplaced version tag without forwarding it", () => {
|
|
const input = { ...report, ui_blocks: [{ version: "answer-ui@1", block, evidence_refs: ["query-1"] }] };
|
|
const args = native.parse({ request_id: requestId, report: input, evidence: [] });
|
|
const canonical = taskReportSchema.parse(args.report);
|
|
expect(canonical.stop_reason).toBeNull();
|
|
expect(canonical.ui_blocks?.[0].block.version).toBe("answer-ui@1");
|
|
expect(canonical.ui_blocks?.[0]).not.toHaveProperty("version");
|
|
expect(taskReportSchema.parse(input)).toEqual(canonical);
|
|
for (const ui_blocks of [
|
|
[{ block: { ...block, version: "unsupported" }, evidence_refs: ["query-1"] }],
|
|
[{ version: "unsupported", block, evidence_refs: ["query-1"] }],
|
|
]) {
|
|
expect(native.safeParse({ request_id: requestId, report: { ...report, ui_blocks }, evidence: [] }).success).toBe(false);
|
|
expect(taskReportSchema.safeParse({ ...report, ui_blocks }).success).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("exposes the same outcome conditions at native and host boundaries", () => {
|
|
const insufficient = { ...report, outcome: "insufficient_data", recommendations: [], pending_items: [],
|
|
stop_reason: "无法取得关键采样", data_gaps: [gap] };
|
|
for (const [input, accepted] of [
|
|
[report, true], [insufficient, true],
|
|
[{ ...report, stop_reason: "本轮已结束" }, false],
|
|
[{ ...report, data_gaps: [gap] }, false],
|
|
[{ ...insufficient, stage: "interim" }, false],
|
|
[{ ...insufficient, stop_reason: null }, false],
|
|
[{ ...insufficient, recommendations: ["继续采样"] }, false],
|
|
[{ ...insufficient, pending_items: ["等人"] }, false],
|
|
[{ ...insufficient, data_gaps: [{ ...gap, blocking: false }] }, false],
|
|
[{ ...insufficient, data_gaps: [{ ...gap, evidence_refs: [] }] }, false],
|
|
] as const) {
|
|
expect(native.safeParse({ request_id: requestId, report: input, evidence: [] }).success).toBe(accepted);
|
|
expect(taskReportSchema.safeParse(input).success).toBe(accepted);
|
|
}
|
|
});
|
|
|
|
it("rejects a contradictory report before issuing any HTTP request", async () => {
|
|
const originalToken = process.env.AGENT_INTERNAL_TOKEN;
|
|
process.env.AGENT_INTERNAL_TOKEN = "test-tool-token";
|
|
const fetch = spyOn(globalThis, "fetch").mockRejectedValue(new Error("Unexpected HTTP request"));
|
|
try {
|
|
await expect(publishReport.execute({ request_id: requestId, evidence: [],
|
|
report: { ...report, stop_reason: "本轮已结束" },
|
|
} as never, { sessionID: "test" })).rejects.toThrow("已形成结论");
|
|
expect(fetch).not.toHaveBeenCalled();
|
|
} finally {
|
|
fetch.mockRestore();
|
|
if (originalToken === undefined) delete process.env.AGENT_INTERNAL_TOKEN;
|
|
else process.env.AGENT_INTERNAL_TOKEN = originalToken;
|
|
}
|
|
});
|
|
|
|
it("sends only canonical report fields to the bridge", async () => {
|
|
const originalToken = process.env.AGENT_INTERNAL_TOKEN;
|
|
process.env.AGENT_INTERNAL_TOKEN = "test-tool-token";
|
|
const fetch = spyOn(globalThis, "fetch").mockResolvedValue(new Response('{"report_revision":1}'));
|
|
try {
|
|
const input = { request_id: requestId, evidence: [], report: { ...report,
|
|
ui_blocks: [{ version: "answer-ui@1", block, evidence_refs: ["query-1"] }],
|
|
} };
|
|
await publishReport.execute(input as never, { sessionID: "test" });
|
|
const body = JSON.parse(String(fetch.mock.calls[0]?.[1]?.body));
|
|
expect(body.report.stop_reason).toBeNull();
|
|
expect(body.report.ui_blocks[0].block.version).toBe("answer-ui@1");
|
|
expect(body.report.ui_blocks[0]).not.toHaveProperty("version");
|
|
expect(body.report.current_conclusion).toBe(report.current_conclusion);
|
|
} finally {
|
|
fetch.mockRestore();
|
|
if (originalToken === undefined) delete process.env.AGENT_INTERNAL_TOKEN;
|
|
else process.env.AGENT_INTERNAL_TOKEN = originalToken;
|
|
}
|
|
});
|
|
});
|