Files
next-tjwater-agent/tests/product/taskReportPublication.test.ts
T
jiang 874bfba907
Generic Container CI/CD / test-build-publish (push) Successful in 56s
Agent CI/CD / build-test-publish-and-deploy (push) Successful in 56s
fix: harden report tools and guide investigation presentation
2026-11-02 15:23:34 +08:00

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