Files
claude-code/packages/remote-control-server/src/__tests__/services.test.ts
2026-04-17 16:21:27 +08:00

425 lines
13 KiB
TypeScript

import { describe, test, expect, beforeEach, mock } from "bun:test";
// Mock config before importing modules
const mockConfig = {
port: 3000,
host: "0.0.0.0",
apiKeys: ["test-api-key"],
baseUrl: "http://localhost:3000",
pollTimeout: 8,
heartbeatInterval: 20,
jwtExpiresIn: 3600,
disconnectTimeout: 300,
};
mock.module("../config", () => ({
config: mockConfig,
getBaseUrl: () => "http://localhost:3000",
}));
import { storeReset, storeCreateEnvironment } from "../store";
import {
createSession,
createCodeSession,
getSession,
updateSessionTitle,
updateSessionStatus,
archiveSession,
incrementEpoch,
listSessions,
listSessionSummaries,
listSessionSummariesByUsername,
listSessionsByEnvironment,
} from "../services/session";
import {
registerEnvironment,
deregisterEnvironment,
getEnvironment,
updatePollTime,
listActiveEnvironments,
listActiveEnvironmentsResponse,
listActiveEnvironmentsByUsername,
reconnectEnvironment,
} from "../services/environment";
import { normalizePayload, publishSessionEvent } from "../services/transport";
import { getEventBus, removeEventBus, getAllEventBuses } from "../transport/event-bus";
// ---------- Session Service ----------
describe("Session Service", () => {
beforeEach(() => {
storeReset();
for (const [key] of getAllEventBuses()) {
removeEventBus(key);
}
});
describe("createSession", () => {
test("creates a session with defaults", () => {
const resp = createSession({});
expect(resp.id).toMatch(/^session_/);
expect(resp.status).toBe("idle");
expect(resp.source).toBe("remote-control");
expect(resp.environment_id).toBeNull();
expect(resp.worker_epoch).toBe(0);
expect(resp.created_at).toBeGreaterThan(0);
});
test("creates a session with all options", () => {
const env = storeCreateEnvironment({ secret: "s" });
const resp = createSession({
environment_id: env.id,
title: "My Session",
source: "cli",
permission_mode: "auto",
});
expect(resp.environment_id).toBe(env.id);
expect(resp.title).toBe("My Session");
expect(resp.source).toBe("cli");
expect(resp.permission_mode).toBe("auto");
});
test("creates session with username", () => {
const resp = createSession({ username: "alice" });
expect(resp.username).toBe("alice");
});
});
describe("createCodeSession", () => {
test("creates a code session with cse_ prefix", () => {
const resp = createCodeSession({});
expect(resp.id).toMatch(/^cse_/);
});
});
describe("getSession", () => {
test("returns null for non-existent session", () => {
expect(getSession("nope")).toBeNull();
});
test("returns created session", () => {
const created = createSession({});
const fetched = getSession(created.id);
expect(fetched).not.toBeNull();
expect(fetched!.id).toBe(created.id);
});
});
describe("updateSessionTitle", () => {
test("updates title", () => {
const s = createSession({});
updateSessionTitle(s.id, "New Title");
expect(getSession(s.id)?.title).toBe("New Title");
});
});
describe("updateSessionStatus", () => {
test("updates status", () => {
const s = createSession({});
updateSessionStatus(s.id, "active");
expect(getSession(s.id)?.status).toBe("active");
});
});
describe("archiveSession", () => {
test("sets status to archived and removes event bus", () => {
const s = createSession({});
// Create event bus for this session
getEventBus(s.id);
archiveSession(s.id);
expect(getSession(s.id)?.status).toBe("archived");
expect(getAllEventBuses().has(s.id)).toBe(false);
});
});
describe("incrementEpoch", () => {
test("increments epoch by 1", () => {
const s = createSession({});
expect(incrementEpoch(s.id)).toBe(1);
expect(incrementEpoch(s.id)).toBe(2);
expect(getSession(s.id)?.worker_epoch).toBe(2);
});
test("throws for non-existent session", () => {
expect(() => incrementEpoch("nope")).toThrow("Session not found");
});
});
describe("listSessions", () => {
test("returns all sessions", () => {
createSession({});
createSession({});
expect(listSessions()).toHaveLength(2);
});
});
describe("listSessionSummaries", () => {
test("returns summaries with correct fields", () => {
createSession({ title: "Test" });
const summaries = listSessionSummaries();
expect(summaries).toHaveLength(1);
expect(summaries[0].title).toBe("Test");
expect(summaries[0].updated_at).toBeGreaterThan(0);
// Summary should not have environment_id
expect("environment_id" in summaries[0]).toBe(false);
});
});
describe("listSessionSummariesByUsername", () => {
test("filters by username", () => {
createSession({ username: "alice" });
createSession({ username: "bob" });
expect(listSessionSummariesByUsername("alice")).toHaveLength(1);
});
});
describe("listSessionsByEnvironment", () => {
test("filters by environment", () => {
const env = storeCreateEnvironment({ secret: "s" });
createSession({ environment_id: env.id });
createSession({});
expect(listSessionsByEnvironment(env.id)).toHaveLength(1);
});
});
});
// ---------- Environment Service ----------
describe("Environment Service", () => {
beforeEach(() => {
storeReset();
});
describe("registerEnvironment", () => {
test("registers environment with defaults", () => {
const result = registerEnvironment({});
expect(result.environment_id).toMatch(/^env_/);
expect(result.environment_secret).toBe("test-api-key");
expect(result.status).toBe("active");
});
test("registers with options", () => {
const result = registerEnvironment({
machine_name: "mac1",
directory: "/home/user",
branch: "main",
git_repo_url: "https://github.com/test/repo",
max_sessions: 5,
worker_type: "custom",
});
const env = getEnvironment(result.environment_id);
expect(env?.machineName).toBe("mac1");
expect(env?.directory).toBe("/home/user");
expect(env?.maxSessions).toBe(5);
});
test("registers with username", () => {
const result = registerEnvironment({ username: "alice" });
const env = getEnvironment(result.environment_id);
expect(env?.username).toBe("alice");
});
});
describe("deregisterEnvironment", () => {
test("sets status to deregistered", () => {
const result = registerEnvironment({});
deregisterEnvironment(result.environment_id);
const env = getEnvironment(result.environment_id);
expect(env?.status).toBe("deregistered");
});
});
describe("updatePollTime", () => {
test("updates lastPollAt", () => {
const result = registerEnvironment({});
const before = getEnvironment(result.environment_id)?.lastPollAt;
// Small delay to ensure time difference
updatePollTime(result.environment_id);
const after = getEnvironment(result.environment_id)?.lastPollAt;
expect(after!.getTime()).toBeGreaterThanOrEqual(before!.getTime());
});
});
describe("listActiveEnvironments", () => {
test("returns active environments", () => {
registerEnvironment({});
registerEnvironment({});
expect(listActiveEnvironments()).toHaveLength(2);
});
});
describe("listActiveEnvironmentsResponse", () => {
test("returns response format", () => {
registerEnvironment({ machine_name: "mac1" });
const envs = listActiveEnvironmentsResponse();
expect(envs).toHaveLength(1);
expect(envs[0].machine_name).toBe("mac1");
expect(envs[0].last_poll_at).toBeGreaterThan(0);
});
});
describe("listActiveEnvironmentsByUsername", () => {
test("filters by username", () => {
registerEnvironment({ username: "alice" });
registerEnvironment({ username: "bob" });
expect(listActiveEnvironmentsByUsername("alice")).toHaveLength(1);
});
});
describe("reconnectEnvironment", () => {
test("sets status back to active", () => {
const result = registerEnvironment({});
deregisterEnvironment(result.environment_id);
expect(getEnvironment(result.environment_id)?.status).toBe("deregistered");
reconnectEnvironment(result.environment_id);
expect(getEnvironment(result.environment_id)?.status).toBe("active");
});
});
});
// ---------- Transport Service ----------
describe("Transport Service", () => {
beforeEach(() => {
storeReset();
for (const [key] of getAllEventBuses()) {
removeEventBus(key);
}
});
describe("normalizePayload", () => {
test("handles string payload", () => {
const result = normalizePayload("user", "hello world");
expect(result.content).toBe("hello world");
expect(result.raw).toBe("hello world");
});
test("handles null payload", () => {
const result = normalizePayload("user", null);
expect(result.content).toBe("");
expect(result.raw).toBeNull();
});
test("handles object with direct content", () => {
const result = normalizePayload("user", { content: "direct text" });
expect(result.content).toBe("direct text");
});
test("handles object with message.content string", () => {
const result = normalizePayload("assistant", { message: { role: "assistant", content: "reply" } });
expect(result.content).toBe("reply");
});
test("handles object with message.content array", () => {
const result = normalizePayload("assistant", {
message: {
content: [
{ type: "text", text: "hello " },
{ type: "text", text: "world" },
],
},
});
expect(result.content).toBe("hello world");
});
test("preserves tool fields", () => {
const result = normalizePayload("tool_use", { tool_name: "Bash", tool_input: { cmd: "ls" } });
expect(result.tool_name).toBe("Bash");
expect(result.tool_input).toEqual({ cmd: "ls" });
});
test("preserves permission fields", () => {
const result = normalizePayload("permission", {
request_id: "req_1",
approved: true,
updated_input: { cmd: "ls -la" },
});
expect(result.request_id).toBe("req_1");
expect(result.approved).toBe(true);
expect(result.updated_input).toEqual({ cmd: "ls -la" });
});
test("preserves message field", () => {
const msg = { role: "user", content: "hi" };
const result = normalizePayload("user", { message: msg });
expect(result.message).toEqual(msg);
});
test("preserves uuid field", () => {
const result = normalizePayload("user", {
uuid: "msg_123",
content: "hi",
});
expect(result.uuid).toBe("msg_123");
});
test("preserves isSynthetic field", () => {
const result = normalizePayload("user", {
content: "scheduled job: refresh analytics cache",
isSynthetic: true,
});
expect(result.isSynthetic).toBe(true);
});
test("uses name as tool_name fallback", () => {
const result = normalizePayload("tool", { name: "Read" });
expect(result.tool_name).toBe("Read");
});
test("uses input as tool_input fallback", () => {
const result = normalizePayload("tool", { input: { path: "/tmp" } });
expect(result.tool_input).toEqual({ path: "/tmp" });
});
test("handles empty content array", () => {
const result = normalizePayload("assistant", {
message: { content: [] },
});
expect(result.content).toBe("");
});
test("preserves task_state fields", () => {
const result = normalizePayload("task_state", {
task_list_id: "team-alpha",
tasks: [{ id: "1", subject: "Task 1", status: "pending" }],
});
expect(result.task_list_id).toBe("team-alpha");
expect(result.tasks).toEqual([
{ id: "1", subject: "Task 1", status: "pending" },
]);
});
test("preserves status metadata for conversation reset events", () => {
const result = normalizePayload("status", {
status: "conversation_cleared",
subtype: "status",
message: "conversation_cleared",
});
expect(result.status).toBe("conversation_cleared");
expect(result.subtype).toBe("status");
expect(result.message).toBe("conversation_cleared");
});
test("handles undefined payload", () => {
const result = normalizePayload("user", undefined);
expect(result.content).toBe("");
});
});
describe("publishSessionEvent", () => {
test("publishes event to session bus", () => {
const event = publishSessionEvent("s1", "user", { content: "hello" }, "outbound");
expect(event.type).toBe("user");
expect(event.direction).toBe("outbound");
expect(event.sessionId).toBe("s1");
expect(event.seqNum).toBe(1);
});
test("normalizes payload before publishing", () => {
const event = publishSessionEvent("s1", "assistant", { message: { content: "reply" } }, "inbound");
const payload = event.payload as Record<string, unknown>;
expect(payload.content).toBe("reply");
});
});
});