import { describe, expect, it } from "vitest"; import { z } from "zod/v4"; import { defineApp, defineFileType, defineTool } from "../define/define"; import { ui } from "../tree/tree"; import { createColineAppHandler } from "./app-server"; import { buildSignedProtocolRequest, type ColineProtocolEnvelope, } from "./protocol"; import { RENDER_SURFACE_KIND, type RenderSurfacePayload } from "./render-surface"; import { TOOL_INVOKE_KIND, type ToolInvokePayload } from "./tool-invoke"; const SECRET = "delivery-secret-of-sufficient-length"; const createContact = defineTool({ name: "crm.create_contact", description: "Create a contact.", input: z.object({ name: z.string() }), effect: "write", execute: async (input, context) => { const file = await context.coline.files.create({ typeKey: "crm.contact", name: input.name, }); return { output: { fileId: file.fileId }, card: ui.stack([ui.text(`Created ${input.name}`)]), }; }, }); const badCardTool = defineTool({ name: "crm.bad_card", description: "Returns a disallowed card.", input: z.object({}), effect: "read", execute: () => ({ card: ui.input({ name: "nope" }), }), }); const app = defineApp({ key: "crm", name: "CRM", permissions: ["files.read", "files.write", "ai.tools"], tools: [createContact, badCardTool], files: [ defineFileType({ metadata: { typeKey: "crm.contact", name: "Contact", indexable: false, surfaces: { preview: { tier: "tree" }, inline: { tier: "tree" } }, }, runtime: { renderPreview: (context) => ui.stack([ui.heading(String(context.document.name ?? "Contact"), { level: 3 })]), renderInline: (context) => ui.text(String(context.document.name ?? "Contact")), }, }), ], }); function buildEnvelope(payload: ToolInvokePayload): ColineProtocolEnvelope { return { protocol: 2, deliveryId: "dlv_0123456789", sentAt: new Date().toISOString(), workspace: { workspaceId: "ws_1", workspaceSlug: "acme" }, app: { appKey: "crm", installId: "inst_1" }, actor: { kind: "user", userId: "user_1" }, kind: TOOL_INVOKE_KIND, payload, }; } async function send(envelope: ColineProtocolEnvelope, options?: { secret?: string }) { const signed = await buildSignedProtocolRequest({ secret: options?.secret ?? SECRET, envelope, }); const handler = createColineAppHandler({ app, secret: SECRET }); const response = await handler( new Request("https://crm.example.com/coline/app", { method: "POST", headers: signed.headers, body: signed.body, }), ); return { status: response.status, body: (await response.json()) as Record }; } async function invoke(payload: ToolInvokePayload, options?: { secret?: string }) { return send(buildEnvelope(payload), options); } const capabilities = { url: "https://coline.test/api/v1/apps/capability", token: "tok_0123456789abcdef" }; describe("createColineAppHandler", () => { it("executes a tool with capability-backed context and returns output + card", async () => { const originalFetch = globalThis.fetch; globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { const body = JSON.parse(String(init?.body)) as { operation: string; args: unknown[] }; expect(String(input)).toBe(capabilities.url); expect(body.operation).toBe("files.create"); expect((init?.headers as Record).authorization).toBe( `Bearer ${capabilities.token}`, ); return new Response( JSON.stringify({ data: { fileId: "file_9", name: "Sarah", typeKey: "crm.contact" } }), { headers: { "content-type": "application/json" } }, ); }) as typeof fetch; try { const { status, body } = await invoke({ tool: "crm.create_contact", input: { name: "Sarah" }, capabilities, }); expect(status).toBe(200); const result = body.result as { output: { fileId: string }; card: unknown }; expect(result.output.fileId).toBe("file_9"); expect(result.card).toBeDefined(); } finally { globalThis.fetch = originalFetch; } }); it("rejects bad signatures", async () => { const { status, body } = await invoke( { tool: "crm.create_contact", input: { name: "Sarah" }, capabilities }, { secret: "the-wrong-secret-of-sufficient-length" }, ); expect(status).toBe(401); expect((body.error as { code: string }).code).toBe("invalid_signature"); }); it("rejects unknown tools", async () => { const { status, body } = await invoke({ tool: "crm.nonexistent", input: {}, capabilities, }); expect(status).toBe(404); expect((body.error as { code: string }).code).toBe("unknown_tool"); }); it("rejects invalid input with issue detail", async () => { const { status, body } = await invoke({ tool: "crm.create_contact", input: { name: 42 }, capabilities, }); expect(status).toBe(400); expect((body.error as { code: string }).code).toBe("invalid_input"); }); it("rejects cards outside the chat subset", async () => { const { status, body } = await invoke({ tool: "crm.bad_card", input: {}, capabilities, }); expect(status).toBe(400); expect((body.error as { code: string }).code).toBe("invalid_card"); }); }); describe("render.surface", () => { function renderEnvelope(payload: RenderSurfacePayload): ColineProtocolEnvelope { return { protocol: 2, deliveryId: "dlv_render_00001", sentAt: new Date().toISOString(), workspace: { workspaceId: "ws_1", workspaceSlug: "acme" }, app: { appKey: "crm", installId: "inst_1" }, actor: { kind: "user", userId: "user_1" }, kind: RENDER_SURFACE_KIND, payload, }; } const file = { fileId: "file_1", name: "Sarah", typeKey: "crm.contact", version: 3 }; it("renders a file preview surface", async () => { const { status, body } = await send( renderEnvelope({ surface: "preview", file, document: { name: "Sarah Chen" }, capabilities, }), ); expect(status).toBe(200); const tree = (body.result as { tree: { type: string } }).tree; expect(tree.type).toBe("Stack"); }); it("renders an inline surface within the chat-card subset", async () => { const { status, body } = await send( renderEnvelope({ surface: "inline", file, document: { name: "Sarah Chen" }, capabilities, }), ); expect(status).toBe(200); expect((body.result as { tree: { type: string } }).tree.type).toBe("Text"); }); it("404s surfaces without handlers", async () => { const { status, body } = await send( renderEnvelope({ surface: "editor", file, capabilities }), ); expect(status).toBe(404); expect((body.error as { code: string }).code).toBe("unknown_surface"); }); it("404s home when the app has no tree home", async () => { const { status } = await send(renderEnvelope({ surface: "home", capabilities })); expect(status).toBe(404); }); it("rejects unknown envelope kinds", async () => { const envelope = { ...renderEnvelope({ surface: "home", capabilities }), kind: "something.else", }; const { status, body } = await send(envelope); expect(status).toBe(400); expect((body.error as { code: string }).code).toBe("unsupported_kind"); }); });