Files
next-tjwater-agent/tests/mcp/queryServer.test.ts
T
jiang 6a49d2209e 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.
2026-10-19 18:46:58 +08:00

162 lines
11 KiB
TypeScript

import { afterEach, expect, it, mock } from "bun:test";
import { Client, StreamableHTTPClientTransport, type Tool } from "@modelcontextprotocol/client";
import { startQueryMcpServer } from "../../src/mcp/query-server.js";
import { createLocalQueryExecutor } from "../../src/mcp/local-query-tools.js";
import { setRuntimeSessionContext, removeRuntimeSessionContext } from "../../src/runtime/sessionContext.js";
import { LOCAL_QUERY_TOOLS, PRODUCT_MCP_ANALYSIS_TOOLS, queryMcpToolId } from "../../src/runtime/product-mcp-tools.js";
import type { QueryOwner } from "../../src/product/mcp-query-client.js";
import type { SessionTranscriptStore } from "../../src/sessions/transcriptStore.js";
const owner = { sessionId: "query-owner", clientSessionId: "chat-owner", actorKey: "actor-a", projectKey: "project-a", projectId: "project-a", traceId: "test", purpose: "ordinary_chat" as const };
const servers: ReturnType<typeof startQueryMcpServer>[] = [];
const clients: Client[] = [];
afterEach(async () => {
removeRuntimeSessionContext(owner.sessionId);
await Promise.all(clients.splice(0).map((client) => client.close()));
await Promise.all(servers.splice(0).map((server) => server.close()));
});
const productTool: Tool = { name: "get_task_context", description: "Bound task", inputSchema: { type: "object", properties: { report_revision: { type: "integer", minimum: 1 } }, additionalProperties: false } };
it("advertises simulation budgets and derives the native connection ceiling from the catalog", async () => {
const simulation: Tool = { name: "run_simulation", inputSchema: { type: "object", properties: {
timeout_seconds: { type: "integer", minimum: 1, maximum: 3600, default: 120 },
}, additionalProperties: false } };
const { server, client, call, query } = await setup({ productTools: [simulation] });
setRuntimeSessionContext(owner);
expect(server.executionTimeoutMs).toBe(3_665_000);
expect((await client.listTools()).tools[0].inputSchema.properties?.timeout_seconds).toMatchObject({ maximum: 3600 });
expect((await call("run_simulation", { timeout_seconds: 900 })).isError).not.toBeTrue();
expect((query.mock.calls[0] as unknown as unknown[])[3]).toEqual({ timeout_seconds: 900 });
try { await call("run_simulation", { timeout_seconds: 3601 }); } catch { /* SDK schema rejection */ }
expect(query).toHaveBeenCalledTimes(1);
});
it("discovers calculations natively but denies verified readers before upstream dispatch", async () => {
const context = { ...owner, purpose: "task_execution" as const, taskId: "task-a" };
const productTools = PRODUCT_MCP_ANALYSIS_TOOLS.map((name) => ({ name, inputSchema: { type: "object" as const, properties: {}, additionalProperties: false } }));
const { client, call, query } = await setup({ productTools, resolveReader: async () => ({ context, runtimeSessionId: owner.sessionId, taskId: "task-a" }) });
setRuntimeSessionContext(context);
expect((await client.listTools()).tools.map((tool) => tool.name)).toEqual(expect.arrayContaining([...PRODUCT_MCP_ANALYSIS_TOOLS]));
for (const name of PRODUCT_MCP_ANALYSIS_TOOLS) {
expect((await call(name)).isError).not.toBeTrue();
expect((await call(name, {}, "verified-reader")).isError).toBeTrue();
}
expect(query).toHaveBeenCalledTimes(PRODUCT_MCP_ANALYSIS_TOOLS.length);
});
async function setup(extra: Partial<Parameters<typeof startQueryMcpServer>[0]> = {}) {
const executeLocal = mock(async () => ({ safe: true }));
const query = mock(async () => ({ content: [{ type: "text" as const, text: "evidence" }], structuredContent: { evidence_id: "E1", data: { value: 1 } } }));
const server = startQueryMcpServer({ internalToken: "MCP_TEST_ONLY", productClient: { query }, productTools: [productTool], resolveReader: async () => null, executeLocal, ...extra });
servers.push(server);
const client = new Client({ name: "query-test", version: "1" }, { versionNegotiation: { mode: { pin: "2026-07-28" } } });
clients.push(client);
await client.connect(new StreamableHTTPClientTransport(new URL(server.url), { requestInit: { headers: { "x-agent-internal-token": "MCP_TEST_ONLY" } } }));
const call = (name: string, args: Record<string, unknown> = {}, id: string | undefined = owner.sessionId) => client.callTool({ name, arguments: args, ...(id ? { _meta: { "ai.opencode/sessionID": id } } : {}) });
return { server, client, call, executeLocal, query };
}
it("discovers native MCP schemas without duplicate plugins, hidden identity or mixed managers", async () => {
const { client, server } = await setup();
const catalog = await client.listTools();
expect(catalog.tools.map((tool) => tool.name).sort()).toEqual([...LOCAL_QUERY_TOOLS, "get_task_context"].sort());
for (const tool of catalog.tools) {
expect(tool.inputSchema.properties).not.toHaveProperty("session_id");
expect(tool.inputSchema.properties).not.toHaveProperty("authorization");
}
expect(JSON.stringify(catalog)).not.toContain("memory_manager");
expect(JSON.stringify(catalog)).not.toContain("skill_manager");
expect(catalog.tools.some((tool) => tool.name === "web_search")).toBeFalse();
for (const headers of [{}, { "x-agent-internal-token": "wrong" }, { "x-agent-internal-token": "MCP_TEST_ONLY", Origin: "http://localhost" }]) {
expect((await fetch(server.url, { method: "POST", headers: headers as Record<string, string>, body: "{}" })).status).toBe(403);
}
});
it("requires host-authenticated session metadata and blocks personal, review and unbound project queries", async () => {
const { call, executeLocal, query } = await setup();
setRuntimeSessionContext(owner);
expect((await call("session_search", { query: "pressure" })).isError).not.toBeTrue();
expect((await call("session_search", { query: "pressure" }, "foreign")).isError).toBeTrue();
expect((await call("session_search", { query: "pressure" }, "")).isError).toBeTrue();
setRuntimeSessionContext({ ...owner, projectId: undefined, projectKey: "personal-actor-a" });
expect((await call("get_task_context")).isError).toBeTrue();
expect((await call("geocode", { keyword: "北京" })).isError).toBeTrue();
expect((await call("session_search", { query: "pressure" })).isError).not.toBeTrue();
setRuntimeSessionContext({ ...owner, learningMode: "review" });
expect((await call("session_search", { query: "pressure" })).isError).toBeTrue();
expect(executeLocal).toHaveBeenCalledTimes(2);
expect(query).not.toHaveBeenCalled();
});
it("delegates only product reads to a verified child and retains parent revision and evidence", async () => {
const taskOwner = { ...owner, purpose: "task_execution" as const, taskId: "task-a", reportRevision: 3 };
const { call, query, executeLocal } = await setup({ resolveReader: async (id) => id === "verified-child" ? { context: taskOwner, runtimeSessionId: owner.sessionId, taskId: "task-a" } : null });
const result = await call("get_task_context", {}, "verified-child");
expect(result.structuredContent).toEqual({ evidence_id: "E1", data: { value: 1 } });
const args = query.mock.calls[0] as unknown as [string, QueryOwner, string, unknown];
expect(args[0]).toBe("verified-child");
expect(args[1].runtimeSessionId).toBe(owner.sessionId);
expect(args[3]).toEqual({ report_revision: 3 });
expect((await call("session_search", { query: "private" }, "verified-child")).isError).toBeTrue();
expect((await call("read_agent_messages", {}, "verified-child")).isError).toBeTrue();
expect(executeLocal).not.toHaveBeenCalled();
});
it("rejects identity overrides, arbitrary URLs and oversized queries before executing", async () => {
setRuntimeSessionContext(owner);
const { call, executeLocal } = await setup();
for (const [name, args] of [
["session_search", { query: "x", session_id: "foreign" }],
["geocode", { keyword: "北京", url: "https://foreign.test" }],
["geocode", { keyword: "x".repeat(501) }],
["session_search", { query: "x", max_results: 51 }],
["list_investigation_tasks", { offset: 10001 }],
["get_investigation_task", { task_id: "made-up" }],
["read_agent_messages", { limit: 0 }],
] as const) {
let rejected = false;
try { rejected = (await call(name, args)).isError === true; } catch { rejected = true; }
expect(rejected).toBeTrue();
}
expect(executeLocal).not.toHaveBeenCalled();
});
it("searches only the owning transcript scope and pages the owning native checklist", async () => {
const search = mock(async () => []);
const readTodos = mock(async () => Array.from({ length: 25 }, (_, id) => ({ id })));
const executeLocal = createLocalQueryExecutor({ transcripts: { search } as unknown as SessionTranscriptStore, readTodos, messageQuery: async () => ({}) });
const { call } = await setup({ executeLocal });
setRuntimeSessionContext(owner);
await call("session_search", { query: "pressure" });
expect(search.mock.calls[0] as unknown).toEqual([{ actorKey: "actor-a", projectKey: "project-a" }, "pressure", 20]);
const first = await call("todoread");
const body = JSON.parse((first.content[0] as { text: string }).text);
expect(body.items).toHaveLength(20);
expect(body.page).toEqual({ limit: 20, offset: 0, total: 25, has_more: true });
expect((readTodos.mock.calls[0] as unknown as unknown[])[0]).toBe(owner.sessionId);
});
it("returns safe MCP errors instead of leaking upstream credentials", async () => {
setRuntimeSessionContext(owner);
const { call } = await setup({ executeLocal: async () => { throw Object.assign(new Error("Bearer PRIVATE_TOKEN https://private.test"), { status: 401 }); } });
const result = await call("session_search", { query: "x" });
expect(result.isError).toBeTrue();
expect(JSON.stringify(result)).toContain("401");
expect(JSON.stringify(result)).not.toContain("PRIVATE_TOKEN");
expect(queryMcpToolId("session_search")).toBe("mcp_tjwater_session_search");
});
it("replaces unavailable evidence resources with the authorized paging tool without changing provenance", async () => {
setRuntimeSessionContext(owner);
const uri = "tjwater://tasks/task-a/evidence/E1";
const upstream = { content: [{ type: "text" as const, text: `Read ${uri}` }, { type: "resource_link" as const, name: "query-evidence", uri }],
structuredContent: { source: "query_scada_readings", source_ref: "saved:1", evidence_id: "E1", origin: "synthetic", truncated: true,
data: { resource_uri: uri, preview: "The complete query snapshot is saved in task evidence" },
data_gaps: ["Inline result abbreviated; read the saved evidence resource"] } };
const { call } = await setup({ productClient: { query: async () => upstream } });
const result = await call("get_task_context");
expect(JSON.stringify(result)).not.toContain("tjwater://");
expect(result.content.some((item) => item.type === "resource_link")).toBeFalse();
expect(result.structuredContent).toMatchObject({ source_ref: "saved:1", evidence_id: "E1", origin: "synthetic", truncated: true,
data: { evidence_read: { tool: "mcp_tjwater_get_task_evidence", arguments: { evidence_id: "E1", limit: 20, offset: 0 } } } });
expect(upstream.structuredContent.data.resource_uri).toBe(uri);
});