feat(agent): unify MCP tools and adopt Responses delivery

Move project and host queries into native MCP with host-owned OAuth/context,
while retaining memory, skills, actions and publication plugins. Add nearby-model
queries and explicit chat/task/learning model defaults.

Adopt Responses HTTP/SSE and native steer/queue inbox delivery, preserving scoped
session ownership, pending input edits and investigation communication.

Disable local-provider discovery: Server /health allowed native vLLM to probe
/v1/models on port 8000. A native before/after regression prevents recurrence.

Validation: 170 focused Bun tests; service/plugin typechecks; native discovery,
provider-discovery and input-inbox smoke checks; no paid model inference.
This commit is contained in:
2026-10-19 18:46:58 +08:00
parent f5c3cde2c2
commit 6a49d2209e
164 changed files with 12286 additions and 2368 deletions
+11
View File
@@ -1,7 +1,18 @@
import { describe, expect, it } from "bun:test";
import { CredentialRefreshCoordinator, runWithCredentialRefresh, bindSessionCredential, credentialRefreshCoordinator, refreshSessionCredential, releaseSessionCredential, requestWithSessionCredential } from "../../src/product/credential-refresh.js";
import { removeRuntimeSessionContext, setRuntimeSessionContext } from "../../src/runtime/sessionContext.js";
describe("credential renewal", () => {
it("requires a project before personal chats can call business APIs", async () => {
const sessionId = "personal-business-access";
let calls = 0;
setRuntimeSessionContext({ actorKey: "actor-personal", projectKey: "personal-actor-personal", sessionId,
clientSessionId: sessionId, traceId: "test" });
try {
await expect(requestWithSessionCredential(sessionId, async () => { calls++; return Response.json({}); })).rejects.toMatchObject({ status: 403, code: "PROJECT_CONTEXT_REQUIRED" });
expect(calls).toBe(0);
} finally { removeRuntimeSessionContext(sessionId); }
});
it("rejects stale replies and stops repeated requests when the renewed credential is still rejected", async () => {
const session = "rejected-credential-session";
bindSessionCredential(session, { authorization: "Bearer OLD", projectId: "p" });
@@ -1,28 +1,17 @@
import { afterEach, describe, expect, it } from "bun:test";
import { ExternalInformationClient, geocodeInput, webSearchInput } from "../../src/product/external-information-client.js";
import { ExternalInformationClient, geocodeInput } from "../../src/product/external-information-client.js";
import { bindSessionCredential, credentialRefreshCoordinator, refreshSessionCredential, releaseSessionCredential } from "../../src/product/credential-refresh.js";
const originalFetch = globalThis.fetch;
const session = "external-information-test";
afterEach(() => { globalThis.fetch = originalFetch; releaseSessionCredential(session); });
describe("typed external information", () => {
it("uses the fixed Server endpoint and trusted project, translating freshness", async () => {
bindSessionCredential(session, { authorization: "Bearer TEST", projectId: "trusted" });
globalThis.fetch = (async (url: Parameters<typeof fetch>[0], init?: RequestInit) => {
expect(String(url)).toBe("http://server.test/api/v1/web-searches");
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer TEST");
expect(new Headers(init?.headers).get("X-Project-Id")).toBe("trusted");
expect(JSON.parse(String(init?.body))).toEqual({ query: "供水政策", freshness: "oneWeek", summary: true, count: 10, include: ["gov.cn"] });
return Response.json({ data: { webPages: { value: [{ url: "https://gov.cn/example", name: "example" }] } } });
}) as unknown as typeof fetch;
const result = await new ExternalInformationClient("http://server.test").webSearch(session, webSearchInput.parse({ query: "供水政策", freshness: "one_week", include: ["gov.cn"] }));
expect(result).toHaveProperty("data.webPages.value");
});
it("normalizes valid Tianditu coordinates for zoom_to_map and rejects fabricated coordinates", async () => {
bindSessionCredential(session, { authorization: "Bearer TEST", projectId: "trusted" });
globalThis.fetch = (async (url: Parameters<typeof fetch>[0], init?: RequestInit) => {
expect(new URL(String(url)).pathname).toBe("/api/v1/geocoding-requests");
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer TEST");
expect(new Headers(init?.headers).get("X-Project-Id")).toBe("trusted");
expect(JSON.parse(String(init?.body))).toEqual({ keyword: "北京市人民政府" });
return Response.json({ status: "0", location: { lon: "116.4", lat: "39.9", level: 1 } });
}) as unknown as typeof fetch;
@@ -52,11 +41,9 @@ describe("typed external information", () => {
} finally { unsubscribe(); }
});
it("rejects scope, URL overrides, empty queries and invalid counts before dispatch", async () => {
expect(webSearchInput.safeParse({ query: "test", url: "http://untrusted" }).success).toBeFalse();
expect(webSearchInput.safeParse({ query: "test", project_id: "other" }).success).toBeFalse();
expect(webSearchInput.safeParse({ query: "test", count: 51 }).success).toBeFalse();
expect(webSearchInput.safeParse({ query: "test", include: ["https://example.com/path"] }).success).toBeFalse();
it("rejects scope, URL overrides and empty keywords before dispatch", async () => {
expect(geocodeInput.safeParse({ keyword: "北京", url: "http://untrusted" }).success).toBeFalse();
expect(geocodeInput.safeParse({ keyword: "北京", project_id: "other" }).success).toBeFalse();
expect(geocodeInput.safeParse({ keyword: " " }).success).toBeFalse();
await expect(new ExternalInformationClient().geocode(session, { keyword: "北京" })).rejects.toMatchObject({ status: 401 });
});
+63
View File
@@ -0,0 +1,63 @@
import { describe, expect, it } from "bun:test";
import { historyChart, queryHistoryChart } from "../../src/product/history-chart.js";
describe("history chart", () => {
it("keeps returned units, drops null samples and never manufactures missing history", () => {
expect(() => historyChart({ series: [] }, "J1")).toThrow("NO_HISTORY_DATA");
const chart = historyChart({ series: [{ element_id: "J1", metric: "pressure", source: "scada_raw", source_unit: "m", points: [{ time: "t1", value: null }, { time: "t2", value: 20 }] }] }, "J1");
expect(chart.title).toContain("(m)");
expect(chart.x_data).toEqual(["t2"]);
expect(chart.series[0].data).toEqual([20]);
});
});
const series = { element_id: "J1", metric: "pressure", source: "raw", source_unit: "m", device_id: "D1" };
it("continues past a null first page to use real later samples", async () => {
const pages: number[][] = [];
const chart = await queryHistoryChart("J1", async (limit, offset) => {
pages.push([limit, offset]);
return { content: [], structuredContent: { page: { limit, offset, has_more: offset === 0 }, truncated: offset === 0,
data: { series: [{ ...series, points: offset === 0 ? Array.from({ length: limit }, (_, i) => ({ time: String(i), value: null })) : [{ time: "500", value: 25 }] }] } } };
});
expect(pages).toEqual([[500, 0], [500, 500]]);
expect(chart.series[0].data).toEqual([25]);
});
it("stops within a fixed budget and distinguishes unread history from absent history", async () => {
let calls = 0;
await expect(queryHistoryChart("J1", async (limit, offset) => {
calls++;
return { content: [], structuredContent: { page: { limit, offset, has_more: true }, data: { series: [{ ...series, points: [{ time: String(offset), value: null }] }] } } };
})).rejects.toThrow("仍有未读取数据");
expect(calls).toBe(2);
});
it("shrinks oversized pages without skipping data or retrying unrelated failures", async () => {
const calls: number[][] = [];
const chart = await queryHistoryChart("J1", async (limit, offset) => {
calls.push([limit, offset]);
if (limit > 125) return { isError: true, content: [], structuredContent: { error: { code: "RESULT_TOO_LARGE" } } };
return { content: [], structuredContent: { page: { limit, offset, has_more: offset === 0 }, data: {
series: [{ ...series, points: [{ time: String(offset), value: offset === 0 ? null : 25 }] }],
} } };
});
expect(calls).toEqual([[500, 0], [250, 0], [125, 0], [125, 125]]);
expect(chart.series[0].data).toEqual([25]);
let failures = 0;
await expect(queryHistoryChart("J1", async () => {
failures++;
return { isError: true, content: [], structuredContent: { error: { code: "ACCESS_DENIED" } } };
})).rejects.toThrow("历史查询暂不可用");
expect(failures).toBe(1);
});
it("bounds displayed values and identifies a partial query", async () => {
let calls = 0;
const chart = await queryHistoryChart("J1", async (limit, offset) => {
calls++;
return { content: [], structuredContent: { page: { limit, offset, has_more: true },
data: { series: [{ ...series, points: Array.from({ length: 100 }, (_, i) => ({ time: String(i), value: i })) }] } } };
});
expect(calls).toBe(1);
expect(chart.series[0].data).toHaveLength(100);
expect(chart.title).toContain("未覆盖完整时间窗");
});
+32 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "bun:test";
import { taskReportSchema } from "../../src/product/investigation-report.js";
import { taskReportSchema, taskProgressSchema } from "../../src/product/investigation-report.js";
const report = { title: "调查报告", current_conclusion: "无法确定原因", conclusion_boundary: "仅有上游数据",
evidence_refs: ["query-1"], recommendations: [], pending_items: [], stage: "final", outcome: "insufficient_data",
@@ -34,4 +34,35 @@ describe("new investigation report writes", () => {
expect(() => taskReportSchema.parse({ ...report, ...changes })).toThrow();
}
});
it("shares explicit node activity and supply feature targets with progress", () => {
const diagram = { title: "当前调查", mermaid: "flowchart LR\nQUERY --> HUMAN",
node_refs: [{ node_id: "QUERY", label: "核对监测", status: "running", evidence_refs: ["query-1"],
map_targets: [{ source_id: "scada", feature_id: "MP01" }] },
{ node_id: "HUMAN", label: "现场核验", status: "waiting_human" }] };
const progress = taskProgressSchema.parse({ summary: "正在核对投诉时段的实际监测记录", diagram });
expect(progress.diagram?.node_refs[0]).toMatchObject({ status: "running", map_targets: [{ source_id: "scada", feature_id: "MP01" }] });
expect(taskReportSchema.parse({ ...report, diagram }).diagram).toEqual(progress.diagram);
expect(taskProgressSchema.parse({ summary: "调整调查方向", diagram: null }).diagram).toBeNull();
for (const change of [{ status: "waiting" }, { map_targets: [{ source_id: "drainage", feature_id: "X" }] },
{ map_targets: [{ source_id: "scada", feature_id: "" }] }]) {
expect(() => taskProgressSchema.parse({ summary: "核对", diagram: {
...diagram, node_refs: [{ ...diagram.node_refs[0], ...change }],
} })).toThrow();
}
});
it("preserves parallel branches with independent activity and a pending join", () => {
const mermaid = 'flowchart TD\nSTART[受理投诉] --> SPLIT{并行核查}\nSPLIT --> TICKETS[工单核查]\nSPLIT --> SCADA[监测核查]\nTICKETS --> JOIN[汇总证据]\nSCADA --> JOIN';
const diagram = { title: "并行调查", mermaid, node_refs: [
{ node_id: "TICKETS", label: "工单核查", status: "running" },
{ node_id: "SCADA", label: "监测核查", status: "running" },
{ node_id: "JOIN", label: "汇总证据", status: "pending" },
] };
const progress = taskProgressSchema.parse({ summary: "两个独立核查正在执行", diagram });
expect(progress.diagram?.mermaid).toBe(mermaid);
expect(progress.diagram?.node_refs.map((node) => node.status)).toEqual(["running", "running", "pending"]);
const updated = taskProgressSchema.parse({ summary: "工单核查已完成,监测核查仍在执行", diagram: {
...diagram, node_refs: [{ ...diagram.node_refs[0], status: "completed", evidence_refs: ["query-1"] }, ...diagram.node_refs.slice(1)],
} });
expect(updated.diagram?.node_refs.map((node) => node.status)).toEqual(["completed", "running", "pending"]);
});
});
+78
View File
@@ -0,0 +1,78 @@
import { expect, it } from "bun:test";
import { McpAuthDiscovery, type McpHttpFetch } from "../../src/product/mcp-auth-discovery.js";
const settings = { resource: "https://water.example/mcp", issuer: "https://identity.example/realm/water" };
const metadata = { issuer: settings.issuer, authorization_endpoint: "https://identity.example/authorize", response_types_supported: ["code"], token_endpoint: "https://identity.example/custom/token", token_endpoint_auth_methods_supported: ["client_secret_post"] };
it("uses the challenge resource metadata and discovers a non-Keycloak endpoint", async () => {
const requests: string[] = [];
const discovery = new McpAuthDiscovery(settings, (async (url, init) => {
requests.push(String(url));
expect(new Headers(init?.headers).has("Authorization")).toBeFalse();
expect(init?.body ? String(init.body).includes("secret") : false).toBeFalse();
if (String(url) === settings.resource) return new Response(null, { status: 401, headers: { "WWW-Authenticate": 'Basic realm="gateway", Bearer resource_metadata="https://water.example/auth/metadata", scope="mcp.catalog.read"' } });
if (String(url).endsWith("/auth/metadata")) return Response.json({ resource: settings.resource, authorization_servers: [settings.issuer] });
if (String(url).includes("oauth-authorization-server")) return Response.json(metadata);
throw new Error("unexpected endpoint");
}) as McpHttpFetch);
expect(await discovery.resolve()).toEqual({ tokenEndpoint: metadata.token_endpoint, authMethod: "client_secret_post" });
await discovery.resolve();
expect(requests).toEqual([settings.resource, "https://water.example/auth/metadata", "https://identity.example/.well-known/oauth-authorization-server/realm/water"]);
});
it("falls back through resource path, resource root and OAuth/OIDC issuer metadata", async () => {
const requests: string[] = [];
const discovery = new McpAuthDiscovery(settings, (async (url) => {
const path = String(url); requests.push(path);
if (path === settings.resource) return new Response(null, { status: 401 });
if (path === "https://water.example/.well-known/oauth-protected-resource") return Response.json({ resource: settings.resource, authorization_servers: [settings.issuer] });
if (path.endsWith("/realm/water/.well-known/openid-configuration")) return Response.json({ ...metadata, authorization_endpoint: "https://identity.example/authorize", jwks_uri: "https://identity.example/keys", response_types_supported: ["code"], subject_types_supported: ["public"], id_token_signing_alg_values_supported: ["RS256"] });
return new Response(null, { status: 404 });
}) as McpHttpFetch);
expect((await discovery.resolve()).tokenEndpoint).toBe(metadata.token_endpoint);
expect(requests).toEqual([
settings.resource, "https://water.example/.well-known/oauth-protected-resource/mcp", "https://water.example/.well-known/oauth-protected-resource",
"https://identity.example/.well-known/oauth-authorization-server/realm/water", "https://identity.example/.well-known/openid-configuration/realm/water", "https://identity.example/realm/water/.well-known/openid-configuration",
]);
});
it.each(["resource", "authority", "issuer", "endpoint", "method"])("rejects invalid %s metadata before credentials can be sent", async (kind) => {
const discovery = new McpAuthDiscovery(settings, (async (url) => {
if (String(url) === settings.resource) return new Response(null, { status: 401 });
if (String(url).includes("oauth-protected-resource")) return Response.json({ resource: kind === "resource" ? "https://other.example/mcp" : settings.resource, authorization_servers: [kind === "authority" ? "https://attacker.example" : settings.issuer] });
expect(String(url).startsWith("https://identity.example/")).toBeTrue();
return Response.json({ ...metadata, ...(kind === "issuer" ? { issuer: `${settings.issuer}/` } : {}), ...(kind === "endpoint" ? { token_endpoint: "http://attacker.example/token" } : {}), ...(kind === "method" ? { token_endpoint_auth_methods_supported: ["none"] } : {}) });
}) as McpHttpFetch);
await expect(discovery.resolve()).rejects.toMatchObject({ status: 503 });
});
it("rechecks the configured issuer after metadata invalidation", async () => {
let changed = false;
const discovery = new McpAuthDiscovery(settings, (async (url) => {
if (String(url) === settings.resource) return new Response(null, { status: 401 });
if (String(url).includes("oauth-protected-resource")) return Response.json({ resource: settings.resource, authorization_servers: [changed ? "https://new.example" : settings.issuer] });
return Response.json(metadata);
}) as McpHttpFetch);
await discovery.resolve();
changed = true; discovery.invalidate();
await expect(discovery.resolve()).rejects.toMatchObject({ status: 503 });
});
it.each(["allowed", "other-host", "other-port", "production"])("bounds development HTTP discovery: %s", async (kind) => {
const issuer = "http://192.168.1.114:8088/realms/tjwater";
let requests = 0;
const discovery = new McpAuthDiscovery({ ...settings, issuer, devHttpIssuer: issuer,
runtimeEnvironment: kind === "production" ? "production" : "development" }, (async (url, init) => {
requests++;
expect(new Headers(init?.headers).has("Authorization")).toBeFalse();
expect(init?.redirect).toBe("error");
if (String(url) === settings.resource) return new Response(null, { status: 401 });
if (String(url).includes("oauth-protected-resource")) return Response.json({ resource: settings.resource, authorization_servers: [issuer] });
expect(String(url).startsWith("http://192.168.1.114:8088/")).toBeTrue();
return Response.json({ ...metadata, issuer, token_endpoint: kind === "other-host" ? "http://192.168.1.115:8088/token" :
kind === "other-port" ? "http://192.168.1.114:8089/token" : `${issuer}/protocol/openid-connect/token` });
}) as McpHttpFetch);
if (kind === "allowed") expect((await discovery.resolve()).tokenEndpoint).toBe(`${issuer}/protocol/openid-connect/token`);
else await expect(discovery.resolve()).rejects.toMatchObject({ status: 503 });
if (kind === "production") expect(requests).toBe(0);
});
+198
View File
@@ -0,0 +1,198 @@
import { afterEach, describe, expect, it } from "bun:test";
import { McpQueryClient, validateMcpSettings, type McpHttpFetch, assertExchangedIdentity } from "../../src/product/mcp-query-client.js";
import { bindSessionCredential, credentialRefreshCoordinator, refreshSessionCredential, releaseSessionCredential } from "../../src/product/credential-refresh.js";
import { PRODUCT_MCP_TOOLS } from "../../src/runtime/product-mcp-tools.js";
const sessions: string[] = [];
afterEach(() => { for (const id of sessions.splice(0)) releaseSessionCredential(id); });
const settings = { issuer: "http://localhost/realms/test", resource: "http://localhost/mcp", audience: "tjwater-mcp", clientId: "tjwater-agent-mcp", clientSecret: "fixture-secret" };
const owner = (id: string) => ({ context: { sessionId: id, actorKey: "actor", clientSessionId: id, projectId: "project", projectKey: "project", traceId: "test", purpose: "ordinary_chat" as const }, runtimeSessionId: id });
const catalog = PRODUCT_MCP_TOOLS.map((name) => ({ name, inputSchema: { type: "object" as const, additionalProperties: false, properties: { context_handle: { type: "string" }, element_type: { type: "string" }, ...(name === "run_simulation" ? { timeout_seconds: { type: "integer", default: 120, maximum: 3600 } } : {}) }, required: ["context_handle", "element_type"] } }));
// Test through a transport-independent fake only where the concern is token ownership/renewal.
function fixture(http: McpHttpFetch, authMethod = "client_secret_post") {
const client = new McpQueryClient(settings, (async (url, init) => {
if (String(url) === settings.resource && !new Headers(init?.headers).has("Authorization")) return new Response(null, {
status: 401, headers: { "WWW-Authenticate": 'Bearer resource_metadata="http://localhost/.well-known/oauth-protected-resource/mcp"' },
});
if (String(url).includes("oauth-protected-resource")) return Response.json({ resource: settings.resource, authorization_servers: [settings.issuer] });
if (String(url).includes(".well-known")) return Response.json({ issuer: settings.issuer, authorization_endpoint: `${settings.issuer}/authorize`, response_types_supported: ["code"], token_endpoint: `${settings.issuer}/protocol/openid-connect/token`, token_endpoint_auth_methods_supported: [authMethod] });
return http(url, init);
}) as McpHttpFetch);
Object.assign(client, { tools: catalog, withClient: async (_token: string, _project: string, _signal: AbortSignal, run: (client: unknown) => Promise<unknown>) => run({ listTools: async () => ({ tools: catalog }), callTool: async (params: unknown) => ({ content: [], structuredContent: params }) }) });
return client;
}
const jwt = (id: string, overrides: Record<string, unknown> = {}) => `e30.${Buffer.from(JSON.stringify({ sub: "test-subject", iss: settings.issuer, aud: settings.audience, azp: settings.clientId, jti: id, ...overrides })).toString("base64url")}.fixture`;
const resourceToken = jwt("resource");
const token = () => Response.json({ access_token: resourceToken, token_type: "Bearer", expires_in: 300 });
function credential(id: string) { sessions.push(id); bindSessionCredential(id, { authorization: `Bearer ${jwt(id)}`, projectId: "project" }); }
it("forwards the selected simulation budget through HTTP and SDK while rejecting excess before OAuth", async () => {
const id = "mcp-calculation-budget"; credential(id);
let requests = 0, transportTimeout = 0, sdkTimeout = 0;
const client = fixture((async (url, init) => {
requests++;
if (String(url).includes("/token")) return token();
if (init?.method === "POST") return Response.json({ context_handle: "x".repeat(43) });
return new Response(null, { status: 204 });
}) as McpHttpFetch);
Object.assign(client, { withClient: async (_token: string, _project: string, _signal: AbortSignal, run: (client: unknown) => Promise<unknown>, timeout: number) => {
transportTimeout = timeout;
return run({ listTools: async () => ({ tools: catalog }), callTool: async (params: unknown, options: { timeout: number }) => {
sdkTimeout = options.timeout;
return { content: [], structuredContent: params };
} });
} });
await expect(client.query(id, owner(id), "run_simulation", { timeout_seconds: 3601 }, "excess")).rejects.toMatchObject({ status: 400 });
expect(requests).toBe(0);
const result = await client.query(id, owner(id), "run_simulation", { timeout_seconds: 900 }, "long");
expect(transportTimeout).toBe(905_000);
expect(sdkTimeout).toBe(905_000);
expect(result.structuredContent).toMatchObject({ arguments: { timeout_seconds: 900, context_handle: "x".repeat(43) } });
});
it("requires HTTPS except loopback, and fixed resource paths", () => {
expect(() => validateMcpSettings({ ...settings, issuer: "http://remote/realm" })).toThrow();
expect(() => validateMcpSettings({ ...settings, resource: "http://localhost/api" })).toThrow();
expect(() => validateMcpSettings({ ...settings, resource: "https://user:password@example/mcp" })).toThrow();
});
it("allows only an explicit private development issuer while keeping MCP resources protected", () => {
const issuer = "http://192.168.1.114:8088/realms/tjwater";
const local = { ...settings, issuer, devHttpIssuer: issuer, runtimeEnvironment: "development" };
expect(() => validateMcpSettings(local)).not.toThrow();
expect(() => validateMcpSettings({ ...local, runtimeEnvironment: "production" })).toThrow();
expect(() => validateMcpSettings({ ...local, runtimeEnvironment: undefined })).toThrow();
expect(() => validateMcpSettings({ ...local, devHttpIssuer: undefined })).toThrow();
expect(() => validateMcpSettings({ ...local, issuer: `${issuer}/other` })).toThrow();
expect(() => validateMcpSettings({ ...local, resource: "http://192.168.1.114:8088/mcp" })).toThrow();
expect(() => validateMcpSettings({ ...local, issuer: "http://example.com/realm", devHttpIssuer: "http://example.com/realm" })).toThrow();
});
it("rejects personal queries and model supplied scope before any network call", async () => {
let count = 0;
const client = fixture((async () => { count++; return token(); }) as McpHttpFetch);
await expect(client.query("p", { ...owner("p"), context: { ...owner("p").context, projectKey: "personal-actor" } }, "list_elements", {}, "call")).rejects.toMatchObject({ status: 403 });
await expect(client.query("p", owner("p"), "list_elements", { context_handle: "MODEL", project_id: "other" }, "call")).rejects.toMatchObject({ status: 400 });
expect(count).toBe(0);
expect(client.catalog()[0].inputSchema.required).toEqual(["element_type"]);
expect(client.catalog()[0].inputSchema.properties).not.toHaveProperty("context_handle");
});
it("exchanges source credentials only with issuer and injects a host context into MCP", async () => {
const id = "mcp-owner"; credential(id); const requests: Array<{ url: string; init?: RequestInit }> = [];
const client = fixture((async (url, init) => {
requests.push({ url: String(url), init });
if (String(url).includes("/token")) return token();
if (init?.method === "POST") return Response.json({ context_handle: "x".repeat(43) });
return new Response(null, { status: 204 });
}) as McpHttpFetch);
const result = await client.query(id, owner(id), "list_elements", { element_type: "pipe" }, "native-call");
const exchange = new URLSearchParams(String(requests[0].init?.body));
expect(exchange.get("subject_token")).toBe(jwt(id));
expect(exchange.get("audience")).toBe("tjwater-mcp");
expect(exchange.get("resource")).toBe(settings.resource);
const bind = requests.find((request) => request.init?.method === "POST" && !request.url.includes("/token"))!;
expect(new Headers(bind.init?.headers).get("Authorization")).toBe(`Bearer ${resourceToken}`);
expect(JSON.parse(String(bind.init?.body))).toMatchObject({ purpose: "ordinary_chat", native_session_id: id });
expect(result.structuredContent).toMatchObject({ arguments: { context_handle: "x".repeat(43) }, _meta: { "io.tjwater/callId": "native-call" } });
expect(requests.at(-1)?.init?.method).toBe("DELETE");
});
it("coalesces concurrent exchange, isolates owners and discards cached tokens on release", async () => {
const a = "mcp-a", b = "mcp-b"; credential(a); credential(b); let exchanges = 0;
const client = fixture((async (url, init) => {
if (String(url).includes("/token")) { exchanges++; await Bun.sleep(5); return token(); }
if (init?.method === "POST") return Response.json({ context_handle: "x".repeat(43) });
return new Response(null, { status: 204 });
}) as McpHttpFetch);
await Promise.all([client.query(a, owner(a), "list_elements", {}, "one"), client.query(a, owner(a), "list_elements", {}, "two"), client.query(b, owner(b), "list_elements", {}, "one")]);
expect(exchanges).toBe(2);
client.dropSession(a);
await client.query(a, owner(a), "list_elements", {}, "three");
expect(exchanges).toBe(3);
});
it("renews an expired source token through existing credential events", async () => {
const id = "mcp-renew"; credential(id); let exchanges = 0, renewals = 0;
const unsubscribe = credentialRefreshCoordinator.subscribe(id, (event) => {
if (event.type === "credential_refresh_required") {
renewals++; refreshSessionCredential(id, { authorization: `Bearer ${jwt("renewed")}`, projectId: "project" }, event.requestId);
}
});
const client = fixture((async (url, init) => {
if (String(url).includes("/token")) {
exchanges++;
const subject = new URLSearchParams(String(init?.body)).get("subject_token");
return subject === jwt("renewed") ? token() : Response.json({ error: "invalid_grant" }, { status: 400 });
}
if (init?.method === "POST") return Response.json({ context_handle: "x".repeat(43) });
return new Response(null, { status: 204 });
}) as McpHttpFetch);
try { await client.query(id, owner(id), "list_elements", {}, "call"); expect(exchanges).toBe(2); expect(renewals).toBe(1); }
finally { unsubscribe(); }
});
it("does not refresh for scope rejection and renews resource authentication only once", async () => {
const id = "mcp-rejected"; credential(id); let exchanges = 0, binds = 0;
const client = fixture((async (url, init) => {
if (String(url).includes("/token")) { exchanges++; return token(); }
if (init?.method === "POST") { binds++; return Response.json({}, { status: 401 }); }
return new Response(null, { status: 204 });
}) as McpHttpFetch);
await expect(client.query(id, owner(id), "list_elements", {}, "call")).rejects.toMatchObject({ status: 401 });
expect(exchanges).toBe(2); expect(binds).toBe(2);
});
it("cancels a caller waiting for shared credential renewal", async () => {
const id = "mcp-cancel"; credential(id);
let resolve!: (response: Response) => void;
let notifyStarted!: () => void;
const started = new Promise<void>((done) => { notifyStarted = done; });
const client = fixture((async () => new Promise<Response>((done) => { resolve = done; notifyStarted(); })) as McpHttpFetch);
const controller = new AbortController();
const pending = client.query(id, owner(id), "list_elements", {}, "call", controller.signal);
await started;
controller.abort(new Error("cancelled"));
await expect(pending).rejects.toThrow("cancelled");
client.dropSession(id); resolve(token());
});
it("sends resource on both grants and supports advertised Basic client authentication", async () => {
const id = "mcp-basic"; credential(id);
const grants: Array<{ headers: Headers; body: URLSearchParams }> = [];
const client = fixture((async (url, init) => {
if (String(url).endsWith("/token")) {
grants.push({ headers: new Headers(init?.headers), body: new URLSearchParams(String(init?.body)) });
return token();
}
if (init?.method === "POST") return Response.json({ context_handle: "x".repeat(43) });
return new Response(null, { status: 204 });
}) as McpHttpFetch, "client_secret_basic");
await client.prepare();
await client.query(id, owner(id), "list_elements", {}, "call");
expect(grants.map((g) => g.body.get("grant_type"))).toEqual(["client_credentials", "urn:ietf:params:oauth:grant-type:token-exchange"]);
for (const grant of grants) {
expect(grant.body.get("resource")).toBe(settings.resource);
expect(grant.body.has("client_secret")).toBeFalse();
expect(grant.headers.get("Authorization")).toBe(`Basic ${Buffer.from(`${settings.clientId}:${settings.clientSecret}`).toString("base64")}`);
}
});
it("reports an unsupported resource indicator as configuration failure without a weaker retry", async () => {
const id = "mcp-invalid-resource"; credential(id);
let attempts = 0;
const client = fixture((async (_url, init) => {
attempts++;
expect(new URLSearchParams(String(init?.body)).get("resource")).toBe(settings.resource);
return Response.json({ error: "invalid_target" }, { status: 400 });
}) as McpHttpFetch);
await expect(client.query(id, owner(id), "list_elements", {}, "call")).rejects.toThrow("RFC 8707");
expect(attempts).toBe(1);
});
it("rejects an exchanged identity, issuer, client or audience that differs from the verified source", () => {
assertExchangedIdentity(jwt("source"), resourceToken, settings);
for (const overrides of [{ sub: "other" }, { aud: "api" }, { iss: "https://other/realm" }, { azp: "browser" }]) {
expect(() => assertExchangedIdentity(jwt("source"), jwt("resource", overrides), settings)).toThrow();
}
});
+30 -43
View File
@@ -166,6 +166,36 @@ describe("OperationalTaskClient", () => {
expect(JSON.stringify(body)).not.toContain("transient-secret");
});
it("publishes complete progress and presentation with the claimed run without browser receipts", async () => {
const requests: Request[] = [];
globalThis.fetch = (async (input, init) => {
const request = new Request(input, init);
requests.push(request);
if (request.url.endsWith("/claim")) return Response.json(run);
if (request.url.endsWith("/presentation-events")) return Response.json({
recorded: true, event_seq: 12, run_id: runId, execution_generation: 1,
});
return Response.json({ task, plan_revision: null, report_revision: null, progress_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: executionRequestId, expectedRevision: 1, sourceSessionId: "session-1",
});
const progress = { summary: "正在查询投诉时段", diagram: null };
expect(await client.publishArtifacts("runtime-1", taskId, {
requestId: executionRequestId, sourceSessionId: "session-1", progress,
})).toMatchObject({ progress_revision: 3 });
expect(await requests[1]!.json()).toMatchObject({ progress, run_id: runId, source_session_id: "session-1" });
const command = { action: "set_overlay" as const, overlay_id: "complaints", title: "投诉位置", mode: "geometry" as const, evidence_id: "query-1" };
expect(await client.publishPresentation("runtime-1", taskId, {
requestId: executionRequestId, sourceSessionId: "session-1", command,
})).toEqual({ recorded: true, event_seq: 12, run_id: runId, execution_generation: 1 });
expect(await requests[2]!.json()).toEqual({ request_id: executionRequestId, run_id: runId, source_session_id: "session-1", command });
expect(requests).toHaveLength(3);
expect(requests[2]!.headers.get("authorization")).toBe("Bearer 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"))
@@ -217,49 +247,6 @@ describe("OperationalTaskClient", () => {
});
});
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;
-45
View File
@@ -1,45 +0,0 @@
import { afterEach, describe, expect, it } from "bun:test";
import { ProjectDataClient, projectDataQuerySchema } from "../../src/product/project-data-client.js";
import { bindSessionCredential, releaseSessionCredential } from "../../src/product/credential-refresh.js";
import { historyChart } from "../../src/product/history-chart.js";
const originalFetch = globalThis.fetch;
afterEach(() => { globalThis.fetch = originalFetch; releaseSessionCredential("query-session"); });
describe("controlled project queries", () => {
it("dispatches SCADA history to the existing fields endpoint using monitored_value", async () => {
bindSessionCredential("query-session", { authorization: "Bearer TEST", projectId: "trusted-project" });
globalThis.fetch = (async (url) => {
const parsed = new URL(String(url));
expect(parsed.pathname).toBe("/api/v1/timeseries/scada-readings/fields");
expect(parsed.searchParams.get("field")).toBe("monitored_value");
expect(parsed.searchParams.get("device_ids")).toBe("D1,D2");
return Response.json([{ device_id: "D1", value: 20 }]);
}) as typeof fetch;
await new ProjectDataClient("http://server.test").query("query-session", { source: "scada_history", device_ids: ["D1", "D2"], field: "monitored_value", start: "2026-09-01T00:00:00Z", end: "2026-09-01T01:00:00Z" });
});
it("uses fixed routes and the bound project credential, including encoded asset IDs", async () => {
bindSessionCredential("query-session", { authorization: "Bearer TEST", projectId: "trusted-project" });
let requested: URL | undefined;
globalThis.fetch = (async (url, init) => {
requested = new URL(String(url));
expect(new Headers(init?.headers).get("X-Project-Id")).toBe("trusted-project");
return Response.json({ pressure: 20 });
}) as typeof fetch;
expect(await new ProjectDataClient("http://server.test").query("query-session", { source: "element_properties", element_type: "junction", element_id: "J&network=other" })).toEqual({ pressure: 20 });
expect(requested?.pathname).toBe("/api/v1/junctions/properties");
expect(requested?.searchParams.get("junction")).toBe("J&network=other");
expect(requested?.searchParams.has("network")).toBeFalse();
});
it("rejects arbitrary routes, project overrides and excessive time windows before dispatch", () => {
expect(projectDataQuerySchema.safeParse({ source: "scada_devices", project_id: "other" }).success).toBeFalse();
expect(projectDataQuerySchema.safeParse({ source: "http", url: "http://other" }).success).toBeFalse();
expect(projectDataQuerySchema.safeParse({ source: "scada_history", device_ids: ["D1"], field: "monitored_value", start: "2026-09-01T00:00:00Z", end: "2026-09-03T00:00:00Z" }).success).toBeFalse();
});
it("keeps returned units, drops null samples and never manufactures missing history", () => {
expect(() => historyChart({ series: [] }, "J1")).toThrow("NO_HISTORY_DATA");
const chart = historyChart({ series: [{ element_id: "J1", metric: "pressure", source: "scada_raw", source_unit: "m", points: [{ time: "t1", value: null }, { time: "t2", value: 20 }] }] }, "J1");
expect(chart.title).toContain("(m)");
expect(chart.x_data).toEqual(["t2"]);
expect(chart.series[0].data).toEqual([20]);
});
});
+51
View File
@@ -0,0 +1,51 @@
import { describe, expect, it } from "bun:test";
import publishPresentation from "../../.opencode/tools/publish_task_presentation.js";
import publishProgress from "../../.opencode/tools/publish_task_progress.js";
import { taskPublicationCommandSchema, taskPublicationProgressSchema } from "../../.opencode/tools/task-publication-schema.js";
import { taskProgressSchema } from "../../src/product/investigation-report.js";
import { taskPresentationCommandSchema } from "../../src/product/task-presentation.js";
describe("task presentation input boundaries", () => {
it("aligns native and product progress schemas for legacy and current diagrams", () => {
const progress = { summary: "实际读取监测资料", diagram: { title: "当前路线", mermaid: "flowchart TD\nQUERY --> REVIEW", node_refs: [
{ node_id: "QUERY", label: "查询", status: "running", map_targets: [{ source_id: "pipes", feature_id: "P-1" }] },
{ node_id: "REVIEW", label: "复核", status: null },
] } };
expect(taskProgressSchema.parse(taskPublicationProgressSchema.parse(progress))).toEqual(taskProgressSchema.parse(progress));
expect(taskProgressSchema.parse(taskPublicationProgressSchema.parse({ summary: "当前无图" }))).toEqual({ summary: "当前无图", diagram: null });
});
it("accepts finite display commands and rejects unsupported data, layers and chart evidence", () => {
const chart = { title: "压力", chart_type: "line", x_data: ["10:00"], series: [{ name: "MP01", data: [0.4] }] };
const commands = [
{ action: "highlight_features", layer: "scada", feature_ids: ["MP01"] },
{ action: "clear_highlights" },
{ action: "open_chart", chart_id: "pressure", chart, evidence_refs: ["reading-1"] },
{ action: "close_chart", chart_id: "pressure" },
{ action: "set_overlay", overlay_id: "complaints", title: "实际投诉", mode: "heatmap", evidence_id: "query-1" },
{ action: "clear_overlay", overlay_id: "complaints" },
];
for (const command of commands) {
expect(taskPresentationCommandSchema.parse(taskPublicationCommandSchema.parse(command))).toEqual(taskPresentationCommandSchema.parse(command));
}
for (const command of [
{ ...commands[0], layer: "drainage" },
{ ...commands[2], evidence_refs: [] },
{ ...commands[2], chart: { ...chart, series: [{ name: "MP01", data: [0.4, 0.5] }] } },
{ ...commands[4], data: { coordinates: [1, 2] } },
{ ...commands[4], script: "alert(1)" },
]) expect(() => taskPresentationCommandSchema.parse(command)).toThrow();
});
it("revalidates semantic chart and Mermaid inputs before sending a tool request", async () => {
const request_id = "60000000-0000-4000-8000-000000000001";
await expect(publishPresentation.execute({ request_id, command: {
action: "open_chart", chart_id: "pressure", evidence_refs: ["reading-1"],
chart: { title: "压力", chart_type: "line", x_data: ["10:00"], series: [{ name: "MP01", data: [0.4, 0.5] }] },
} }, { sessionID: "test" })).rejects.toThrow("series must align");
await expect(publishProgress.execute({ request_id, progress: {
summary: "查询", diagram: { title: "路线", mermaid: "flowchart TD; A-->B; click A callback", node_refs: [] },
} }, { sessionID: "test" })).rejects.toThrow("Mermaid");
});
});