import { describe, expect, test } from "bun:test"; import type { AssistantEvent } from "../api/index.js"; import type { Conversation } from "../daemon/conversation.js"; import { buildCompletionSummary, createSurfaceMutex, surfaceProxyResolver, } from "../daemon/conversation-surfaces.js"; import type { ChoiceSurfaceData, CopyBlockSurfaceData, OAuthConnectSurfaceData, SurfaceType, UiSurfaceShow, } from "../daemon/message-protocol.js"; import { INTERACTIVE_SURFACE_TYPES } from "../daemon/message-protocol.js"; import { uiShowTool } from "../tools/ui-surface/definitions.js"; import { uiShowTeachingError } from "../tools/ui-surface/surface-shape-docs.js"; import { asConversation } from "./helpers/mock-conversation.js"; function makeContext(sent: AssistantEvent[] = []): Conversation { return asConversation({ conversationId: "session-1", emit: (msg) => sent.push(msg), pendingSurfaceActions: new Map(), lastSurfaceAction: new Map< string, { actionId: string; data?: Record } >(), surfaceState: new Map(), surfaceUndoStacks: new Map(), accumulatedSurfaceState: new Map>(), surfaceActionRequestIds: new Set(), currentTurnSurfaces: [], isProcessing: () => false, enqueueMessage: () => ({ queued: false, requestId: "req-1" }), getQueueDepth: () => 0, processMessage: async () => "ok", withSurface: createSurfaceMutex(), }); } function getSurfaceTypeEnum(): string[] { return ( uiShowTool.input_schema as { properties: { surface_type: { enum: string[] } }; } ).properties.surface_type.enum; } describe("choice and copy_block surface definitions", () => { test("ui_show advertises the new surface types", () => { expect(getSurfaceTypeEnum()).toContain("choice"); expect(getSurfaceTypeEnum()).toContain("copy_block"); expect(getSurfaceTypeEnum()).toContain("oauth_connect"); expect(uiShowTool.description).toContain("recommended"); expect(uiShowTool.description).toContain("visible copy button"); expect(uiShowTool.description).toContain("managed OAuth"); }); test("choice and oauth_connect are interactive but copy_block is display-only", () => { expect(INTERACTIVE_SURFACE_TYPES).toContain("choice"); expect(INTERACTIVE_SURFACE_TYPES).toContain("oauth_connect"); expect(INTERACTIVE_SURFACE_TYPES).not.toContain("copy_block"); }); }); describe("choice and copy_block surface proxying", () => { test("ui_show normalizes choice options and creates recommended action payloads", async () => { const sent: AssistantEvent[] = []; const ctx = makeContext(sent); const result = await surfaceProxyResolver(ctx, "ui_show", { surface_type: "choice", title: "Pick a next move", data: { description: "Choose where to start.", options: [ { id: "clean-inbox", title: "Clean up my inbox", description: "Triage unread mail and archive noise.", recommended: true, data: { outcome: "inbox_cleanup" }, }, { id: "plan-week", title: "Plan my week" }, { id: "", title: "Ignored" }, ], }, }); expect(result.isError).toBe(false); expect(result.yieldToUser).toBe(true); const showMessage = sent.find( (msg): msg is UiSurfaceShow => msg.type === "ui_surface_show", ); expect(showMessage).toBeDefined(); if (!showMessage || showMessage.surfaceType !== "choice") { return; } const data = showMessage.data as ChoiceSurfaceData; expect(data.options.map((option) => option.id)).toEqual([ "clean-inbox", "plan-week", ]); expect(data.options[0].recommended).toBe(true); expect(showMessage.actions?.[0]).toEqual({ id: "clean-inbox", label: "Clean up my inbox", style: "primary", data: { choiceId: "clean-inbox", choiceTitle: "Clean up my inbox", selectedIds: ["clean-inbox"], selectedTitles: ["Clean up my inbox"], choiceDescription: "Triage unread mail and archive noise.", recommended: true, outcome: "inbox_cleanup", }, }); expect(ctx.pendingSurfaceActions.get(showMessage.surfaceId)).toEqual({ surfaceType: "choice", }); }); test("ui_show rejects choice surfaces without valid options", async () => { const sent: AssistantEvent[] = []; const ctx = makeContext(sent); const result = await surfaceProxyResolver(ctx, "ui_show", { surface_type: "choice", data: { options: [{ id: "", title: "Missing id" }] }, }); expect(result.isError).toBe(true); expect(result.content).toContain( "choice surfaces require at least one option", ); expect(sent).toHaveLength(0); }); test("ui_show passes copy_block data through without awaiting action", async () => { const sent: AssistantEvent[] = []; const ctx = makeContext(sent); const result = await surfaceProxyResolver(ctx, "ui_show", { surface_type: "copy_block", data: { text: "Paste this prompt into another assistant.", label: "Port prompt", language: "text", }, }); expect(result.isError).toBe(false); expect(result.yieldToUser).toBeUndefined(); const showMessage = sent.find( (msg): msg is UiSurfaceShow => msg.type === "ui_surface_show", ); expect(showMessage).toBeDefined(); if (!showMessage || showMessage.surfaceType !== "copy_block") { return; } expect(showMessage.data as CopyBlockSurfaceData).toEqual({ text: "Paste this prompt into another assistant.", label: "Port prompt", language: "text", }); expect(ctx.pendingSurfaceActions.has(showMessage.surfaceId)).toBe(false); }); test("ui_show passes oauth_connect data through and awaits action", async () => { const sent: AssistantEvent[] = []; const ctx = makeContext(sent); const result = await surfaceProxyResolver(ctx, "ui_show", { surface_type: "oauth_connect", title: "Connect Google", data: { providerKey: "google", displayName: "Google", description: "Connect Gmail for this task.", connectLabel: "Connect Google Account", requestedScopes: ["gmail.readonly"], }, }); expect(result.isError).toBe(false); expect(result.yieldToUser).toBe(true); const showMessage = sent.find( (msg): msg is UiSurfaceShow => msg.type === "ui_surface_show", ); expect(showMessage).toBeDefined(); if (!showMessage || showMessage.surfaceType !== "oauth_connect") { return; } expect(showMessage.data as OAuthConnectSurfaceData).toEqual({ providerKey: "google", displayName: "Google", description: "Connect Gmail for this task.", requestedScopes: ["gmail.readonly"], }); expect(ctx.pendingSurfaceActions.get(showMessage.surfaceId)).toEqual({ surfaceType: "oauth_connect", }); }); test("ui_show rejects oauth_connect without providerKey", async () => { const sent: AssistantEvent[] = []; const ctx = makeContext(sent); const result = await surfaceProxyResolver(ctx, "ui_show", { surface_type: "oauth_connect", data: { displayName: "Google" }, }); expect(result.isError).toBe(true); expect(result.content).toContain("data.providerKey"); expect(sent).toHaveLength(0); }); test("ui_show recovers copy_block data sent as a JSON-encoded string", async () => { const sent: AssistantEvent[] = []; const ctx = makeContext(sent); const result = await surfaceProxyResolver(ctx, "ui_show", { surface_type: "copy_block", data: JSON.stringify({ text: "bunx vellum doctor", label: "Command" }), }); expect(result.isError).toBe(false); const showMessage = sent.find( (msg): msg is UiSurfaceShow => msg.type === "ui_surface_show", ); expect(showMessage).toBeDefined(); if (!showMessage || showMessage.surfaceType !== "copy_block") { return; } expect(showMessage.data).toEqual({ text: "bunx vellum doctor", label: "Command", }); }); test("ui_show recovers copy_block fields placed at the top level of the input", async () => { const sent: AssistantEvent[] = []; const ctx = makeContext(sent); const result = await surfaceProxyResolver(ctx, "ui_show", { surface_type: "copy_block", text: "ssh user@example.com", label: "SSH", data: {}, }); expect(result.isError).toBe(false); const showMessage = sent.find( (msg): msg is UiSurfaceShow => msg.type === "ui_surface_show", ); expect(showMessage).toBeDefined(); if (!showMessage || showMessage.surfaceType !== "copy_block") { return; } expect(showMessage.data).toEqual({ text: "ssh user@example.com", label: "SSH", }); }); test("ui_show rejects an unknown surface_type with the valid values", async () => { const sent: AssistantEvent[] = []; const ctx = makeContext(sent); const result = await surfaceProxyResolver(ctx, "ui_show", { surface_type: "copyblock", data: { text: "hello" }, }); expect(result.isError).toBe(true); expect(result.content).toContain('"copyblock" is not a valid surface_type'); expect(result.content).toContain("copy_block"); expect(sent).toHaveLength(0); }); test("ui_show rejects daemon-internal surface types and omits them from the valid list", async () => { for (const internalType of ["skill_card", "call_summary"]) { const sent: AssistantEvent[] = []; const ctx = makeContext(sent); const result = await surfaceProxyResolver(ctx, "ui_show", { surface_type: internalType, data: { anything: true }, }); expect(result.isError).toBe(true); expect(result.content).toContain( `"${internalType}" is not a valid surface_type`, ); // The advertised valid list must never enumerate the internal types // (the leading echo of the rejected input naming it is expected). const validList = typeof result.content === "string" ? (result.content.split("Valid surface_type values:")[1] ?? "") : ""; expect(validList).not.toContain("skill_card"); expect(validList).not.toContain("call_summary"); expect(sent).toHaveLength(0); } }); test("ui_show teaching guard accepts copy_block text at the top level of the input", () => { const teachingError = uiShowTeachingError({ surface_type: "copy_block", text: "bunx vellum doctor", data: {}, }); expect(teachingError).toBeNull(); }); test("uiShowTool.execute renders a copy_block whose text is at the top level", async () => { const sent: AssistantEvent[] = []; const ctx = makeContext(sent); const result = await uiShowTool.execute( { surface_type: "copy_block", text: "bunx vellum doctor", data: {} }, { proxyToolResolver: (toolName: string, input: Record) => surfaceProxyResolver(ctx, toolName, input), } as unknown as Parameters[1], ); expect(result.isError).toBe(false); const showMessage = sent.find( (msg): msg is UiSurfaceShow => msg.type === "ui_surface_show", ); expect(showMessage).toBeDefined(); if (!showMessage || showMessage.surfaceType !== "copy_block") { return; } expect(showMessage.data).toEqual({ text: "bunx vellum doctor" }); }); test("uiShowTool.execute renders a dynamic_page whose html is at the top level", async () => { const sent: AssistantEvent[] = []; const ctx = makeContext(sent); const result = await uiShowTool.execute( { surface_type: "dynamic_page", html: "

hello

", data: {} }, { proxyToolResolver: (toolName: string, input: Record) => surfaceProxyResolver(ctx, toolName, input), } as unknown as Parameters[1], ); expect(result.isError).toBe(false); const showMessage = sent.find( (msg): msg is UiSurfaceShow => msg.type === "ui_surface_show", ); expect(showMessage).toBeDefined(); if (!showMessage || showMessage.surfaceType !== "dynamic_page") { return; } expect(showMessage.data.html).toBe("

hello

"); }); test("ui_show teaching guard accepts JSON-string copy_block data with text", () => { const teachingError = uiShowTeachingError({ surface_type: "copy_block", data: JSON.stringify({ text: "populated" }), }); expect(teachingError).toBeNull(); }); test("ui_show teaching guard still flags copy_block without text", () => { const teachingError = uiShowTeachingError({ surface_type: "copy_block", data: { label: "Command" }, }); expect(teachingError).toContain("`data.text` must be a non-empty string"); }); test("choice completion summary names multi-select choices", () => { expect( buildCompletionSummary("choice", "submit", { selectedIds: ["a", "b"], selectedTitles: ["Clean up my inbox", "Plan my week"], }), ).toBe('User chose 2 options: "Clean up my inbox", "Plan my week"'); }); test("oauth_connect completion summary names the connected account", () => { expect( buildCompletionSummary("oauth_connect", "connect", { providerKey: "google", providerLabel: "Google", accountLabel: "user@example.com", status: "connected", }), ).toBe("Connected Google: user@example.com"); }); });