Files
openwork/evals/specs/remote-session-capability.test.ts

345 lines
14 KiB
TypeScript

import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
import { afterAll, beforeAll, expect } from "vitest";
import { test } from "@openwork/testkit";
import type {
RemoteSessionExecuteDeps,
RemoteSessionRuntime,
RemoteSessionToolResult,
} from "../../ee/apps/den-api/src/mcp/remote-session-capabilities.js";
type RemoteSessionModule = typeof import("../../ee/apps/den-api/src/mcp/remote-session-capabilities.js");
/**
* The module reads den-api env at import time (db wiring for its default
* runtime resolver). Seed placeholder env before the dynamic import; every
* test here injects its own runtime resolver, so no database is touched.
*/
function seedRequiredEnv() {
process.env.DATABASE_URL = process.env.DATABASE_URL ?? "mysql://root:password@127.0.0.1:3306/openwork_test";
process.env.DEN_DB_ENCRYPTION_KEY = process.env.DEN_DB_ENCRYPTION_KEY ?? "x".repeat(32);
process.env.BETTER_AUTH_SECRET = process.env.BETTER_AUTH_SECRET ?? "y".repeat(32);
process.env.BETTER_AUTH_URL = process.env.BETTER_AUTH_URL ?? "http://127.0.0.1:8790";
process.env.DEN_API_PUBLIC_URL = process.env.DEN_API_PUBLIC_URL ?? "http://127.0.0.1:8790";
}
let DEFAULT_REMOTE_SESSION_DEPS: RemoteSessionModule["DEFAULT_REMOTE_SESSION_DEPS"];
let executeRemoteSessionCapability: RemoteSessionModule["executeRemoteSessionCapability"];
let searchRemoteSessionCapabilities: RemoteSessionModule["searchRemoteSessionCapabilities"];
/**
* A witness openwork-server: the exact native OpenCode routes the
* headless-threads client speaks through `/workspace/:id/opencode`,
* recording every request so assertions observe the real wire traffic the
* `remote-session:*` capabilities produce — not a stubbed client.
*/
const WORKSPACE_ID = "ws_witness";
const CLIENT_TOKEN = "witness-client-token";
const HOST_TOKEN = "witness-host-token";
type WitnessRequest = {
method: string;
path: string;
authorization: string | undefined;
hostToken: string | undefined;
body: unknown;
};
type WitnessSession = {
id: string;
title: string;
prompts: string[];
};
const witness = {
requests: [] as WitnessRequest[],
sessions: new Map<string, WitnessSession>(),
nextSessionId: 1,
};
function readBody(request: IncomingMessage): Promise<unknown> {
return new Promise((resolve) => {
const chunks: Buffer[] = [];
request.on("data", (chunk: Buffer) => chunks.push(chunk));
request.on("end", () => {
const text = Buffer.concat(chunks).toString("utf8");
if (!text) return resolve(undefined);
try {
resolve(JSON.parse(text));
} catch {
resolve(text);
}
});
});
}
function json(response: ServerResponse, status: number, payload: unknown) {
response.writeHead(status, { "Content-Type": "application/json" });
response.end(JSON.stringify(payload));
}
function snapshotWire(session: WitnessSession) {
const messages = session.prompts.flatMap((prompt, index) => [
{
info: { id: `msg_user_${index}`, role: "user", time: { created: index * 2 } },
parts: [{ id: `part_user_${index}`, type: "text", text: prompt }],
},
{
info: { id: `msg_assistant_${index}`, role: "assistant", parentID: `msg_user_${index}`, time: { created: index * 2 + 1 } },
parts: [{ id: `part_assistant_${index}`, type: "text", text: `witness reply to: ${prompt}` }],
},
]);
return {
session: { id: session.id, title: session.title, directory: null, time: { created: 1 } },
messages,
todos: [],
status: { type: "idle" },
};
}
async function handle(request: IncomingMessage, response: ServerResponse) {
const path = request.url ?? "";
const method = request.method ?? "GET";
const body = await readBody(request);
witness.requests.push({
method,
path,
authorization: request.headers.authorization,
hostToken: typeof request.headers["x-openwork-host-token"] === "string" ? request.headers["x-openwork-host-token"] : undefined,
body,
});
const prefix = `/workspace/${WORKSPACE_ID}/opencode/session`;
if (method === "POST" && path === prefix) {
const title = typeof body === "object" && body !== null && typeof (body as Record<string, unknown>).title === "string"
? String((body as Record<string, unknown>).title)
: "Untitled";
const session: WitnessSession = {
id: `ses_witness_${witness.nextSessionId++}`,
title,
prompts: [],
};
witness.sessions.set(session.id, session);
return json(response, 200, snapshotWire(session).session);
}
const messagesMatch = path.match(new RegExp(`^${prefix}/([^/]+)/message$`));
if (method === "GET" && messagesMatch) {
const session = witness.sessions.get(decodeURIComponent(messagesMatch[1] ?? ""));
if (!session) return json(response, 404, { code: "not_found", message: "session not found" });
return json(response, 200, snapshotWire(session).messages);
}
if (method === "GET" && path === `${prefix}/status`) {
return json(response, 200, {});
}
const sessionMatch = path.match(new RegExp(`^${prefix}/([^/]+)$`));
if (method === "GET" && sessionMatch) {
const session = witness.sessions.get(decodeURIComponent(sessionMatch[1] ?? ""));
if (!session) return json(response, 404, { code: "not_found", message: "session not found" });
return json(response, 200, snapshotWire(session).session);
}
const todoMatch = path.match(new RegExp(`^${prefix}/([^/]+)/todo$`));
if (method === "GET" && todoMatch) {
const session = witness.sessions.get(decodeURIComponent(todoMatch[1] ?? ""));
if (!session) return json(response, 404, { code: "not_found", message: "session not found" });
return json(response, 200, snapshotWire(session).todos);
}
const promptMatch = path.match(new RegExp(`^${prefix}/([^/]+)/prompt_async$`));
if (method === "POST" && promptMatch) {
const session = witness.sessions.get(decodeURIComponent(promptMatch[1] ?? ""));
if (!session) return json(response, 404, { code: "not_found", message: "session not found" });
const parts = typeof body === "object" && body !== null ? (body as Record<string, unknown>).parts : undefined;
const text = Array.isArray(parts) && typeof parts[0] === "object" && parts[0] !== null
? String((parts[0] as Record<string, unknown>).text ?? "")
: "";
session.prompts.push(text);
return json(response, 200, {});
}
return json(response, 404, { code: "not_found", message: `no witness route for ${method} ${path}` });
}
let server: Server;
let runtime: RemoteSessionRuntime;
let deps: RemoteSessionExecuteDeps;
beforeAll(async () => {
seedRequiredEnv();
const module = await import("../../ee/apps/den-api/src/mcp/remote-session-capabilities.js");
DEFAULT_REMOTE_SESSION_DEPS = module.DEFAULT_REMOTE_SESSION_DEPS;
executeRemoteSessionCapability = module.executeRemoteSessionCapability;
searchRemoteSessionCapabilities = module.searchRemoteSessionCapabilities;
server = createServer((request, response) => {
void handle(request, response);
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
if (address === null || typeof address === "string") throw new Error("witness server did not report a port");
runtime = {
workerId: "worker_witness",
baseUrl: `http://127.0.0.1:${address.port}`,
workspaceId: WORKSPACE_ID,
clientToken: CLIENT_TOKEN,
hostToken: HOST_TOKEN,
};
deps = {
resolveRuntime: async () => ({ ok: true, runtime }),
createClient: DEFAULT_REMOTE_SESSION_DEPS.createClient,
};
});
afterAll(async () => {
await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
});
// DenTypeId<"organization"> is the template-literal type `org_${string}`.
const ORGANIZATION_ID = "org_witness_fixture";
function input(action: "create" | "send" | "read", body: unknown, hasWriteScope = true) {
return { action, organizationId: ORGANIZATION_ID, userId: "user_witness", hasWriteScope, body };
}
function payload(result: RemoteSessionToolResult): Record<string, unknown> {
return JSON.parse(result.content[0]?.text ?? "{}") as Record<string, unknown>;
}
test("remote-session capabilities drive a real openwork-server wire with scoped guardrails", async ({ evidence }) => {
const matches = searchRemoteSessionCapabilities("send a chat to my cloud web session", 10);
expect(matches.map((match) => match.name)).toContain("remote-session:send");
expect(matches.every((match) => match.invocation?.argumentsField === "body")).toBe(true);
evidence.recordAssertionEvidence(
"Discovery precedes execution",
"search_capabilities-shaped matches expose remote-session names with body-argument invocation metadata.",
true,
);
const created = await executeRemoteSessionCapability(
input("create", { title: "Witness handoff", prompt: "start working" }),
deps,
);
expect(created.isError).toBeUndefined();
const createdBody = payload(created);
const sessionId = String(createdBody.sessionId);
expect(sessionId.startsWith("ses_witness_")).toBe(true);
expect(createdBody.workspaceId).toBe(WORKSPACE_ID);
expect(createdBody.started).toBe(true);
const createRequest = witness.requests.find(
(request) => request.method === "POST" && request.path === `/workspace/${WORKSPACE_ID}/opencode/session`,
);
expect(createRequest).toBeDefined();
expect(createRequest?.authorization).toBe(`Bearer ${CLIENT_TOKEN}`);
expect(createRequest?.hostToken).toBe(HOST_TOKEN);
evidence.recordAssertionEvidence(
"Create reaches the worker's native OpenCode route with worker credentials",
"remote-session:create produced POST /workspace/:id/opencode/session on the witness with the collaborator bearer token and host token header, returning the native session id.",
true,
);
const sent = await executeRemoteSessionCapability(
input("send", { sessionId, prompt: "and now summarize" }),
deps,
);
expect(sent.isError).toBeUndefined();
expect(payload(sent).state).toBe("accepted");
const promptRequest = witness.requests.findLast(
(request) => request.method === "POST" && request.path.includes("/prompt_async"),
);
expect(promptRequest).toBeDefined();
const promptParts = (promptRequest?.body as { parts?: { text?: string }[] }).parts;
expect(promptParts?.[0]?.text).toBe("and now summarize");
evidence.recordAssertionEvidence(
"Send routes through the native prompt mount asynchronously",
"remote-session:send posted the prompt to /workspace/:id/opencode/session/:id/prompt_async and returned an acceptance receipt without waiting for a reply.",
true,
);
const read = await executeRemoteSessionCapability(input("read", { sessionId, limit: 3 }, false), deps);
expect(read.isError).toBeUndefined();
const readBody = payload(read);
expect(readBody.status).toBe("idle");
expect(readBody.messageCount).toBe(4);
expect((readBody.messages as unknown[]).length).toBe(3);
expect(String(readBody.finalAssistantText)).toBe("witness reply to: and now summarize");
evidence.recordAssertionEvidence(
"Read returns a bounded transcript from the worker snapshot",
"remote-session:read (read scope only) returned session status, total message count 4, a limit-bounded slice of 3 messages, and the final assistant text from the witness snapshot.",
true,
);
// Guardrail: a session id absent from the caller's own worker.
const foreign = await executeRemoteSessionCapability(
input("read", { sessionId: "ses_of_someone_else" }),
deps,
);
expect(foreign.isError).toBe(true);
const foreignBody = payload(foreign);
expect(foreignBody.error).toBe("unknown_session");
expect(foreignBody.retryable).toBe(false);
evidence.recordAssertionEvidence(
"Member scoping degrades to unknown_session",
"A session id absent from the caller's own worker (including another member's session id) returns unknown_session rather than leaking existence or data.",
true,
);
// Guardrail: no cloud runtime resolved for the member.
const requestsBeforeNoRuntime = witness.requests.length;
const noRuntime = await executeRemoteSessionCapability(input("create", { title: "no runtime" }), {
resolveRuntime: async () => ({
ok: false,
error: "needs_cloud_setup",
message: "No OpenWork Cloud workspace is available for your account yet.",
retryable: false,
}),
createClient: DEFAULT_REMOTE_SESSION_DEPS.createClient,
});
expect(noRuntime.isError).toBe(true);
expect(payload(noRuntime).error).toBe("needs_cloud_setup");
expect(witness.requests.length).toBe(requestsBeforeNoRuntime);
evidence.recordAssertionEvidence(
"Runtime resolution gates all worker traffic",
"When the member has no cloud runtime, the capability returns the actionable needs_cloud_setup result and zero requests reach the worker.",
true,
);
// Guardrail: mcp:write gates mutations before any side effect.
const requestsBeforeScope = witness.requests.length;
const createdWithoutScope = await executeRemoteSessionCapability(input("create", {}, false), deps);
const sentWithoutScope = await executeRemoteSessionCapability(
input("send", { sessionId, prompt: "x" }, false),
deps,
);
expect(createdWithoutScope.isError).toBe(true);
expect(sentWithoutScope.isError).toBe(true);
expect(payload(createdWithoutScope).error).toBe("insufficient_mcp_scope");
expect(payload(sentWithoutScope).error).toBe("insufficient_mcp_scope");
expect(witness.requests.length).toBe(requestsBeforeScope);
evidence.recordAssertionEvidence(
"mcp:write gates mutations",
"Without the write scope, create and send fail with insufficient_mcp_scope and no request reaches the worker; read remains available with read scope.",
true,
);
});
test("remote-session capability accepts a JSON-stringified body", async ({ evidence }) => {
const created = await executeRemoteSessionCapability(
input("create", JSON.stringify({ title: "Stringified handoff" })),
deps,
);
expect(created.isError).toBeUndefined();
expect(payload(created).sessionId).toMatch(/^ses_witness_/);
expect(witness.requests.findLast(
(request) => request.method === "POST" && request.path === `/workspace/${WORKSPACE_ID}/opencode/session`,
)?.body).toMatchObject({ title: "Stringified handoff" });
evidence.recordAssertionEvidence(
"JSON-stringified capability body is normalized",
"remote-session:create accepted a JSON-stringified body and passed its title to the native session route.",
true,
);
});