import { describe, test, expect } from "bun:test"; import { toClientPayload } from "../transport/client-payload"; import type { SessionEvent } from "../transport/event-bus"; function makeEvent(overrides: Partial & Pick): SessionEvent { return { id: "evt-1", payload: null, direction: "inbound", seqNum: 1, createdAt: Date.now(), ...overrides, }; } // ============================================================================= // user / user_message // ============================================================================= describe("toClientPayload — user message", () => { test("maps user type with content", () => { const event = makeEvent({ type: "user", sessionId: "sess-1", payload: { content: "hello" }, }); const result = toClientPayload(event); expect(result.type).toBe("user"); expect(result.session_id).toBe("sess-1"); expect((result as any).message.role).toBe("user"); expect((result as any).message.content).toBe("hello"); }); test("maps user_message type same as user", () => { const event = makeEvent({ type: "user_message", sessionId: "sess-2", payload: { content: "world" }, }); const result = toClientPayload(event); expect(result.type).toBe("user"); expect(result.session_id).toBe("sess-2"); }); test("falls back to message field when content is missing", () => { const event = makeEvent({ type: "user", sessionId: "sess-3", payload: { message: "fallback msg" }, }); const result = toClientPayload(event); expect((result as any).message.content).toBe("fallback msg"); }); test("falls back to empty string when both content and message missing", () => { const event = makeEvent({ type: "user", sessionId: "sess-4", payload: {}, }); const result = toClientPayload(event); expect((result as any).message.content).toBe(""); }); test("includes isSynthetic when true", () => { const event = makeEvent({ type: "user", sessionId: "sess-5", payload: { content: "auto", isSynthetic: true }, }); const result = toClientPayload(event); expect((result as any).isSynthetic).toBe(true); }); test("does not include isSynthetic when false", () => { const event = makeEvent({ type: "user", sessionId: "sess-6", payload: { content: "manual", isSynthetic: false }, }); const result = toClientPayload(event); expect((result as any).isSynthetic).toBeUndefined(); }); test("uses payload.uuid when present", () => { const event = makeEvent({ type: "user", sessionId: "sess-7", payload: { content: "hi", uuid: "custom-uuid" }, }); const result = toClientPayload(event); expect(result.uuid).toBe("custom-uuid"); }); test("falls back to event.id when payload.uuid is missing", () => { const event = makeEvent({ type: "user", sessionId: "sess-8", payload: { content: "hi" }, }); const result = toClientPayload(event); expect(result.uuid).toBe("evt-1"); }); }); // ============================================================================= // permission_response / control_response // ============================================================================= describe("toClientPayload — permission response", () => { test("approved=true maps to allow behavior", () => { const event = makeEvent({ type: "permission_response", sessionId: "sess-1", payload: { approved: true, request_id: "req-1" }, }); const result = toClientPayload(event); expect(result.type).toBe("control_response"); const resp = (result as any).response; expect(resp.subtype).toBe("success"); expect(resp.request_id).toBe("req-1"); expect(resp.response.behavior).toBe("allow"); }); test("approved=false maps to deny behavior with error", () => { const event = makeEvent({ type: "permission_response", sessionId: "sess-2", payload: { approved: false, request_id: "req-2" }, }); const result = toClientPayload(event); expect(result.type).toBe("control_response"); const resp = (result as any).response; expect(resp.subtype).toBe("error"); expect(resp.error).toBe("Permission denied by user"); expect(resp.response.behavior).toBe("deny"); }); test("approved=false includes feedback message when provided", () => { const event = makeEvent({ type: "permission_response", sessionId: "sess-3", payload: { approved: false, request_id: "req-3", message: "please revise" }, }); const result = toClientPayload(event); expect((result as any).response.message).toBe("please revise"); }); test("passes through existingResponse directly", () => { const existingResponse = { subtype: "success", custom: true }; const event = makeEvent({ type: "control_response", sessionId: "sess-4", payload: { approved: true, response: existingResponse }, }); const result = toClientPayload(event); expect(result.type).toBe("control_response"); expect((result as any).response).toBe(existingResponse); }); test("includes updatedInput when approved with updated_input", () => { const updatedInput = { file_path: "/new/path" }; const event = makeEvent({ type: "permission_response", sessionId: "sess-5", payload: { approved: true, request_id: "req-5", updated_input: updatedInput }, }); const result = toClientPayload(event); expect((result as any).response.response.updatedInput).toEqual(updatedInput); }); test("includes updatedPermissions when approved with updated_permissions", () => { const perms = [{ type: "allow", tool: "bash" }]; const event = makeEvent({ type: "permission_response", sessionId: "sess-6", payload: { approved: true, request_id: "req-6", updated_permissions: perms }, }); const result = toClientPayload(event); expect((result as any).response.response.updatedPermissions).toEqual(perms); }); }); // ============================================================================= // interrupt // ============================================================================= describe("toClientPayload — interrupt", () => { test("maps interrupt to control_request with subtype interrupt", () => { const event = makeEvent({ type: "interrupt", sessionId: "sess-1", }); const result = toClientPayload(event); expect(result.type).toBe("control_request"); expect((result as any).request_id).toBe("evt-1"); expect((result as any).request.subtype).toBe("interrupt"); }); }); // ============================================================================= // control_request // ============================================================================= describe("toClientPayload — control_request", () => { test("passes through request_id and request from payload", () => { const event = makeEvent({ type: "control_request", sessionId: "sess-1", payload: { request_id: "req-99", request: { subtype: "permission", tool: "bash" } }, }); const result = toClientPayload(event); expect(result.type).toBe("control_request"); expect((result as any).request_id).toBe("req-99"); expect((result as any).request.subtype).toBe("permission"); }); test("falls back request to payload when no request field", () => { const event = makeEvent({ type: "control_request", sessionId: "sess-2", payload: { request_id: "req-10", custom: "data" }, }); const result = toClientPayload(event); expect((result as any).request).toEqual({ request_id: "req-10", custom: "data" }); }); test("falls back request_id to event.id when missing", () => { const event = makeEvent({ type: "control_request", sessionId: "sess-3", payload: { request: { subtype: "test" } }, }); const result = toClientPayload(event); expect((result as any).request_id).toBe("evt-1"); }); }); // ============================================================================= // default fallback // ============================================================================= describe("toClientPayload — default types", () => { test("passes through unknown type with type/uuid/session_id/message", () => { const event = makeEvent({ type: "assistant", sessionId: "sess-1", payload: { uuid: "u-1", content: "response text" }, }); const result = toClientPayload(event); expect(result.type).toBe("assistant"); expect(result.uuid).toBe("u-1"); expect(result.session_id).toBe("sess-1"); expect(result.message).toEqual({ uuid: "u-1", content: "response text" }); }); });