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.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
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()));
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user