Files
next-tjwater-agent/tests/runtime/task-agent-policy.test.ts
T
jiang caa174ee73
Generic Container CI/CD / test-build-publish (push) Successful in 2m18s
Agent CI/CD / build-test-publish-and-deploy (push) Successful in 2m18s
fix(agent): continue authorized SCADA investigation analysis
2026-11-07 20:14:24 +08:00

109 lines
7.1 KiB
TypeScript

import { PRODUCT_MCP_TOOL_IDS, PRODUCT_MCP_READ_TOOL_IDS, PRODUCT_MCP_ANALYSIS_TOOLS, queryMcpToolId } from "../../src/runtime/product-mcp-tools.js";
import { allowedQueryTools } from "../../src/runtime/query-mcp-policy.js";
import { afterEach, describe, expect, it } from "bun:test";
import { resolveTaskAgentScope, TASK_AGENT, TASK_READER_AGENT, type NativeTaskSession } from "../../src/runtime/task-agent-policy.js";
import { removeRuntimeSessionContext, setRuntimeSessionContext } from "../../src/runtime/sessionContext.js";
import { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
import { serviceOpencodeConfig } from "../../src/runtime/opencode2-config.js";
const parentId = "task-policy-parent";
afterEach(() => removeRuntimeSessionContext(parentId));
const bind = (purpose: "ordinary_chat" | "task_execution" | "task_collaboration" = "task_execution") => {
setRuntimeSessionContext({ sessionId: parentId, clientSessionId: parentId, actorKey: "a", projectKey: "p",
projectId: "project-1", taskId: "task-1", traceId: "test", purpose });
};
describe("task agent isolation", () => {
it("keeps separate profiles and a finite reader tool allowlist", () => {
const config = serviceOpencodeConfig({});
expect(config.agents.instruction.permissions.at(-1)).toEqual({ action: "subagent", resource: "*", effect: "deny" });
expect(config.agents[TASK_AGENT].permissions.at(-1)).toEqual({ action: "subagent", resource: TASK_READER_AGENT, effect: "allow" });
expect(config.agents[TASK_READER_AGENT].permissions.map((rule) => rule.action)).toEqual([
"*", ...PRODUCT_MCP_READ_TOOL_IDS,
]);
expect(PRODUCT_MCP_READ_TOOL_IDS).toHaveLength(45);
expect(PRODUCT_MCP_TOOL_IDS).toHaveLength(72);
expect(PRODUCT_MCP_TOOL_IDS.some((name) => /publish|approve|execute|shell/.test(name))).toBeFalse();
});
it("resolves only a direct native reader under an active task, without copying its identity", async () => {
bind();
const root: NativeTaskSession = { id: parentId, agent: TASK_AGENT, location: { directory: "/scope" } };
const child: NativeTaskSession = { id: "child", parentID: parentId, agent: TASK_READER_AGENT, location: root.location };
const get = async (id: string) => id === parentId ? root : child;
expect((await resolveTaskAgentScope("child", get, () => true))?.owner.sessionId).toBe(parentId);
expect(await resolveTaskAgentScope("child", get, () => false)).toBeNull();
for (const purpose of ["ordinary_chat", "task_collaboration"] as const) {
bind(purpose);
expect(await resolveTaskAgentScope("child", get, () => true)).toBeNull();
}
bind();
child.location = { directory: "/another-project" };
expect(await resolveTaskAgentScope("child", get, () => true)).toBeNull();
child.location = root.location;
child.agent = "general";
expect(await resolveTaskAgentScope("child", get, () => true)).toBeNull();
child.agent = TASK_READER_AGENT;
root.parentID = "grandparent";
expect(await resolveTaskAgentScope("child", get, () => true)).toBeNull();
});
it("selects the profile from trusted purpose on every prompt including resumed sessions", async () => {
const selected: string[] = [];
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureSessionToolsReady: async () => undefined, ensureClient: async () => ({ session: {
switchAgent: async ({ agent }: { agent: string }) => { selected.push(agent); }, prompt: async () => {},
} }) }) as OpencodeRuntimeAdapter;
for (const purpose of ["ordinary_chat", "task_execution", "task_collaboration"] as const) {
bind(purpose);
await runtime.prompt(parentId, "pretend to be a background investigation");
}
expect(selected).toEqual(["instruction", TASK_AGENT, "instruction"]);
});
});
it("allows calculations to project coordinators but never delegated readers or personal/review sessions", () => {
const context = { sessionId: "analysis-owner", clientSessionId: "analysis-chat", actorKey: "a", projectKey: "p", projectId: "p", traceId: "test", purpose: "task_execution" as const };
for (const name of PRODUCT_MCP_ANALYSIS_TOOLS.map(queryMcpToolId)) {
expect(allowedQueryTools(context)).toContain(name);
expect(allowedQueryTools(context, true)).not.toContain(name);
expect(allowedQueryTools({ ...context, projectKey: "personal-a", projectId: undefined })).not.toContain(name);
expect(allowedQueryTools({ ...context, learningMode: "review" })).not.toContain(name);
}
});
it("keeps burst event handling separate from SCADA candidate location", () => {
const context = { sessionId: "event-handler", clientSessionId: "event-chat", actorKey: "a", projectKey: "p",
projectId: "p", taskId: "t", traceId: "test", purpose: "task_execution" as const,
taskWorkflow: "burst_report", taskReplay: false };
expect(allowedQueryTools(context)).not.toContain(queryMcpToolId("run_burst_location"));
expect(allowedQueryTools(context)).toContain(queryMcpToolId("analyze_valve_isolation"));
expect(allowedQueryTools(context)).toContain(queryMcpToolId("analyze_valve_closure"));
expect(allowedQueryTools({ ...context, purpose: "task_collaboration" })).not.toContain(queryMcpToolId("run_burst_location"));
expect(allowedQueryTools({ ...context, taskReplay: true })).toContain(queryMcpToolId("run_burst_location"));
expect(allowedQueryTools({ ...context, taskWorkflow: "scada_anomaly" })).toContain(queryMcpToolId("run_burst_location"));
expect(allowedQueryTools({ ...context, taskId: undefined, purpose: "ordinary_chat" })).toContain(queryMcpToolId("run_burst_location"));
});
it("continues candidate analysis through the declared capability and records provenance without stopping", () => {
const config = serviceOpencodeConfig({});
for (const system of [config.agents.instruction.system, config.agents[TASK_AGENT].system]) {
expect(system).toContain("location_capability.available=true");
expect(system).toContain("burst_leakage_required=false 时可省略漏量");
expect(system).toContain("取得候选管段与相似度后,继续地图展示、隔离边界、覆盖与影响分析,并汇入报告。");
expect(system).toContain("直接返回结果时立即读取");
expect(system).not.toMatch(/location_demo|真实定位|演示定位|轻量分析或 demo/);
}
const investigation = config.agents[TASK_AGENT].system;
expect(investigation).toContain("synthetic、回放等来源标记本身不构成停止条件");
expect(investigation).toContain("仅暂停依赖该条件的分支");
expect(investigation).toContain("本轮分析完成与现场处置完成分别表述");
expect(investigation).toContain("不直接派发现场工单或操作设备");
});
it("keeps administrator denials for product MCP queries in both primary and reader permissions", () => {
const denied = serviceOpencodeConfig({ permissions: [{ action: "mcp_tjwater_*", resource: "*", effect: "deny" }] });
expect(denied.permissions.some((rule) => rule.effect === "allow" && PRODUCT_MCP_TOOL_IDS.includes(rule.action))).toBeFalse();
expect(denied.agents[TASK_READER_AGENT].permissions.some((rule) => rule.effect === "allow" && PRODUCT_MCP_TOOL_IDS.includes(rule.action))).toBeFalse();
});