claude-code-best/packages/remote-control-server/src/__tests__/routes.test.ts
Cheng Zi Feng d1176325a4 fix(remote-control): harden self-hosted session flows (#278)
Co-authored-by: chengzifeng <chengzifeng@meituan.com>
2026-06-04 23:50:24 +08:00

1351 lines
46 KiB
TypeScript

import { describe, test, expect, beforeEach, mock } from "bun:test";
// Mock config
const mockConfig = {
port: 3000,
host: "0.0.0.0",
apiKeys: ["test-api-key"],
baseUrl: "http://localhost:3000",
pollTimeout: 1,
heartbeatInterval: 20,
jwtExpiresIn: 3600,
disconnectTimeout: 300,
};
mock.module("../config", () => ({
config: mockConfig,
getBaseUrl: () => "http://localhost:3000",
}));
import { Hono } from "hono";
import { storeReset, storeCreateSession, storeCreateEnvironment, storeBindSession } from "../store";
import { removeEventBus, getAllEventBuses, getEventBus } from "../transport/event-bus";
import { issueToken } from "../auth/token";
import { publishSessionEvent } from "../services/transport";
// Import route modules
import v1Sessions from "../routes/v1/sessions";
import v1Environments from "../routes/v1/environments";
import v1EnvironmentsWork from "../routes/v1/environments.work";
import v1SessionIngress, { websocket as sessionIngressWebsocket } from "../routes/v1/session-ingress";
import v2CodeSessions from "../routes/v2/code-sessions";
import v2Worker from "../routes/v2/worker";
import v2WorkerEventsStream from "../routes/v2/worker-events-stream";
import v2WorkerEvents from "../routes/v2/worker-events";
import webAuth from "../routes/web/auth";
import webSessions from "../routes/web/sessions";
import webControl from "../routes/web/control";
import webEnvironments from "../routes/web/environments";
function createApp() {
const app = new Hono();
app.route("/v1/sessions", v1Sessions);
app.route("/v1/environments", v1Environments);
app.route("/v1/environments", v1EnvironmentsWork);
app.route("/v2/session_ingress", v1SessionIngress);
app.route("/v1/code/sessions", v2CodeSessions);
app.route("/v1/code/sessions", v2Worker);
app.route("/v1/code/sessions", v2WorkerEventsStream);
app.route("/v1/code/sessions", v2WorkerEvents);
app.route("/web", webAuth);
app.route("/web", webSessions);
app.route("/web", webControl);
app.route("/web", webEnvironments);
return app;
}
const AUTH_HEADERS = { Authorization: "Bearer test-api-key", "X-Username": "testuser" };
function toWebSessionId(sessionId: string): string {
if (!sessionId.startsWith("cse_")) return sessionId;
return `session_${sessionId.slice("cse_".length)}`;
}
describe("V1 Session Routes", () => {
let app: Hono;
beforeEach(() => {
storeReset();
for (const [key] of getAllEventBuses()) {
removeEventBus(key);
}
app = createApp();
});
test("POST /v1/sessions — creates a session", async () => {
const res = await app.request("/v1/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ title: "Test Session" }),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.id).toMatch(/^session_/);
expect(body.title).toBe("Test Session");
expect(body.status).toBe("idle");
});
test("POST /v1/sessions — requires auth", async () => {
const res = await app.request("/v1/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
expect(res.status).toBe(401);
});
test("GET /v1/sessions/:id — returns created session", async () => {
const createRes = await app.request("/v1/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await createRes.json();
const getRes = await app.request(`/v1/sessions/${id}`, {
headers: AUTH_HEADERS,
});
expect(getRes.status).toBe(200);
const body = await getRes.json();
expect(body.id).toBe(id);
});
test("GET /v1/sessions/:id — 404 for unknown session", async () => {
const res = await app.request("/v1/sessions/nope", {
headers: AUTH_HEADERS,
});
expect(res.status).toBe(404);
});
test("GET /v1/sessions/:id — resolves compat code session IDs", async () => {
const createRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const {
session: { id },
} = await createRes.json();
const getRes = await app.request(`/v1/sessions/${toWebSessionId(id)}`, {
headers: AUTH_HEADERS,
});
expect(getRes.status).toBe(200);
const body = await getRes.json();
expect(body.id).toBe(id);
});
test("PATCH /v1/sessions/:id — updates title", async () => {
const createRes = await app.request("/v1/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await createRes.json();
const patchRes = await app.request(`/v1/sessions/${id}`, {
method: "PATCH",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ title: "Updated Title" }),
});
expect(patchRes.status).toBe(200);
const body = await patchRes.json();
expect(body.title).toBe("Updated Title");
});
test("POST /v1/sessions/:id/archive — archives session", async () => {
const createRes = await app.request("/v1/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await createRes.json();
const archiveRes = await app.request(`/v1/sessions/${id}/archive`, {
method: "POST",
headers: AUTH_HEADERS,
});
expect(archiveRes.status).toBe(200);
});
test("POST /v1/sessions/:id/archive — archives compat code session IDs", async () => {
const createRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const {
session: { id },
} = await createRes.json();
const compatId = toWebSessionId(id);
const archiveRes = await app.request(`/v1/sessions/${compatId}/archive`, {
method: "POST",
headers: AUTH_HEADERS,
});
expect(archiveRes.status).toBe(200);
const getRes = await app.request(`/v1/sessions/${compatId}`, {
headers: AUTH_HEADERS,
});
expect(getRes.status).toBe(200);
const body = await getRes.json();
expect(body.id).toBe(id);
expect(body.status).toBe("archived");
});
test("POST /v1/sessions/:id/events — publishes events", async () => {
const createRes = await app.request("/v1/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await createRes.json();
const eventsRes = await app.request(`/v1/sessions/${id}/events`, {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ events: [{ type: "user", content: "hello" }] }),
});
expect(eventsRes.status).toBe(200);
const body = await eventsRes.json();
expect(body.events).toBe(1);
});
test("POST /v1/sessions/:id/events — resolves compat code session IDs", async () => {
const createRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const {
session: { id },
} = await createRes.json();
const compatId = toWebSessionId(id);
const eventsRes = await app.request(`/v1/sessions/${compatId}/events`, {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ events: [{ type: "user", content: "hello from compat" }] }),
});
expect(eventsRes.status).toBe(200);
const events = getEventBus(id).getEventsSince(0);
expect(events).toHaveLength(1);
expect(events[0]?.type).toBe("user");
expect((events[0]?.payload as { content?: string }).content).toBe("hello from compat");
});
test("POST /v1/sessions with environment_id creates work item", async () => {
// First register an environment
const envRes = await app.request("/v1/environments/bridge", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ machine_name: "test" }),
});
const { environment_id } = await envRes.json();
const sessRes = await app.request("/v1/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ environment_id }),
});
expect(sessRes.status).toBe(200);
const body = await sessRes.json();
expect(body.environment_id).toBe(environment_id);
});
test("POST /v1/sessions with invalid environment_id — session created, work item fails silently", async () => {
const sessRes = await app.request("/v1/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ environment_id: "env_nonexistent" }),
});
expect(sessRes.status).toBe(200);
const body = await sessRes.json();
expect(body.id).toMatch(/^session_/);
});
test("POST /v1/sessions with events — publishes initial events", async () => {
const sessRes = await app.request("/v1/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ events: [{ type: "init", data: "starting" }] }),
});
expect(sessRes.status).toBe(200);
});
});
describe("V1 Environment Routes", () => {
let app: Hono;
beforeEach(() => {
storeReset();
app = createApp();
});
test("POST /v1/environments/bridge — registers environment", async () => {
const res = await app.request("/v1/environments/bridge", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ machine_name: "mac1", directory: "/home" }),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.environment_id).toMatch(/^env_/);
expect(body.status).toBe("active");
});
test("DELETE /v1/environments/bridge/:id — deregisters environment", async () => {
const envRes = await app.request("/v1/environments/bridge", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { environment_id } = await envRes.json();
const delRes = await app.request(`/v1/environments/bridge/${environment_id}`, {
method: "DELETE",
headers: AUTH_HEADERS,
});
expect(delRes.status).toBe(200);
});
test("POST /v1/environments/:id/bridge/reconnect — reconnects environment", async () => {
const envRes = await app.request("/v1/environments/bridge", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { environment_id } = await envRes.json();
const reconnectRes = await app.request(`/v1/environments/${environment_id}/bridge/reconnect`, {
method: "POST",
headers: AUTH_HEADERS,
});
expect(reconnectRes.status).toBe(200);
});
});
describe("V1 Work Routes", () => {
let app: Hono;
let envId: string;
beforeEach(async () => {
storeReset();
app = createApp();
const envRes = await app.request("/v1/environments/bridge", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
envId = (await envRes.json()).environment_id;
});
test("GET /v1/environments/:id/work/poll — returns 204 when no work", async () => {
const res = await app.request(`/v1/environments/${envId}/work/poll`, {
headers: AUTH_HEADERS,
});
expect(res.status).toBe(204);
});
test("work lifecycle: create → poll → ack → stop", async () => {
// Create session with environment (creates work item)
const sessRes = await app.request("/v1/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ environment_id: envId }),
});
const sessionId = (await sessRes.json()).id;
// Poll for work
const pollRes = await app.request(`/v1/environments/${envId}/work/poll`, {
headers: AUTH_HEADERS,
});
expect(pollRes.status).toBe(200);
const work = await pollRes.json();
expect(work.id).toMatch(/^work_/);
expect(work.data.id).toBe(sessionId);
// Ack work
const ackRes = await app.request(`/v1/environments/${envId}/work/${work.id}/ack`, {
method: "POST",
headers: AUTH_HEADERS,
});
expect(ackRes.status).toBe(200);
// Stop work
const stopRes = await app.request(`/v1/environments/${envId}/work/${work.id}/stop`, {
method: "POST",
headers: AUTH_HEADERS,
});
expect(stopRes.status).toBe(200);
});
test("POST work heartbeat", async () => {
// Create session + work
await app.request("/v1/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ environment_id: envId }),
});
const pollRes = await app.request(`/v1/environments/${envId}/work/poll`, {
headers: AUTH_HEADERS,
});
const work = await pollRes.json();
const hbRes = await app.request(`/v1/environments/${envId}/work/${work.id}/heartbeat`, {
method: "POST",
headers: AUTH_HEADERS,
});
expect(hbRes.status).toBe(200);
const body = await hbRes.json();
expect(body.lease_extended).toBe(true);
});
});
describe("V2 Code Session Routes", () => {
let app: Hono;
beforeEach(() => {
storeReset();
process.env.RCS_API_KEYS = "test-api-key";
app = createApp();
});
test("POST /v1/code/sessions — creates code session", async () => {
const res = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ title: "Code Session" }),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.session.id).toMatch(/^cse_/);
expect(body.session.title).toBe("Code Session");
});
test("POST /v1/code/sessions/:id/bridge — returns bridge info with JWT", async () => {
// Create code session
const createRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = (await createRes.json()).session;
const bridgeRes = await app.request(`/v1/code/sessions/${id}/bridge`, {
method: "POST",
headers: AUTH_HEADERS,
});
expect(bridgeRes.status).toBe(200);
const body = await bridgeRes.json();
expect(body.api_base_url).toBe("http://localhost:3000");
expect(body.worker_epoch).toBe(1);
expect(body.worker_jwt).toBeTruthy();
expect(body.expires_in).toBe(3600);
});
test("POST /v1/code/sessions/:id/bridge — 404 for unknown session", async () => {
const res = await app.request("/v1/code/sessions/nope/bridge", {
method: "POST",
headers: AUTH_HEADERS,
});
expect(res.status).toBe(404);
});
});
describe("V2 Worker Routes", () => {
let app: Hono;
beforeEach(() => {
storeReset();
process.env.RCS_API_KEYS = "test-api-key";
app = createApp();
});
test("POST /v1/code/sessions/:id/worker/register — increments epoch", async () => {
// Create session
const createRes = await app.request("/v1/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await createRes.json();
const regRes = await app.request(`/v1/code/sessions/${id}/worker/register`, {
method: "POST",
headers: AUTH_HEADERS,
});
expect(regRes.status).toBe(200);
const body = await regRes.json();
expect(body.worker_epoch).toBe(1);
});
test("POST /v1/code/sessions/:id/worker/register — 404 for unknown", async () => {
const res = await app.request("/v1/code/sessions/nope/worker/register", {
method: "POST",
headers: AUTH_HEADERS,
});
expect(res.status).toBe(404);
});
});
describe("Web Auth Routes", () => {
let app: Hono;
beforeEach(() => {
storeReset();
app = createApp();
});
test("POST /web/bind — binds session to UUID", async () => {
// Create session first
const sessRes = await app.request("/v1/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await sessRes.json();
const bindRes = await app.request("/web/bind?uuid=test-uuid", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId: id }),
});
expect(bindRes.status).toBe(200);
const body = await bindRes.json();
expect(body.ok).toBe(true);
});
test("POST /web/bind — binds compat code session ID to UUID", async () => {
const sessRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const body = await sessRes.json();
const compatId = toWebSessionId(body.session.id);
const bindRes = await app.request("/web/bind?uuid=test-uuid", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId: compatId }),
});
expect(bindRes.status).toBe(200);
const bindBody = await bindRes.json();
expect(bindBody.ok).toBe(true);
expect(bindBody.sessionId).toBe(compatId);
});
test("POST /web/bind — 404 for unknown session", async () => {
const res = await app.request("/web/bind?uuid=test-uuid", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId: "nope" }),
});
expect(res.status).toBe(404);
});
test("POST /web/bind — 400 when missing params", async () => {
const res = await app.request("/web/bind", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
expect(res.status).toBe(400);
});
});
describe("Web Session Routes", () => {
let app: Hono;
beforeEach(() => {
storeReset();
for (const [key] of getAllEventBuses()) {
removeEventBus(key);
}
app = createApp();
});
test("POST /web/sessions — creates and auto-binds session", async () => {
const res = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "Web Session" }),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.id).toMatch(/^session_/);
expect(body.source).toBe("web");
});
test("GET /web/sessions — returns sessions owned by UUID", async () => {
// Create and bind
const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await createRes.json();
const listRes = await app.request("/web/sessions?uuid=user-1");
expect(listRes.status).toBe(200);
const sessions = await listRes.json();
expect(sessions).toHaveLength(1);
expect(sessions[0].id).toBe(id);
});
test("GET /web/sessions and /all — serialize owned code sessions as compat IDs", async () => {
const codeSession = storeCreateSession({ idPrefix: "cse_" });
storeBindSession(codeSession.id, "user-1");
const compatId = toWebSessionId(codeSession.id);
const listRes = await app.request("/web/sessions?uuid=user-1");
expect(listRes.status).toBe(200);
const sessions = await listRes.json();
expect(sessions).toHaveLength(1);
expect(sessions[0].id).toBe(compatId);
const allRes = await app.request("/web/sessions/all?uuid=user-1");
expect(allRes.status).toBe(200);
const summaries = await allRes.json();
expect(summaries).toHaveLength(1);
expect(summaries[0].id).toBe(compatId);
});
test("GET /web/sessions — requires UUID", async () => {
const res = await app.request("/web/sessions");
expect(res.status).toBe(401);
});
test("GET /web/sessions/all — lists only sessions owned by requesting UUID", async () => {
// Create 2 sessions via different users
await app.request("/web/sessions?uuid=user-1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
await app.request("/web/sessions?uuid=user-2", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const allRes = await app.request("/web/sessions/all?uuid=user-1");
expect(allRes.status).toBe(200);
const sessions = await allRes.json();
expect(sessions).toHaveLength(1); // only user-1's session, not user-2's
});
test("GET /web/sessions and /all — hides archived and inactive sessions", async () => {
const archived = storeCreateSession({});
const inactive = storeCreateSession({});
const open = storeCreateSession({});
storeBindSession(archived.id, "user-1");
storeBindSession(inactive.id, "user-1");
storeBindSession(open.id, "user-1");
await app.request(`/v1/sessions/${archived.id}/archive`, {
method: "POST",
headers: AUTH_HEADERS,
});
const { storeUpdateSession } = await import("../store");
storeUpdateSession(inactive.id, { status: "inactive" });
const listRes = await app.request("/web/sessions?uuid=user-1");
expect(listRes.status).toBe(200);
const sessions = await listRes.json();
expect(sessions.map((session: { id: string }) => session.id)).toEqual([open.id]);
const allRes = await app.request("/web/sessions/all?uuid=user-1");
expect(allRes.status).toBe(200);
const summaries = await allRes.json();
expect(summaries.map((session: { id: string }) => session.id)).toEqual([open.id]);
});
test("GET /web/sessions/:id — returns owned session", async () => {
const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await createRes.json();
const getRes = await app.request(`/web/sessions/${id}?uuid=user-1`);
expect(getRes.status).toBe(200);
});
test("GET /web/sessions/:id — 403 for non-owner", async () => {
const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await createRes.json();
const getRes = await app.request(`/web/sessions/${id}?uuid=user-2`);
expect(getRes.status).toBe(403);
});
test("GET /web/sessions/:id/history — returns events", async () => {
const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await createRes.json();
const histRes = await app.request(`/web/sessions/${id}/history?uuid=user-1`);
expect(histRes.status).toBe(200);
const body = await histRes.json();
expect(body.events).toEqual([]);
});
test("GET /web/sessions/:id and history — supports compat code session IDs", async () => {
const codeSession = storeCreateSession({ idPrefix: "cse_" });
storeBindSession(codeSession.id, "user-1");
const compatId = toWebSessionId(codeSession.id);
const getRes = await app.request(`/web/sessions/${compatId}?uuid=user-1`);
expect(getRes.status).toBe(200);
const session = await getRes.json();
expect(session.id).toBe(compatId);
const histRes = await app.request(`/web/sessions/${compatId}/history?uuid=user-1`);
expect(histRes.status).toBe(200);
const history = await histRes.json();
expect(history.events).toEqual([]);
});
test("GET /web/sessions/:id/history — 403 for non-owner", async () => {
const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await createRes.json();
const histRes = await app.request(`/web/sessions/${id}/history?uuid=user-2`);
expect(histRes.status).toBe(403);
});
test("GET /web/sessions/:id — 404 after session deleted", async () => {
const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await createRes.json();
// Archive/delete the session via v1
await app.request(`/v1/sessions/${id}/archive`, {
method: "POST",
headers: AUTH_HEADERS,
});
// Session still exists (archived), so we can still get it
const getRes = await app.request(`/web/sessions/${id}?uuid=user-1`);
// After archive, session status is "archived" but still exists
expect(getRes.status).toBe(200);
});
test("GET /web/sessions/:id/history — 404 for non-existent session", async () => {
// Bind to a non-existent session won't work, but if ownership was set
// and session deleted, we need to test the 404 path
const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await createRes.json();
// Delete the session from store directly
const { storeDeleteSession } = await import("../store");
storeDeleteSession(id);
const histRes = await app.request(`/web/sessions/${id}/history?uuid=user-1`);
expect(histRes.status).toBe(404);
});
test("POST /web/sessions with invalid environment_id — handles work item error", async () => {
const res = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ environment_id: "env_nonexistent" }),
});
// Session is still created even if work item fails
expect(res.status).toBe(200);
const body = await res.json();
expect(body.id).toMatch(/^session_/);
});
test("GET /web/sessions/:id/events — returns SSE stream", async () => {
const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await createRes.json();
const eventsRes = await app.request(`/web/sessions/${id}/events?uuid=user-1`);
expect(eventsRes.status).toBe(200);
expect(eventsRes.headers.get("Content-Type")).toBe("text/event-stream");
// Read initial keepalive and cancel
const reader = eventsRes.body?.getReader();
if (reader) {
const { value } = await reader.read();
const text = new TextDecoder().decode(value!);
expect(text).toContain(": keepalive");
reader.cancel();
}
});
test("GET /web/sessions/:id/events — supports compat code session IDs", async () => {
const codeSession = storeCreateSession({ idPrefix: "cse_" });
storeBindSession(codeSession.id, "user-1");
const compatId = toWebSessionId(codeSession.id);
const eventsRes = await app.request(`/web/sessions/${compatId}/events?uuid=user-1`);
expect(eventsRes.status).toBe(200);
expect(eventsRes.headers.get("Content-Type")).toBe("text/event-stream");
const reader = eventsRes.body?.getReader();
if (reader) {
const { value } = await reader.read();
const text = new TextDecoder().decode(value!);
expect(text).toContain(": keepalive");
reader.cancel();
}
});
test("GET /web/sessions/:id/events — 403 for non-owner", async () => {
const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await createRes.json();
const eventsRes = await app.request(`/web/sessions/${id}/events?uuid=user-2`);
expect(eventsRes.status).toBe(403);
});
test("GET /web/sessions/:id/events — 409 for archived session", async () => {
const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await createRes.json();
await app.request(`/v1/sessions/${id}/archive`, {
method: "POST",
headers: AUTH_HEADERS,
});
const res = await app.request(`/web/sessions/${id}/events?uuid=user-1`);
expect(res.status).toBe(409);
const body = await res.json();
expect(body.error.type).toBe("session_closed");
});
});
describe("Web Control Routes", () => {
let app: Hono;
let sessionId: string;
beforeEach(async () => {
storeReset();
for (const [key] of getAllEventBuses()) {
removeEventBus(key);
}
app = createApp();
// Create and bind session
const createRes = await app.request("/web/sessions?uuid=user-1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
sessionId = (await createRes.json()).id;
});
test("POST /web/sessions/:id/events — sends user message", async () => {
const res = await app.request(`/web/sessions/${sessionId}/events?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "user", content: "hello" }),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.status).toBe("ok");
expect(body.event).toBeTruthy();
});
test("POST /web/sessions/:id/events/control/interrupt — supports compat code session IDs", async () => {
const rawSessionId = storeCreateSession({ idPrefix: "cse_" }).id;
storeBindSession(rawSessionId, "user-1");
const compatId = toWebSessionId(rawSessionId);
const eventsRes = await app.request(`/web/sessions/${compatId}/events?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "user", content: "hello" }),
});
expect(eventsRes.status).toBe(200);
const controlRes = await app.request(`/web/sessions/${compatId}/control?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "permission_response", approved: true, request_id: "r1" }),
});
expect(controlRes.status).toBe(200);
const interruptRes = await app.request(`/web/sessions/${compatId}/interrupt?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
});
expect(interruptRes.status).toBe(200);
});
test("POST /web/sessions/:id/events — 403 for non-owner", async () => {
const res = await app.request(`/web/sessions/${sessionId}/events?uuid=user-2`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "user", content: "hello" }),
});
expect(res.status).toBe(403);
});
test("POST /web/sessions/:id/control — sends control request", async () => {
const res = await app.request(`/web/sessions/${sessionId}/control?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "permission_response", approved: true, request_id: "r1" }),
});
expect(res.status).toBe(200);
});
test("POST /web/sessions/:id/interrupt — interrupts session", async () => {
const res = await app.request(`/web/sessions/${sessionId}/interrupt?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
});
expect(res.status).toBe(200);
});
test("POST /web/sessions/:id/interrupt — 403 for non-owner", async () => {
const res = await app.request(`/web/sessions/${sessionId}/interrupt?uuid=user-2`, {
method: "POST",
headers: { "Content-Type": "application/json" },
});
expect(res.status).toBe(403);
});
test("POST /web/sessions/:id/control — 403 for non-owner", async () => {
const res = await app.request(`/web/sessions/${sessionId}/control?uuid=user-2`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "permission_response", approved: true }),
});
expect(res.status).toBe(403);
});
test("POST /web/sessions/:id/events — 403 for non-existent session with no ownership", async () => {
const res = await app.request("/web/sessions/nonexistent/events?uuid=user-1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "user", content: "hello" }),
});
expect(res.status).toBe(403);
});
test("POST /web/sessions/:id/events/control/interrupt — 409 for archived session", async () => {
await app.request(`/v1/sessions/${sessionId}/archive`, {
method: "POST",
headers: AUTH_HEADERS,
});
const eventsRes = await app.request(`/web/sessions/${sessionId}/events?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "user", content: "hello" }),
});
expect(eventsRes.status).toBe(409);
const controlRes = await app.request(`/web/sessions/${sessionId}/control?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ type: "permission_response", approved: true, request_id: "r1" }),
});
expect(controlRes.status).toBe(409);
const interruptRes = await app.request(`/web/sessions/${sessionId}/interrupt?uuid=user-1`, {
method: "POST",
headers: { "Content-Type": "application/json" },
});
expect(interruptRes.status).toBe(409);
});
});
describe("Web Environment Routes", () => {
let app: Hono;
beforeEach(() => {
storeReset();
app = createApp();
});
test("GET /web/environments — lists active environments", async () => {
// Register an env via v1
await app.request("/v1/environments/bridge", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ machine_name: "mac1" }),
});
const res = await app.request("/web/environments?uuid=user-1");
expect(res.status).toBe(200);
const envs = await res.json();
expect(envs).toHaveLength(1);
expect(envs[0].machine_name).toBe("mac1");
});
test("GET /web/environments — requires UUID", async () => {
const res = await app.request("/web/environments");
expect(res.status).toBe(401);
});
});
describe("V1 Session Ingress Routes (HTTP)", () => {
let app: Hono;
beforeEach(() => {
storeReset();
for (const [key] of getAllEventBuses()) {
removeEventBus(key);
}
process.env.RCS_API_KEYS = "test-api-key";
app = createApp();
});
test("POST /v2/session_ingress/session/:sessionId/events — ingests events with API key", async () => {
// Create session first
const sessRes = await app.request("/v1/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await sessRes.json();
const res = await app.request(`/v2/session_ingress/session/${id}/events`, {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ events: [{ type: "assistant", content: "response" }] }),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.status).toBe("ok");
});
test("POST /v2/session_ingress/session/:sessionId/events — rejects without auth", async () => {
const res = await app.request("/v2/session_ingress/session/nope/events", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ events: [] }),
});
expect(res.status).toBe(401);
});
test("POST /v2/session_ingress/session/:sessionId/events — 404 for unknown session", async () => {
const res = await app.request("/v2/session_ingress/session/nope/events", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ events: [{ type: "user", content: "hi" }] }),
});
expect(res.status).toBe(404);
});
test("POST /v2/session_ingress/session/:sessionId/events — resolves compat code session IDs", async () => {
const sessRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const {
session: { id },
} = await sessRes.json();
const compatId = toWebSessionId(id);
const res = await app.request(`/v2/session_ingress/session/${compatId}/events`, {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ events: [{ type: "assistant", message: { role: "assistant", content: "compat ok" } }] }),
});
expect(res.status).toBe(200);
const events = getEventBus(id).getEventsSince(0);
expect(events).toHaveLength(1);
expect(events[0]?.type).toBe("assistant");
});
test("GET /v2/session_ingress/ws/:sessionId — resolves compat code session IDs", async () => {
const sessRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const {
session: { id },
} = await sessRes.json();
const compatId = toWebSessionId(id);
publishSessionEvent(id, "user", { content: "compat ws replay" }, "outbound");
const server = Bun.serve({
port: 0,
fetch: app.fetch,
websocket: {
...sessionIngressWebsocket,
idleTimeout: 30,
},
});
try {
const message = await new Promise<string>((resolve, reject) => {
const ws = new WebSocket(`ws://127.0.0.1:${server.port}/v2/session_ingress/ws/${compatId}?token=test-api-key`);
const timeout = setTimeout(() => {
ws.close();
reject(new Error("Timed out waiting for compat WebSocket replay"));
}, 2000);
ws.onmessage = (event) => {
const data = typeof event.data === "string" ? event.data : String(event.data);
if (data.includes("\"type\":\"user\"")) {
clearTimeout(timeout);
ws.close();
resolve(data);
}
};
ws.onerror = () => {
clearTimeout(timeout);
reject(new Error("Compat WebSocket connection failed"));
};
});
expect(message).toContain("\"type\":\"user\"");
expect(message).toContain(`"session_id":"${id}"`);
expect(message).toContain("compat ws replay");
} finally {
await server.stop(true);
}
});
});
describe("V2 Worker Events Routes", () => {
let app: Hono;
beforeEach(() => {
storeReset();
for (const [key] of getAllEventBuses()) {
removeEventBus(key);
}
process.env.RCS_API_KEYS = "test-api-key";
app = createApp();
});
test("POST /v1/code/sessions/:id/worker/events — publishes worker events", async () => {
// Create session
const sessRes = await app.request("/v1/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await sessRes.json();
const res = await app.request(`/v1/code/sessions/${id}/worker/events`, {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify([{ type: "assistant", content: "response" }]),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.status).toBe("ok");
expect(body.count).toBe(1);
});
test("POST /v1/code/sessions/:id/worker/events — unwraps CCR batch payloads", async () => {
const sessRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { session: { id } } = await sessRes.json();
const res = await app.request(`/v1/code/sessions/${id}/worker/events`, {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({
worker_epoch: 1,
events: [{ payload: { type: "assistant", content: "response" } }],
}),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.count).toBe(1);
const events = getEventBus(id).getEventsSince(0);
expect(events).toHaveLength(1);
expect(events[0]?.type).toBe("assistant");
expect((events[0]?.payload as { content?: string }).content).toBe("response");
});
test("GET/PUT /v1/code/sessions/:id/worker — stores worker state", async () => {
const sessRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { session: { id } } = await sessRes.json();
const putRes = await app.request(`/v1/code/sessions/${id}/worker`, {
method: "PUT",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({
worker_epoch: 1,
worker_status: "running",
external_metadata: { permission_mode: "default" },
}),
});
expect(putRes.status).toBe(200);
const getRes = await app.request(`/v1/code/sessions/${id}/worker`, {
headers: AUTH_HEADERS,
});
expect(getRes.status).toBe(200);
const body = await getRes.json();
expect(body.worker.worker_status).toBe("running");
expect(body.worker.external_metadata.permission_mode).toBe("default");
});
test("POST /v1/code/sessions/:id/worker/heartbeat — updates heartbeat", async () => {
const sessRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { session: { id } } = await sessRes.json();
const heartbeatRes = await app.request(`/v1/code/sessions/${id}/worker/heartbeat`, {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ worker_epoch: 1 }),
});
expect(heartbeatRes.status).toBe(200);
const getRes = await app.request(`/v1/code/sessions/${id}/worker`, {
headers: AUTH_HEADERS,
});
const body = await getRes.json();
expect(body.worker.last_heartbeat_at).toBeTruthy();
});
test("GET /v1/code/sessions/:id/worker/events/stream — emits CCR client_event frames", async () => {
const sessRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { session: { id } } = await sessRes.json();
const streamRes = await app.request(`/v1/code/sessions/${id}/worker/events/stream`, {
headers: AUTH_HEADERS,
});
expect(streamRes.status).toBe(200);
const reader = streamRes.body?.getReader();
expect(reader).toBeTruthy();
if (!reader) return;
const firstChunk = await reader.read();
const keepalive = new TextDecoder().decode(firstChunk.value!);
expect(keepalive).toContain(": keepalive");
publishSessionEvent(id, "user", { type: "user", content: "hello" }, "outbound");
const secondChunk = await reader.read();
const frame = new TextDecoder().decode(secondChunk.value!);
expect(frame).toContain("event: client_event");
expect(frame).toContain("\"payload\":{\"type\":\"user\",\"content\":\"hello\",\"message\":{\"content\":\"hello\"}}");
reader.cancel();
});
test("PUT /v1/code/sessions/:id/worker/state — updates session status", async () => {
const sessRes = await app.request("/v1/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await sessRes.json();
const res = await app.request(`/v1/code/sessions/${id}/worker/state`, {
method: "PUT",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ status: "running" }),
});
expect(res.status).toBe(200);
});
test("PUT /v1/code/sessions/:id/worker/external_metadata — no-op", async () => {
const sessRes = await app.request("/v1/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await sessRes.json();
const res = await app.request(`/v1/code/sessions/${id}/worker/external_metadata`, {
method: "PUT",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ meta: "data" }),
});
expect(res.status).toBe(200);
});
test("POST /v1/code/sessions/:id/worker/events/:eventId/delivery — no-op", async () => {
const sessRes = await app.request("/v1/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { id } = await sessRes.json();
const res = await app.request(`/v1/code/sessions/${id}/worker/events/evt123/delivery`, {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ status: "received" }),
});
expect(res.status).toBe(200);
});
test("POST /v1/code/sessions/:id/worker/events/delivery — batch no-op", async () => {
const sessRes = await app.request("/v1/code/sessions", {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const { session: { id } } = await sessRes.json();
const res = await app.request(`/v1/code/sessions/${id}/worker/events/delivery`, {
method: "POST",
headers: { ...AUTH_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ worker_epoch: 1, updates: [{ event_id: "evt123", status: "received" }] }),
});
expect(res.status).toBe(200);
});
});