feat(agent): add controlled task execution

This commit is contained in:
2026-09-24 04:19:02 +08:00
parent d018bcae67
commit b31033e9ce
48 changed files with 14498 additions and 86 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ const disabledServerTools = [
];
describe("disabled opencode tools", () => {
it("exposes only the two SCADA frontend-action tools", () => {
it("keeps removed generic server tools disabled while retaining SCADA frontend actions", () => {
for (const toolName of disabledServerTools) {
expect(existsSync(`.opencode/tools/${toolName}.ts`)).toBeFalse();
}
+266
View File
@@ -0,0 +1,266 @@
import { afterEach, describe, expect, it } from "bun:test";
let bridgeServer: ReturnType<typeof Bun.serve> | undefined;
afterEach(() => {
bridgeServer?.stop(true);
bridgeServer = undefined;
});
describe("OpenCode internal bridge tools", () => {
it("uses the documented AGENT_INTERNAL_TOKEN from an external process", async () => {
let receivedToken: string | null = null;
let receivedSessionId: string | null = null;
bridgeServer = Bun.serve({
hostname: "127.0.0.1",
port: 0,
fetch(request) {
const url = new URL(request.url);
receivedToken = request.headers.get("x-agent-internal-token");
receivedSessionId = url.searchParams.get("session_id");
return Response.json({ task: { task_id: "task-1" } });
},
});
const child = Bun.spawn([
processExecPath(),
"-e",
[
'import tool from "./.opencode/tools/get_operational_task_context.ts";',
'const result = await tool.execute({}, { sessionID: "runtime-session-1" });',
"process.stdout.write(result);",
].join("\n"),
], {
cwd: process.cwd(),
env: {
...process.env,
AGENT_INTERNAL_TOKEN: "external-shared-token",
TJWATER_AGENT_INTERNAL_BASE_URL: `http://127.0.0.1:${bridgeServer.port}`,
TJWATER_AGENT_INTERNAL_TOKEN: "",
},
stderr: "pipe",
stdout: "pipe",
});
const [exitCode, stdout, stderr] = await Promise.all([
child.exited,
new Response(child.stdout).text(),
new Response(child.stderr).text(),
]);
expect({ exitCode, stderr }).toEqual({ exitCode: 0, stderr: "" });
expect(JSON.parse(stdout)).toEqual({ task: { task_id: "task-1" } });
expect(String(receivedToken)).toBe("external-shared-token");
expect(String(receivedSessionId)).toBe("runtime-session-1");
});
it("fails explicitly before making a bridge request when no shared token exists", async () => {
const child = Bun.spawn([
processExecPath(),
"-e",
[
'import tool from "./.opencode/tools/get_operational_task_context.ts";',
'await tool.execute({}, { sessionID: "runtime-session-1" });',
].join("\n"),
], {
cwd: process.cwd(),
env: {
...process.env,
AGENT_INTERNAL_TOKEN: "",
TJWATER_AGENT_INTERNAL_TOKEN: "",
},
stderr: "pipe",
stdout: "pipe",
});
const [exitCode, stderr] = await Promise.all([
child.exited,
new Response(child.stderr).text(),
]);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("internal Agent tool bridge is not configured");
});
it("publishes execution plans through the authenticated task artifact bridge", async () => {
let receivedBody: Record<string, unknown> | undefined;
bridgeServer = Bun.serve({
hostname: "127.0.0.1",
port: 0,
async fetch(request) {
receivedBody = await request.json() as Record<string, unknown>;
return Response.json({ plan_revision: 1, report_revision: null });
},
});
const child = Bun.spawn([
processExecPath(),
"-e",
[
'import tool from "./.opencode/tools/publish_execution_plan.ts";',
'const result = await tool.execute({ request_id: "60000000-0000-4000-8000-000000000001", title: "调查", reason: "开始", nodes: [{ id: "read", title: "读取", status: "completed", owner: "agent", detail: "完成", evidence: ["evidence-1"], action_refs: [] }], edges: [] }, { sessionID: "runtime-session-1" });',
"process.stdout.write(result);",
].join("\n"),
], {
cwd: process.cwd(),
env: {
...process.env,
AGENT_INTERNAL_TOKEN: "external-shared-token",
TJWATER_AGENT_INTERNAL_BASE_URL: `http://127.0.0.1:${bridgeServer.port}`,
TJWATER_AGENT_INTERNAL_TOKEN: "",
},
stderr: "pipe",
stdout: "pipe",
});
const [exitCode, stdout, stderr] = await Promise.all([
child.exited,
new Response(child.stdout).text(),
new Response(child.stderr).text(),
]);
expect({ exitCode, stderr }).toEqual({ exitCode: 0, stderr: "" });
expect(JSON.parse(stdout)).toEqual({ plan_revision: 1, report_revision: null });
expect(receivedBody).toMatchObject({
session_id: "runtime-session-1",
request_id: "60000000-0000-4000-8000-000000000001",
execution_plan: { title: "调查", reason: "开始" },
});
});
it("fails the execution plan tool when the task artifact bridge rejects it", async () => {
let requestCount = 0;
bridgeServer = Bun.serve({
hostname: "127.0.0.1",
port: 0,
fetch() {
requestCount += 1;
return Response.json({ message: "A running or completed node has incomplete predecessors" }, { status: 409 });
},
});
const child = Bun.spawn([
processExecPath(),
"-e",
[
'import tool from "./.opencode/tools/publish_execution_plan.ts";',
'await tool.execute({ request_id: "60000000-0000-4000-8000-000000000001", title: "调查", reason: "开始", nodes: [{ id: "read", title: "读取", status: "completed", owner: "agent", detail: "完成", evidence: ["evidence-1"], action_refs: [] }], edges: [] }, { sessionID: "runtime-session-1" });',
].join("\n"),
], {
cwd: process.cwd(),
env: {
...process.env,
AGENT_INTERNAL_TOKEN: "external-shared-token",
TJWATER_AGENT_INTERNAL_BASE_URL: `http://127.0.0.1:${bridgeServer.port}`,
TJWATER_AGENT_INTERNAL_TOKEN: "",
},
stderr: "pipe",
stdout: "pipe",
});
const [exitCode, stdout, stderr] = await Promise.all([
child.exited,
new Response(child.stdout).text(),
new Response(child.stderr).text(),
]);
expect(exitCode).not.toBe(0);
expect(stdout).toBe("");
expect(stderr).toContain("task plan publish failed (409)");
expect(requestCount).toBe(1);
});
it("rejects an invalid work-order action payload before a bridge request", async () => {
let requestCount = 0;
bridgeServer = Bun.serve({
hostname: "127.0.0.1",
port: 0,
fetch() {
requestCount += 1;
return Response.json({ action_id: "70000000-0000-4000-8000-000000000001" });
},
});
const child = Bun.spawn([
processExecPath(),
"-e",
[
'import tool from "./.opencode/tools/propose_task_action.ts";',
'await tool.execute({ request_id: "60000000-0000-4000-8000-000000000001", kind: "create_work_order", reason: "现场采样", payload: { title: "采样", summary: "错误字段", priority: "high" }, evidence_refs: [] }, { sessionID: "runtime-session-1" });',
].join("\n"),
], {
cwd: process.cwd(),
env: {
...process.env,
AGENT_INTERNAL_TOKEN: "external-shared-token",
TJWATER_AGENT_INTERNAL_BASE_URL: `http://127.0.0.1:${bridgeServer.port}`,
TJWATER_AGENT_INTERNAL_TOKEN: "",
},
stderr: "pipe",
stdout: "pipe",
});
const [exitCode, stdout] = await Promise.all([
child.exited,
new Response(child.stdout).text(),
new Response(child.stderr).text(),
]);
expect(exitCode).not.toBe(0);
expect(stdout).toBe("");
expect(requestCount).toBe(0);
});
it("sends a valid work-order proposal using the exact Server payload fields", async () => {
let receivedBody: Record<string, unknown> | undefined;
bridgeServer = Bun.serve({
hostname: "127.0.0.1",
port: 0,
async fetch(request) {
receivedBody = await request.json() as Record<string, unknown>;
return Response.json({
action_id: "70000000-0000-4000-8000-000000000001",
approval_status: "pending",
execution_status: "not_submitted",
});
},
});
const child = Bun.spawn([
processExecPath(),
"-e",
[
'import tool from "./.opencode/tools/propose_task_action.ts";',
'const result = await tool.execute({ request_id: "60000000-0000-4000-8000-000000000001", kind: "create_work_order", reason: "现场采样", payload: { title: "采样", description: "采集二次供水水样", work_order_type: "water_quality_sampling", priority: "urgent", location: "联调演示小区" }, evidence_refs: ["evidence-1"], node_id: "field_sampling" }, { sessionID: "runtime-session-1" });',
"process.stdout.write(result);",
].join("\n"),
], {
cwd: process.cwd(),
env: {
...process.env,
AGENT_INTERNAL_TOKEN: "external-shared-token",
TJWATER_AGENT_INTERNAL_BASE_URL: `http://127.0.0.1:${bridgeServer.port}`,
TJWATER_AGENT_INTERNAL_TOKEN: "",
},
stderr: "pipe",
stdout: "pipe",
});
const [exitCode, stdout, stderr] = await Promise.all([
child.exited,
new Response(child.stdout).text(),
new Response(child.stderr).text(),
]);
expect({ exitCode, stderr }).toEqual({ exitCode: 0, stderr: "" });
expect(JSON.parse(stdout)).toMatchObject({ approval_status: "pending" });
expect(receivedBody).toMatchObject({
session_id: "runtime-session-1",
action: {
kind: "create_work_order",
payload: {
title: "采样",
description: "采集二次供水水样",
work_order_type: "water_quality_sampling",
priority: "urgent",
},
},
});
});
});
const processExecPath = () => process.execPath;
+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, it } from "bun:test";
import { isTrustedInternalBridgeRequest } from "../../src/product/internal-bridge-auth.js";
const request = (headers: Record<string, string>) => ({
header: (name: string) => headers[name.toLowerCase()],
});
describe("internal operational-task bridge authentication", () => {
it("requires the server-only secret and rejects browser-originated requests", () => {
expect(isTrustedInternalBridgeRequest(request({ "x-agent-internal-token": "secret" }), "secret")).toBe(true);
expect(isTrustedInternalBridgeRequest(request({ "x-agent-internal-token": "known-session-id" }), "secret")).toBe(false);
expect(isTrustedInternalBridgeRequest(request({
"x-agent-internal-token": "secret",
origin: "https://tjwater.example",
"sec-fetch-site": "same-site",
}), "secret")).toBe(false);
});
});
+343
View File
@@ -0,0 +1,343 @@
import { afterEach, describe, expect, it } from "bun:test";
import {
bindOperationalTaskCredential,
humanTaskEvidenceCommandSchema,
OperationalTaskClient,
proposedTaskActionSchema,
releaseOperationalTaskCredential,
} from "../../src/product/operational-task-client.js";
const originalFetch = globalThis.fetch;
const taskId = "50000000-0000-4000-8000-000000000001";
const runId = "55000000-0000-4000-8000-000000000001";
const executionRequestId = "60000000-0000-4000-8000-000000000001";
const run = {
run_id: runId,
task_id: taskId,
generation: 1,
execution_request_id: executionRequestId,
status: "running",
revision: 2,
agent_session_id: "session-1",
lease_expires_at: "2026-09-23T01:01:00Z",
heartbeat_at: "2026-09-23T01:00:00Z",
started_at: "2026-09-23T01:00:00Z",
finished_at: null,
failure_code: null,
failure_detail: null,
dispatch_attempts: 0,
next_dispatch_at: "2026-09-23T01:00:00Z",
created_at: "2026-09-23T01:00:00Z",
updated_at: "2026-09-23T01:00:00Z",
};
const task = {
task_id: taskId,
task_type: "event_investigation",
workflow: "water_quality_complaint",
lifecycle_status: "open",
active_run: run,
status: "running",
revision: 2,
execution_request_id: executionRequestId,
title: "水质投诉调查",
summary: "调查投诉",
};
afterEach(() => {
globalThis.fetch = originalFetch;
releaseOperationalTaskCredential("runtime-1");
});
describe("OperationalTaskClient", () => {
it("matches proposed action payloads to the generated Server contract", () => {
const common = {
request_id: "60000000-0000-4000-8000-000000000001",
kind: "create_work_order" as const,
reason: "需要现场采样",
evidence_refs: [],
node_id: "field_sampling",
};
expect(proposedTaskActionSchema.safeParse({
...common,
payload: {
title: "现场采样",
description: "采集二次供水出水和末端水样",
work_order_type: "water_quality_sampling",
priority: "urgent",
location: "联调演示小区",
},
}).success).toBe(true);
expect(proposedTaskActionSchema.safeParse({
...common,
payload: {
title: "现场采样",
summary: "字段名不属于后端合同",
priority: "high",
},
}).success).toBe(false);
expect(proposedTaskActionSchema.safeParse({
...common,
kind: "issue_command",
payload: {
title: "现场采样",
description: "错误动作类型不得复用工单载荷",
work_order_type: "water_quality_sampling",
},
}).success).toBe(false);
});
it("accepts only explicit human input on the collaboration evidence path", () => {
const evidence = {
request_id: "60000000-0000-4000-8000-000000000001",
evidence: {
evidence_id: "human-confirmation-1",
kind: "human_input",
summary: "现场人员确认已完成采样。",
source_label: "调度员补充",
observed_at: "2026-09-23T01:00:00Z",
},
};
expect(humanTaskEvidenceCommandSchema.safeParse(evidence).success).toBe(true);
expect(humanTaskEvidenceCommandSchema.safeParse({
...evidence,
evidence: { ...evidence.evidence, kind: "system_data" },
}).success).toBe(false);
});
it("uses an in-memory credential and converts plans to the server contract", async () => {
let request: Request | undefined;
globalThis.fetch = (async (input, init) => {
request = new Request(input, init);
if (request.url.endsWith(`/runs/${runId}/claim`)) return Response.json(run);
return Response.json({
task,
plan_revision: 1,
report_revision: null,
});
}) as typeof fetch;
bindOperationalTaskCredential("runtime-1", {
authorization: "Bearer transient-secret",
projectId: "project-1",
});
const client = new OperationalTaskClient("http://server.test");
await client.claimTask("runtime-1", taskId, runId, {
requestId: crypto.randomUUID(), expectedRevision: 1, sourceSessionId: "session-1",
});
await client.publishArtifacts("runtime-1", taskId, {
requestId: executionRequestId,
sourceSessionId: "session-1",
executionPlan: {
title: "调查",
reason: "更新",
nodes: [{ id: "READ", title: "读取", owner: "agent", status: "completed", detail: "完成", evidence: ["evidence-1"] }],
edges: [],
},
});
expect(request?.headers.get("authorization")).toBe("Bearer transient-secret");
expect(request?.headers.get("x-project-id")).toBe("project-1");
const body = await request?.json() as Record<string, unknown>;
expect(body).toMatchObject({
request_id: "60000000-0000-4000-8000-000000000001",
source_session_id: "session-1",
run_id: runId,
plan: {
schema: "execution-plan@1",
nodes: [{ evidence_refs: ["evidence-1"] }],
},
});
expect(JSON.stringify(body)).not.toContain("transient-secret");
});
it("fails closed without a current product credential", async () => {
const client = new OperationalTaskClient("http://server.test");
expect(client.getAgentContext("runtime-1", "50000000-0000-4000-8000-000000000001"))
.rejects.toThrow("current product credential");
});
it("preserves the Server problem code for lease authority failures", async () => {
let call = 0;
globalThis.fetch = (async (_input, _init) => {
call += 1;
if (call === 1) return Response.json(run);
return Response.json({
type: "https://tjwater.example/problems/lease-expired",
title: "Conflict",
status: 409,
detail: "Agent lease has expired",
code: "LEASE_EXPIRED",
}, { status: 409 });
}) as typeof fetch;
bindOperationalTaskCredential("runtime-1", {
authorization: "Bearer transient-secret",
projectId: "project-1",
});
const client = new OperationalTaskClient("http://server.test");
await client.claimTask("runtime-1", taskId, runId, {
requestId: crypto.randomUUID(), expectedRevision: 1, sourceSessionId: "session-1",
});
expect(client.publishArtifacts(
"runtime-1",
taskId,
{
requestId: "60000000-0000-4000-8000-000000000001",
sourceSessionId: "session-1",
report: {
title: "失败",
current_conclusion: "租约已失效",
conclusion_boundary: "未继续写回",
evidence_refs: [],
recommendations: [],
pending_items: [],
},
},
)).rejects.toMatchObject({
status: 409,
code: "LEASE_EXPIRED",
message: "Agent lease has expired",
});
});
it("returns the persisted bounded data snapshot with its evidence id", async () => {
const urls: string[] = [];
globalThis.fetch = (async (input) => {
const url = String(input);
urls.push(url);
return urls.length === 1
? Response.json({
evidence_id: "scada_readings:sha256:abc",
kind: "system_data",
summary: "scada_readings 数据快照",
source_label: "scada_readings",
source_ref: "scada_readings:sha256:abc",
observed_at: "2026-09-23T01:00:00Z",
})
: Response.json({
source: "scada_readings",
source_ref: "scada_readings:sha256:abc",
queried_at: "2026-09-23T01:00:00Z",
origin: "system_data",
data: { items: [{ value: 0.42 }] },
truncated: false,
});
}) as typeof fetch;
bindOperationalTaskCredential("runtime-1", {
authorization: "Bearer transient-secret",
projectId: "project-1",
});
const client = new OperationalTaskClient("http://server.test");
const result = await client.queryData("runtime-1", "50000000-0000-4000-8000-000000000001", {
source: "scada_readings",
device_ids: ["device-1"],
start: "2026-09-23T00:00:00Z",
end: "2026-09-23T01:00:00Z",
});
expect(result).toMatchObject({
evidence: { evidence_id: "scada_readings:sha256:abc" },
snapshot: { origin: "system_data", truncated: false, data: { items: [{ value: 0.42 }] } },
});
expect(urls[1]).toContain("/evidence/scada_readings%3Asha256%3Aabc/snapshot");
});
it("keeps the current task revision for heartbeats after an action-only response", async () => {
const requestBodies: Record<string, unknown>[] = [];
let call = 0;
globalThis.fetch = (async (_input, init) => {
if (init?.body) requestBodies.push(JSON.parse(String(init.body)) as Record<string, unknown>);
call += 1;
if (call === 1) return Response.json(run);
if (call === 2) return Response.json({
action_id: "70000000-0000-4000-8000-000000000001",
approval_status: "pending",
execution_status: "not_submitted",
});
return Response.json({ ...run, revision: 3 });
}) as typeof fetch;
bindOperationalTaskCredential("runtime-1", {
authorization: "Bearer transient-secret",
projectId: "project-1",
});
const client = new OperationalTaskClient("http://server.test");
await client.claimTask("runtime-1", taskId, runId, {
requestId: "60000000-0000-4000-8000-000000000002",
expectedRevision: 1,
sourceSessionId: "session-1",
});
await client.proposeAction("runtime-1", "50000000-0000-4000-8000-000000000001", {
request_id: "60000000-0000-4000-8000-000000000003",
kind: "create_work_order",
reason: "需要现场采样",
payload: {
title: "现场采样",
description: "采集水样",
work_order_type: "water_quality_sampling",
priority: "urgent",
},
evidence_refs: [],
});
await client.heartbeatTask(
"runtime-1",
taskId,
runId,
"session-1",
);
expect(requestBodies[2]).toMatchObject({
expected_revision: 2,
source_session_id: "session-1",
});
});
it("serializes artifact publication and heartbeat revision updates", async () => {
const requestBodies: Record<string, unknown>[] = [];
let releaseArtifact: (() => void) | undefined;
const artifactGate = new Promise<void>((resolve) => { releaseArtifact = resolve; });
let call = 0;
globalThis.fetch = (async (_input, init) => {
if (init?.body) requestBodies.push(JSON.parse(String(init.body)) as Record<string, unknown>);
call += 1;
if (call === 1) return Response.json(run);
if (call === 2) {
await artifactGate;
return Response.json({
task: { ...task, active_run: { ...run, revision: 3 } },
plan_revision: 1,
report_revision: null,
});
}
return Response.json({ ...run, revision: 4 });
}) as typeof fetch;
bindOperationalTaskCredential("runtime-1", {
authorization: "Bearer transient-secret",
projectId: "project-1",
});
const client = new OperationalTaskClient("http://server.test");
await client.claimTask("runtime-1", taskId, runId, {
requestId: executionRequestId,
expectedRevision: 1,
sourceSessionId: "session-1",
});
const publication = client.publishArtifacts("runtime-1", taskId, {
requestId: "60000000-0000-4000-8000-000000000002",
sourceSessionId: "session-1",
executionPlan: {
title: "调查计划",
reason: "核实告警",
nodes: [],
edges: [],
},
});
const heartbeat = client.heartbeatTask("runtime-1", taskId, runId, "session-1");
releaseArtifact?.();
await Promise.all([publication, heartbeat]);
expect(requestBodies[2]).toMatchObject({ expected_revision: 3 });
});
});
+45
View File
@@ -0,0 +1,45 @@
import { describe, expect, it } from "bun:test";
import {
agentTriggerContextSchema,
buildAgentTriggerPrompt,
} from "../../src/routes/agentTrigger.js";
const trigger = {
schema_version: "agent-trigger@1",
kind: "event",
trigger_id: "peripheral.work_order.created:project-1:50000000-0000-4000-8000-000000000001:1",
occurred_at: "2026-09-22T10:00:00+08:00",
source: "peripheral_sample",
subject_type: "work_order",
subject_id: "50000000-0000-4000-8000-000000000001",
workflow: "water_quality_complaint",
payload: {
title: "居民反映自来水有异味",
summary: "演示小区多户居民反映晨间用水有异味。",
location: "演示小区",
priority: "high",
scenario_code: "WQ-SECONDARY-SUPPLY-001",
origin: "synthetic",
},
} as const;
describe("agent event trigger", () => {
it("accepts the water-quality work-order contract and builds a bounded prompt", () => {
const parsed = agentTriggerContextSchema.parse(trigger);
const prompt = buildAgentTriggerPrompt(parsed);
expect(prompt).toContain("水质监测点");
expect(prompt).toContain("二次供水");
expect(prompt).toContain("合成演练数据");
expect(prompt).toContain("疑似二次供水环节问题(待现场核验)");
expect(prompt).toContain(trigger.trigger_id);
});
it("rejects a different scenario instead of routing arbitrary work orders", () => {
expect(agentTriggerContextSchema.safeParse({
...trigger,
payload: { ...trigger.payload, scenario_code: "UNKNOWN" },
}).success).toBe(false);
});
});
+94
View File
@@ -0,0 +1,94 @@
import { describe, expect, it } from "bun:test";
import { createExecutionPlanSnapshot, executionPlanNeedsTaskPublish, executionPlanSchema, readExecutionPlans } from "../../src/routes/executionPlan.js";
import { streamPromptResponse } from "../../src/routes/chatStream.js";
import type { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
const plan = {
title: "水质调查", reason: "收到投诉",
nodes: [
{ id: "upstream", title: "上游监测", status: "completed", owner: "agent", detail: "合成数据", evidence: ["场景输入:上游指标正常"] },
{ id: "sample", title: "现场采样", status: "waiting", owner: "human", detail: "等待现场记录", evidence: [] },
],
edges: [{ from: "upstream", to: "sample", label: "上游正常" }],
};
describe("execution plans", () => {
it("rejects missing dependencies, cycles, duplicate IDs and unsupported completion", () => {
for (const invalid of [
{ ...plan, edges: [{ from: "unknown", to: "sample", label: "" }] },
{ ...plan, edges: [...plan.edges, { from: "sample", to: "upstream", label: "" }] },
{ ...plan, nodes: [plan.nodes[0], plan.nodes[0]] },
{ ...plan, nodes: [{ ...plan.nodes[0], evidence: [] }, plan.nodes[1]] },
{
...plan,
nodes: [
{ ...plan.nodes[0], status: "waiting", detail: "等待数据", evidence: [] },
{ ...plan.nodes[1], status: "completed", evidence: ["evidence-2"] },
],
},
]) expect(executionPlanSchema.safeParse(invalid).success).toBe(false);
});
it("preserves completed evidence and versions across persisted UI messages and active snapshots", () => {
const first = createExecutionPlanSnapshot(plan, [], "session-1");
const messages = [{ parts: [{ type: "data-execution_plan", data: first }] }, { executionPlans: [first] }];
expect(readExecutionPlans(messages)).toHaveLength(1);
const next = createExecutionPlanSnapshot({ ...plan, reason: "等待人工补充" }, messages, "session-1");
expect(next.revision).toBe(2);
expect(next.nodes[1].status).toBe("waiting");
expect(() => createExecutionPlanSnapshot({ ...plan, nodes: [plan.nodes[1]], edges: [] }, messages, "session-1")).toThrow("Keep previous nodes");
expect(() => createExecutionPlanSnapshot({ ...plan, nodes: [{ ...plan.nodes[0], status: "running" }, plan.nodes[1]] }, messages, "session-1")).toThrow("Keep completed evidence");
});
it("publishes only successful validated tool completions, once per call", async () => {
const part = (status: string) => ({ type: "message.part.updated", properties: { sessionID: "runtime", part: {
id: "plan-call", messageID: "assistant", sessionID: "runtime", type: "tool", tool: "publish_execution_plan",
callID: "call-1", state: { status, input: plan, output: JSON.stringify({ plan_revision: 1, report_revision: null }), title: "plan", time: { start: 1, end: 2 } }
} } });
const events = [part("running"), part("completed"), part("completed"), { type: "session.idle", properties: { sessionID: "runtime" } }];
const runtime = {
subscribeEvents: async () => ({ async *[Symbol.asyncIterator]() { for (const event of events) yield event; } }),
prompt: async () => undefined, messages: async () => []
} as unknown as OpencodeRuntimeAdapter;
const published: unknown[] = [];
await streamPromptResponse({ runtime, sessionId: "runtime", clientSessionId: "session-1", message: "investigate", write(event, data) { if (event === "execution_plan") published.push(data); } });
expect(published).toHaveLength(1);
expect(published[0]).toMatchObject({ session_id: "session-1", server_published: true, nodes: plan.nodes });
});
it("falls back to route publication for a completed legacy local-only plan tool", async () => {
const completed = { type: "message.part.updated", properties: { sessionID: "runtime", part: {
id: "plan-call", messageID: "assistant", sessionID: "runtime", type: "tool", tool: "publish_execution_plan",
callID: "call-1", state: { status: "completed", input: plan, output: JSON.stringify(plan), title: "plan", time: { start: 1, end: 2 } }
} } };
const runtime = {
subscribeEvents: async () => ({ async *[Symbol.asyncIterator]() { yield completed; yield { type: "session.idle", properties: { sessionID: "runtime" } }; } }),
prompt: async () => undefined, messages: async () => []
} as unknown as OpencodeRuntimeAdapter;
const published: Record<string, unknown>[] = [];
await streamPromptResponse({ runtime, sessionId: "runtime", clientSessionId: "session-1", message: "investigate", write(event, data) { if (event === "execution_plan") published.push(data); } });
expect(published).toHaveLength(1);
expect(published[0]?.server_published).toBeUndefined();
expect(executionPlanNeedsTaskPublish(published[0])).toBe(true);
});
it("does not publish a failed tool plan or duplicate an already persisted plan", async () => {
const failed = { type: "message.part.updated", properties: { sessionID: "runtime", part: {
id: "plan-call", messageID: "assistant", sessionID: "runtime", type: "tool", tool: "publish_execution_plan",
callID: "call-1", state: { status: "error", input: plan, error: "server rejected plan", time: { start: 1, end: 2 } }
} } };
const runtime = {
subscribeEvents: async () => ({ async *[Symbol.asyncIterator]() { yield failed; yield { type: "session.idle", properties: { sessionID: "runtime" } }; } }),
prompt: async () => undefined, messages: async () => []
} as unknown as OpencodeRuntimeAdapter;
const published: unknown[] = [];
await streamPromptResponse({ runtime, sessionId: "runtime", clientSessionId: "session-1", message: "investigate", write(event, data) { if (event === "execution_plan") published.push(data); } });
expect(published).toEqual([]);
expect(executionPlanNeedsTaskPublish({ server_published: true })).toBe(false);
expect(executionPlanNeedsTaskPublish(plan)).toBe(true);
});
});
+122
View File
@@ -0,0 +1,122 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import express from "express";
import { mkdtemp, rm } from "node:fs/promises";
import { request as httpRequest, type Server } from "node:http";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { buildChatRouter } from "../../src/routes/chat.js";
import { SessionMetadataStore } from "../../src/sessions/metadataStore.js";
import { toActorKey, toProjectKey } from "../../src/utils/fileStore.js";
const originalFetch = globalThis.fetch;
describe("chat session HTTP isolation", () => {
let metadataDir: string;
let server: Server;
beforeEach(async () => {
metadataDir = await mkdtemp(join(tmpdir(), "tjwater-route-sessions-"));
});
afterEach(async () => {
globalThis.fetch = originalFetch;
if (server) {
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
await rm(metadataDir, { force: true, recursive: true });
});
it("hides task sessions from ordinary history and scopes every list to the verified user and project", async () => {
globalThis.fetch = (async (_input, init) => {
const headers = new Headers(init?.headers);
const authorization = headers.get("authorization");
const projectId = headers.get("x-project-id");
const userId = authorization === "Bearer token-a" ? "user-a" : "user-b";
return Response.json({
user_id: userId,
project_id: projectId,
network: "supply",
});
}) as typeof fetch;
const sessionMetadataStore = new SessionMetadataStore(metadataDir);
await sessionMetadataStore.initialize();
const ownerContext = {
actorKey: toActorKey("product:user-a"),
projectId: "10000000-0000-4000-8000-000000000001",
projectKey: toProjectKey("product:10000000-0000-4000-8000-000000000001"),
userId: "user-a",
};
await sessionMetadataStore.ensure({
...ownerContext,
sessionId: "ordinary-session",
});
await sessionMetadataStore.ensure({
...ownerContext,
sessionId: "execution-session",
purpose: "task_execution",
taskId: "50000000-0000-4000-8000-000000000001",
requestId: "60000000-0000-4000-8000-000000000001",
});
const app = express();
app.use(express.json());
app.use("/api/v1/agent/chat", buildChatRouter({
frontendActionCoordinator: {} as never,
learningOrchestrator: {} as never,
memoryStore: {} as never,
resultReferenceResolver: {} as never,
runtime: {} as never,
sessionBridge: {} as never,
sessionMetadataStore,
sessionTranscriptStore: {} as never,
sessionUiStateStore: {} as never,
}));
server = app.listen(0, "127.0.0.1");
await new Promise<void>((resolve) => server.once("listening", resolve));
const address = server.address();
if (!address || typeof address === "string") throw new Error("expected HTTP server address");
const ownedOrdinary = await getJson(address.port, "/api/v1/agent/chat/sessions", "token-a", ownerContext.projectId);
const ownedExecution = await getJson(address.port, "/api/v1/agent/chat/sessions?purpose=task_execution", "token-a", ownerContext.projectId);
const otherUser = await getJson(address.port, "/api/v1/agent/chat/sessions?purpose=task_execution", "token-b", ownerContext.projectId);
const otherProject = await getJson(address.port, "/api/v1/agent/chat/sessions?purpose=task_execution", "token-a", "10000000-0000-4000-8000-000000000002");
expect(ownedOrdinary.body.sessions.map((item: { id: string }) => item.id)).toEqual(["ordinary-session"]);
expect(ownedExecution.body.sessions.map((item: { id: string }) => item.id)).toEqual(["execution-session"]);
expect(otherUser.body.sessions).toEqual([]);
expect(otherProject.body.sessions).toEqual([]);
});
});
const getJson = (
port: number,
path: string,
token: string,
projectId: string,
) => new Promise<{ status: number; body: Record<string, any> }>((resolve, reject) => {
const request = httpRequest({
hostname: "127.0.0.1",
port,
path,
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"X-Project-Id": projectId,
},
}, (response) => {
const chunks: Buffer[] = [];
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
response.on("end", () => {
resolve({
status: response.statusCode ?? 0,
body: JSON.parse(Buffer.concat(chunks).toString("utf8")),
});
});
});
request.on("error", reject);
request.end();
});
+50
View File
@@ -0,0 +1,50 @@
import { describe, expect, it } from "bun:test";
import type { UIMessage } from "ai";
import {
buildFailedTaskExecutionPlan,
toCollaborationMessages,
} from "../../src/routes/chatStreamRoute.js";
describe("task collaboration history", () => {
it("persists only user and assistant text instead of execution trajectory", () => {
const messages = [
{ id: "user-1", role: "user", parts: [{ type: "text", text: "为什么这样判断?" }] },
{
id: "assistant-1",
role: "assistant",
parts: [
{ type: "data-progress", data: { title: "查询 SCADA" } },
{ type: "text", text: "因为上游数据正常,而投诉集中在同一小区。" },
{ type: "data-tool_call", data: { tool: "query_operational_task_data" } },
],
},
] as unknown as UIMessage[];
expect(toCollaborationMessages(messages)).toEqual([
{ id: "user-1", role: "user", parts: [{ type: "text", text: "为什么这样判断?" }] },
{ id: "assistant-1", role: "assistant", parts: [{ type: "text", text: "因为上游数据正常,而投诉集中在同一小区。" }] },
]);
});
});
describe("failed task execution plan", () => {
it("turns active work into an explicit failed task artifact", () => {
const plan = buildFailedTaskExecutionPlan([{
title: "调查",
reason: "开始",
nodes: [
{ id: "READ", title: "读取", status: "completed", owner: "agent", detail: "完成", evidence: ["evidence-1"], action_refs: [] },
{ id: "ANALYZE", title: "分析", status: "running", owner: "agent", detail: "分析中", evidence: [], action_refs: [] },
{ id: "SAMPLE", title: "采样", status: "waiting", owner: "human", detail: "等待现场", evidence: [], action_refs: [] },
],
edges: [{ from: "READ", to: "ANALYZE", label: "数据已读取" }],
}]);
expect(plan.nodes).toEqual([
{ id: "READ", title: "读取", status: "completed", owner: "agent", detail: "完成", evidence: ["evidence-1"], action_refs: [] },
expect.objectContaining({ id: "ANALYZE", status: "failed" }),
expect.objectContaining({ id: "SAMPLE", status: "cancelled" }),
]);
});
});
+29
View File
@@ -91,6 +91,35 @@ describe("OpencodeRuntimeAdapter.ensureClient", () => {
});
});
describe("OpencodeRuntimeAdapter.resolveAvailableModel", () => {
it("selects the first preferred model actually exposed by OpenCode", async () => {
const client = {
config: {
providers: async () => ({
data: {
providers: [{
id: "deepseek",
models: {
"deepseek-flash": {},
"deepseek-v4-pro": {},
},
}],
default: {},
},
}),
},
} as unknown as OpencodeClient;
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
ensureClient: async () => client,
}) as OpencodeRuntimeAdapter;
await expect(runtime.resolveAvailableModel([
"deepseek/deepseek-v4-flash",
"deepseek/deepseek-v4-pro",
])).resolves.toBe("deepseek/deepseek-v4-pro");
});
});
describe("resolveEmbeddedOpencodePort", () => {
it("falls back to a dynamic port when the configured port is occupied", async () => {
const server = createServer();
+20
View File
@@ -0,0 +1,20 @@
import { describe, expect, it } from "bun:test";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
const projectRoot = join(import.meta.dir, "../..");
describe("supply Agent identity", () => {
it("keeps the primary instruction scoped to validated supply data", async () => {
const instruction = await readFile(
join(projectRoot, ".opencode/agents/instruction.md"),
"utf8",
);
expect(instruction).toContain("TJWater 实验性供水数据 Agent");
expect(instruction).toContain("供水事件数据边界");
expect(instruction).not.toContain("source/");
expect(instruction).not.toContain("排水");
expect(instruction).not.toContain("雨污混接");
});
});
+62
View File
@@ -95,4 +95,66 @@ describe("SessionMetadataStore", () => {
);
expect(fetched?.title).toBe("新标题");
});
it("finds a triggered session only inside the owning context", async () => {
const context = {
actorKey: "actor-trigger",
projectId: "project-trigger",
projectKey: "project-key-trigger",
userId: "user-trigger",
};
const { record } = await store.ensure({
...context,
sessionId: "triggered-session",
trigger: {
triggerId: "peripheral.work_order.created:project:order:1",
source: "peripheral_sample",
subjectType: "work_order",
subjectId: "50000000-0000-4000-8000-000000000001",
workflow: "water_quality_complaint",
occurredAt: "2026-09-22T10:00:00+08:00",
},
});
expect(
await store.findByTrigger(
context,
"peripheral.work_order.created:project:order:1",
),
).toEqual(record);
expect(
await store.findByTrigger(
{ ...context, actorKey: "other-actor", userId: "other-user" },
"peripheral.work_order.created:project:order:1",
),
).toBeNull();
});
it("keeps ordinary chat, task execution, and task collaboration distinct", async () => {
const context = {
actorKey: "actor-purpose",
projectId: "project-purpose",
projectKey: "project-key-purpose",
userId: "user-purpose",
};
await store.ensure({ ...context, sessionId: "ordinary" });
await store.ensure({
...context,
sessionId: "execution",
purpose: "task_execution",
taskId: "50000000-0000-4000-8000-000000000001",
requestId: "60000000-0000-4000-8000-000000000001",
});
await store.ensure({
...context,
sessionId: "collaboration",
purpose: "task_collaboration",
taskId: "50000000-0000-4000-8000-000000000001",
});
expect((await store.list(context, { purpose: "ordinary_chat" })).map((item) => item.sessionId)).toEqual(["ordinary"]);
expect((await store.list(context, { purpose: "task_execution" })).map((item) => item.sessionId)).toEqual(["execution"]);
expect((await store.findByTask(context, "50000000-0000-4000-8000-000000000001", "task_collaboration"))?.sessionId).toBe("collaboration");
expect((await store.findByRequest(context, "60000000-0000-4000-8000-000000000001"))?.sessionId).toBe("execution");
});
});
+20
View File
@@ -0,0 +1,20 @@
import { expect, it } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { SessionUiStateStore } from "../../src/sessions/uiStateStore.js";
it("restores plan history independently of chat messages after reconnecting", async () => {
const directory = await mkdtemp(join(tmpdir(), "tjwater-plan-state-"));
try {
const store = new SessionUiStateStore(directory);
await store.initialize();
const executionPlans = [{ revision: 1, nodes: [{ id: "sampling", status: "waiting" }] }];
await store.write({ sessionId: "session-1" }, { sessionId: "session-1", messages: [], executionPlans });
const reloaded = await new SessionUiStateStore(directory).read({ sessionId: "session-1" });
expect(reloaded?.executionPlans).toEqual(executionPlans);
expect(await store.read({ sessionId: "other-session" })).toBeNull();
} finally {
await rm(directory, { recursive: true, force: true });
}
});