Files
next-tjwater-agent/tests/product/investigationReferenceContract.test.ts
T
jiang db87323b69 feat(investigation): publish incremental reference graphs
Publish graph deltas using the Server-owned cumulative graph contract. Validate historical comparison chart publications and consolidate Agent documentation.
2026-11-11 19:10:41 +08:00

88 lines
5.0 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import express from "express";
import { tool } from "../../.opencode/tool.js";
import publishProgress from "../../.opencode/tools/publish_task_progress.js";
import type { OperationalTaskClient } from "../../src/product/operational-task-client.js";
import { investigationNodeAnnotations } from "../../src/product/investigation-node-state.js";
import { registerTaskPublicationBridge } from "../../src/routes/task-publication-bridge.js";
import { removeRuntimeSessionContext, setRuntimeSessionContext } from "../../src/runtime/sessionContext.js";
const native = tool.schema.object(publishProgress.args);
describe("investigation reference contract", () => {
it("advertises five optional annotations to the model without execution identities or Mermaid", () => {
const schema = tool.schema.toJSONSchema(native, { io: "input" });
const progress = schema.properties!.progress as {
properties: Record<string, {
properties: { nodes: { items: { properties: { status: { anyOf: Array<{ enum?: unknown[] }> } }; required?: string[] } } };
}>;
};
const node = progress.properties.graph_update.properties.nodes.items;
const statuses = node.properties.status.anyOf.flatMap((variant) => variant.enum ?? []);
expect(statuses).toEqual([...investigationNodeAnnotations]);
expect(node.required).not.toContain("status");
expect(Object.keys(schema.properties!)).toEqual(["request_id", "progress"]);
expect(Object.keys(progress.properties)).toEqual(["summary", "graph_update"]);
});
it("transports a native graph update and text-only update through the trusted task bridge", async () => {
const runtimeSessionId = "reference-contract-native-session";
const taskId = "50000000-0000-4000-8000-000000000031";
const token = "reference-contract-test-token";
const published: unknown[][] = [];
setRuntimeSessionContext({
sessionId: runtimeSessionId, clientSessionId: "reference-contract-client", actorKey: "test-actor",
projectKey: "test-project", traceId: "test", taskId, purpose: "task_execution",
});
const app = express();
app.use(express.json());
registerTaskPublicationBridge(app, { internalToken: token, client: {
publishArtifacts: async (...args: unknown[]) => {
published.push(args);
return { progress_revision: published.length };
},
} as unknown as Pick<OperationalTaskClient, "publishArtifacts" | "publishPresentation"> });
const server = app.listen(0, "127.0.0.1");
await new Promise<void>((resolve) => server.once("listening", resolve));
const baseUrl = `http://127.0.0.1:${(server.address() as { port: number }).port}`;
const oldToken = process.env.AGENT_INTERNAL_TOKEN;
const oldBaseUrl = process.env.TJWATER_AGENT_INTERNAL_BASE_URL;
process.env.AGENT_INTERNAL_TOKEN = token;
process.env.TJWATER_AGENT_INTERNAL_BASE_URL = baseUrl;
try {
const requestId = "60000000-0000-4000-8000-000000000031";
const progress = { summary: "整理已有路线与结果", graph_update: {
nodes: investigationNodeAnnotations.map((status, index) => ({
node_id: `step_${index}`, label: `参考步骤 ${index}`, status,
...(status === "failed" ? { detail: "此前查询超时,保留失败路径" } : {}),
})), edges: [{ source: "step_3", target: "step_0", label: "替代路线" }],
} };
const args = native.parse({ request_id: requestId, progress });
expect(JSON.parse(await publishProgress.execute(args, { sessionID: runtimeSessionId }))).toEqual({ progress_revision: 1 });
expect(published[0]?.slice(0, 2)).toEqual([runtimeSessionId, taskId]);
expect(published[0]?.[2]).toMatchObject({ requestId, sourceSessionId: "reference-contract-client",
progress: { ...progress, diagram: null },
});
await publishProgress.execute(native.parse({ request_id: "60000000-0000-4000-8000-000000000032", progress: { summary: "补充本轮说明" } }),
{ sessionID: runtimeSessionId });
expect(published[1]?.[2]).toMatchObject({ progress: { summary: "补充本轮说明", diagram: null } });
const invalid = await fetch(`${baseUrl}/internal/operational-task/artifacts`, {
method: "POST", headers: { "Content-Type": "application/json", "x-agent-internal-token": token },
body: JSON.stringify({ session_id: runtimeSessionId, request_id: requestId, progress: {
summary: "旧执行状态不进入新图", graph_update: { nodes: [{ node_id: "old", label: "核查", status: "running" }] },
} }),
});
expect(invalid.status).toBe(400);
expect(published).toHaveLength(2);
} finally {
if (oldToken === undefined) delete process.env.AGENT_INTERNAL_TOKEN;
else process.env.AGENT_INTERNAL_TOKEN = oldToken;
if (oldBaseUrl === undefined) delete process.env.TJWATER_AGENT_INTERNAL_BASE_URL;
else process.env.TJWATER_AGENT_INTERNAL_BASE_URL = oldBaseUrl;
removeRuntimeSessionContext(runtimeSessionId);
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});
});