// Interactive outbound tests cover channel outbound interactive payload construction. import { describe, expect, it } from "vitest"; import { renderMessagePresentationChartFallbackText, renderMessagePresentationFallbackText, normalizeMessagePresentation, } from "../../../interactive/payload.js"; import { adaptMessagePresentationForChannel, applyPresentationActionLimits, presentationPageSize, reduceInteractiveReply, } from "../../../plugin-sdk/interactive-runtime.js"; describe("reduceInteractiveReply", () => { it("walks authored blocks in order", () => { const order = reduceInteractiveReply( { blocks: [ { type: "text", text: "first" }, { type: "buttons", buttons: [{ label: "Retry", value: "retry" }] }, { type: "select", options: [{ label: "Alpha", value: "alpha" }] }, ], }, [] as string[], (state, block) => { state.push(block.type); return state; }, ); expect(order).toEqual(["text", "buttons", "select"]); }); it("returns the initial state when interactive payload is missing", () => { expect(reduceInteractiveReply(undefined, 3, (value) => value + 1)).toBe(3); }); }); describe("presentation capability limits", () => { it("drops model-picker actions until a channel integration opts in", () => { const buttons = applyPresentationActionLimits( [ { label: "Model", action: { type: "model-picker", version: 1, snapshotToken: "snapshot_1", intent: "choose-model", providerToken: "provider_1", modelToken: "model_1", }, }, ], { buttons: true }, ); expect(buttons).toEqual([]); }); it("keeps highest-priority buttons inside action capacity", () => { const buttons = applyPresentationActionLimits( [ { label: "Low", value: "low", priority: -1 }, { label: "Default", value: "default" }, { label: "High", value: "high", priority: 10 }, { label: "Next", value: "next", priority: 5 }, ], { limits: { actions: { maxActions: 2, maxLabelLength: 4, supportsStyles: false, }, }, }, ); expect(buttons).toEqual([ { label: "High", value: "high", priority: 10 }, { label: "Next", value: "next", priority: 5 }, ]); }); it("keeps authored button order when nothing is dropped", () => { const buttons = applyPresentationActionLimits( [ { label: "First", value: "first", priority: 1 }, { label: "Second", value: "second", priority: 100 }, { label: "Third", value: "third" }, ], { limits: { actions: { maxActionsPerRow: 5, }, }, }, ); expect(buttons).toEqual([ { label: "First", value: "first", priority: 1 }, { label: "Second", value: "second", priority: 100 }, { label: "Third", value: "third" }, ]); }); it("applies callback byte limits to typed command actions", () => { const buttons = applyPresentationActionLimits( [ { label: "Keep", action: { type: "command", command: "/codex plugins menu" }, }, { label: "Drop", action: { type: "command", command: `/codex plugins enable ${"x".repeat(20)}` }, }, ], { limits: { actions: { maxValueBytes: 24, }, }, }, ); expect(buttons).toEqual([ { label: "Keep", action: { type: "command", command: "/codex plugins menu" }, }, ]); }); it("keeps typed button actions when only the legacy fallback exceeds value limits", () => { const buttons = applyPresentationActionLimits( [ { label: "Keep", value: "legacy-value-that-is-too-long", action: { type: "command", command: "/short" }, }, ], { limits: { actions: { maxValueBytes: 8, }, }, }, ); expect(buttons).toEqual([ { label: "Keep", action: { type: "command", command: "/short" }, }, ]); }); it("keeps approval and link actions out of generic callback byte limits", () => { const buttons = applyPresentationActionLimits( [ { label: "Approve", action: { type: "approval", approvalId: "approval/with/a/long/stable/id", approvalKind: "exec", decision: "allow-once", }, }, { label: "Review", action: { type: "url", url: "https://example.test/approve/a-long-id" }, }, { label: "Open app", action: { type: "web-app", url: "https://example.test/app/a-long-id" }, }, { label: "Open widget", action: { type: "web-app", widgetId: "AAAAAAAAAAAAAAAAAAAAAA" }, }, ], { limits: { actions: { maxValueBytes: 4, }, }, }, ); expect(buttons).toEqual([ { label: "Approve", action: { type: "approval", approvalId: "approval/with/a/long/stable/id", approvalKind: "exec", decision: "allow-once", }, }, { label: "Review", action: { type: "url", url: "https://example.test/approve/a-long-id" }, }, { label: "Open app", action: { type: "web-app", url: "https://example.test/app/a-long-id" }, }, { label: "Open widget", action: { type: "web-app", widgetId: "AAAAAAAAAAAAAAAAAAAAAA" }, }, ]); }); it("preserves legacy fields without letting them override canonical action semantics", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "buttons", buttons: [ { label: "Approve", action: { type: "approval", approvalId: "approval:1", approvalKind: "plugin", decision: "deny", }, value: "legacy-shadow", url: "https://ignored.example.test", }, ], }, ], }, capabilities: { limits: { actions: { maxValueBytes: 4 } }, }, }); expect(presentation.blocks).toEqual([ { type: "buttons", buttons: [ { label: "Approve", action: { type: "approval", approvalId: "approval:1", approvalKind: "plugin", decision: "deny", }, url: "https://ignored.example.test", }, ], }, ]); }); it("keeps typed select actions when only the legacy fallback exceeds value limits", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "select", options: [ { label: "Keep", value: "legacy-value-that-is-too-long", action: { type: "callback", value: "short" }, }, ], }, ], }, capabilities: { limits: { selects: { maxValueBytes: 8, }, }, }, }); expect(presentation.blocks).toEqual([ { type: "select", options: [{ label: "Keep", action: { type: "callback", value: "short" } }], }, ]); }); it("adapts button and select blocks without touching text blocks", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { title: "Deploy", blocks: [ { type: "text", text: "Ready" }, { type: "buttons", buttons: [ { label: "Approve deployment", value: "approve", style: "success", }, { label: "Reject", value: "x".repeat(12), priority: 10 }, ], }, { type: "select", placeholder: "Environment target", options: [ { label: "Canary cluster", value: "canary" }, { label: "Production cluster", value: "production" }, ], }, ], }, capabilities: { limits: { actions: { maxActions: 2, maxLabelLength: 7, maxValueBytes: 8, supportsStyles: false, supportsDisabled: false, }, selects: { maxOptions: 1, maxLabelLength: 6, maxValueBytes: 20, }, }, }, }); expect(presentation).toEqual({ title: "Deploy", blocks: [ { type: "text", text: "Ready" }, { type: "buttons", buttons: [{ label: "Approve", value: "approve" }], }, { type: "context", text: "Actions:\n- Reject" }, { type: "select", placeholder: "Enviro", options: [{ label: "Canary", value: "canary" }], }, { type: "context", text: "Environment target:\n- Production cluster" }, ], }); }); it("keeps visible fallback labels when controls exceed channel value limits", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "buttons", buttons: [ { label: "Approve deployment", value: "approve-prod" }, { label: "Rollback deployment", value: "rollback-prod" }, ], }, { type: "select", placeholder: "Environment", options: [ { label: "Canary cluster", value: "canary-target" }, { label: "Production cluster", value: "production-target" }, ], }, ], }, capabilities: { limits: { actions: { maxValueBytes: 4, maxLabelLength: 8, }, selects: { maxValueBytes: 4, maxLabelLength: 7, }, }, }, }); expect(presentation.blocks).toEqual([ { type: "context", text: "Actions:\n- Approve deployment\n- Rollback deployment" }, { type: "context", text: "Environment:\n- Canary cluster\n- Production cluster" }, ]); }); it("keeps dropped command buttons actionable without exposing private callbacks", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "buttons", buttons: [ { label: "Keep", value: "keep" }, { label: "Deploy", action: { type: "command", command: "/deploy production" } }, { label: "Approval", action: { type: "approval", approvalId: "approval:private-transport-token", approvalKind: "exec", decision: "allow-once", }, }, { label: "Opaque", action: { type: "callback", value: "private-callback-token" }, }, ], }, ], }, capabilities: { limits: { actions: { maxActions: 1 } } }, }); expect(presentation.blocks).toEqual([ { type: "buttons", buttons: [{ label: "Keep", value: "keep" }] }, { type: "context", text: "Actions:\n- Deploy: `/deploy production`\n- Approval\n- Opaque", }, ]); }); it("keeps unavailable typed select commands actionable without exposing callback values", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "select", placeholder: "Environment", options: [ { label: "Canary", action: { type: "command", command: "/deploy canary" } }, { label: "Production", action: { type: "command", command: "/deploy production" } }, { label: "Opaque", action: { type: "callback", value: "private-callback-token" } }, ], }, ], }, capabilities: { selects: false }, }); expect(presentation.blocks).toEqual([ { type: "context", text: "Environment:\n- Canary: `/deploy canary`\n- Production: `/deploy production`\n- Opaque", }, ]); }); it("keeps fallback labels for invalid buttons in mixed button blocks", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "buttons", buttons: [ { label: "Approve", value: "ok" }, { label: "Audit trail", value: "x".repeat(20) }, { label: "Docs", value: "x".repeat(20), url: "https://docs.example.test" }, { label: "Retry", action: { type: "callback", value: "x".repeat(20) }, url: "https://ignored.example.test", }, ], }, ], }, capabilities: { limits: { actions: { maxValueBytes: 4, }, }, }, }); expect(presentation.blocks).toEqual([ { type: "buttons", buttons: [ { label: "Approve", value: "ok" }, { label: "Docs", url: "https://docs.example.test" }, ], }, { type: "context", text: "Actions:\n- Audit trail\n- Retry" }, ]); }); it("degrades disabled buttons unless the channel supports disabled controls", () => { const unsupported = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "buttons", buttons: [{ label: "Wait", value: "wait", disabled: true }], }, ], }, capabilities: { limits: { actions: {}, }, }, }); const supported = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "buttons", buttons: [{ label: "Wait", value: "wait", disabled: true }], }, ], }, capabilities: { limits: { actions: { supportsDisabled: true, }, }, }, }); expect(unsupported.blocks).toEqual([{ type: "context", text: "Actions:\n- Wait" }]); expect(supported.blocks).toEqual([ { type: "buttons", buttons: [{ label: "Wait", value: "wait", disabled: true }], }, ]); }); it("keeps disabled link fallback non-actionable", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "buttons", buttons: [ { label: "Unavailable", action: { type: "url", url: "https://private.example.test" }, disabled: true, }, ], }, ], }, capabilities: { limits: { actions: {} } }, }); expect(presentation.blocks).toEqual([{ type: "context", text: "Actions:\n- Unavailable" }]); }); it("degrades unsupported controls before channel rendering", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "buttons", buttons: [{ label: "Approve", value: "approve" }], }, { type: "select", placeholder: "Target", options: [{ label: "Canary", value: "canary" }], }, { type: "divider" }, { type: "context", text: "Muted details" }, ], }, capabilities: { buttons: false, selects: false, context: false, divider: false, limits: { actions: { maxLabelLength: 4 }, selects: { maxLabelLength: 6 }, }, }, }); expect(presentation.blocks).toEqual([ { type: "text", text: "Actions:\n- Approve" }, { type: "text", text: "Target:\n- Canary" }, { type: "text", text: "Muted details" }, ]); }); it("keeps fallback labels for invalid or overflowed select options", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "select", placeholder: "Target", options: [ { label: "Canary", value: "canary" }, { label: "Production", value: "prod" }, { label: "Long callback", value: "x".repeat(20) }, ], }, ], }, capabilities: { limits: { selects: { maxOptions: 1, maxValueBytes: 8, }, }, }, }); expect(presentation.blocks).toEqual([ { type: "select", placeholder: "Target", options: [{ label: "Canary", value: "canary" }], }, { type: "context", text: "Target:\n- Production\n- Long callback" }, ]); }); it("splits unsupported button fallbacks without losing action labels", () => { const labels = ["Approve production", "Rollback release", "Show audit trail"]; const maxLength = 24; const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "buttons", buttons: labels.map((label, index) => ({ label, value: `action-${index}` })), }, ], }, capabilities: { buttons: false, context: false, limits: { actions: { maxLabelLength: 4 }, text: { maxLength, encoding: "characters" }, }, }, }); const fallback = presentation.blocks.map((block) => { expect(block.type).toBe("text"); return block.type === "text" ? block.text : ""; }); expect(fallback.length).toBeGreaterThan(1); expect(fallback.every((value) => Array.from(value).length <= maxLength)).toBe(true); expect(fallback.join("")).toBe(`Actions:\n${labels.map((label) => `- ${label}`).join("\n")}`); expect(renderMessagePresentationFallbackText({ presentation })).toBe( `Actions:\n${labels.map((label) => `- ${label}`).join("\n")}`, ); }); it("splits overflow select fallbacks without losing unavailable options", () => { const maxLength = 18; const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "select", placeholder: "Deployment target", options: [ { label: "Canary", value: "canary" }, { label: "Production cluster", value: "production" }, { label: "Rollback environment", value: "rollback" }, ], }, ], }, capabilities: { limits: { selects: { maxOptions: 1, maxLabelLength: 4 }, text: { maxLength, encoding: "characters" }, }, }, }); expect(presentation.blocks[0]).toMatchObject({ type: "select", options: [{ label: "Cana", value: "canary" }], }); const fallback = presentation.blocks.slice(1).map((block) => { expect(block.type).toBe("context"); return block.type === "context" ? block.text : ""; }); expect(fallback.length).toBeGreaterThan(1); expect(fallback.every((value) => Array.from(value).length <= maxLength)).toBe(true); expect(fallback.join("")).toBe( "Deployment target:\n- Production cluster\n- Rollback environment", ); expect(renderMessagePresentationFallbackText({ presentation })).toBe( "Depl:\n- Cana\n\nDeployment target:\n- Production cluster\n- Rollback environment", ); }); it("splits titles, text, context, and generated fallback without losing content", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { title: "abcdef", blocks: [ { type: "text", text: "hello world" }, { type: "context", text: "abcdef" }, { type: "buttons", buttons: [{ label: "Deploy", value: "toolong" }], }, ], }, capabilities: { limits: { actions: { maxValueBytes: 2, }, text: { maxLength: 5, encoding: "characters", }, }, }, }); expect(presentation).toEqual({ title: "abcde", blocks: [ { type: "text", text: "f" }, { type: "text", text: "hello" }, { type: "text", text: " worl" }, { type: "text", text: "d" }, { type: "context", text: "abcde" }, { type: "context", text: "f" }, { type: "context", text: "Actio" }, { type: "context", text: "ns:\n" }, { type: "context", text: "- Dep" }, { type: "context", text: "loy" }, ], }); expect(renderMessagePresentationFallbackText({ presentation })).toBe( "abcdef\n\nhello world\n\nabcdef\n\nActions:\n- Deploy", ); }); it.each( [ { encoding: "characters" as const, length: (text: string) => Array.from(text).length }, { encoding: "utf8-bytes" as const, length: (text: string) => Buffer.byteLength(text, "utf8"), }, { encoding: "utf16-units" as const, length: (text: string) => text.length }, ].flatMap((mode) => [ { sample: "mixed text", text: "abc😀 def\nlast" }, { sample: "long text", text: `${"😀".repeat(64)} split \n${"e\u0301 ".repeat(24)}last` }, ].map((sample) => ({ encoding: mode.encoding, length: mode.length, sample: sample.sample, text: sample.text, })), ), )( "preserves authored Unicode content under $encoding limits for $sample", ({ encoding, length, text }) => { const original = { title: text, blocks: [ { type: "text" as const, text }, { type: "context" as const, text }, ], }; const capabilities = { context: false, limits: { text: { maxLength: 6, encoding } } }; const presentation = adaptMessagePresentationForChannel({ presentation: original, capabilities, }); expect(length(presentation.title ?? "")).toBeLessThanOrEqual(6); for (const block of presentation.blocks) { expect(block.type).toBe("text"); if (block.type === "text") { expect(length(block.text)).toBeLessThanOrEqual(6); expect(Buffer.from(block.text, "utf8").toString("utf8")).toBe(block.text); } } expect(renderMessagePresentationFallbackText({ presentation })).toBe( renderMessagePresentationFallbackText({ presentation: original }), ); expect( renderMessagePresentationFallbackText({ presentation: normalizeMessagePresentation( adaptMessagePresentationForChannel({ presentation, capabilities }), ), }), ).toBe(renderMessagePresentationFallbackText({ presentation: original })); expect(adaptMessagePresentationForChannel({ presentation: original })).toEqual(original); }, ); it.each([ { sample: "astral labels", labels: ["😀😀😀", "🚀🚀🚀", "👍👍👍"] as const, prefixes: ["😀😀", "🚀🚀", "👍👍"], }, { sample: "long labels with isolated surrogates", labels: ["A\uD800B".repeat(24), "\uDC00AB".repeat(24), "😀\uD800Z".repeat(24)] as const, prefixes: ["A\uD800", "\uDC00A", "😀\uD800"], }, ])("preserves code-point prefixes for $sample", ({ labels, prefixes }) => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "buttons", buttons: [{ label: labels[0], value: "ok" }], }, { type: "select", placeholder: labels[1], options: [{ label: labels[2], value: "yes" }], }, ], }, capabilities: { limits: { actions: { maxLabelLength: 2, }, selects: { maxLabelLength: 2, }, }, }, }); expect(presentation.blocks).toEqual([ { type: "buttons", buttons: [{ label: prefixes[0], value: "ok" }], }, { type: "select", placeholder: prefixes[1], options: [{ label: prefixes[2], value: "yes" }], }, ]); }); it("preserves link buttons by dropping only over-limit callback values", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "buttons", buttons: [{ label: "Open report", value: "x".repeat(20), url: "https://example.test" }], }, ], }, capabilities: { limits: { actions: { maxValueBytes: 4, }, }, }, }); expect(presentation.blocks).toEqual([ { type: "buttons", buttons: [{ label: "Open report", url: "https://example.test" }], }, ]); }); it("applies button priority across the shared action budget", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "buttons", buttons: [{ label: "Low", value: "low" }], }, { type: "buttons", buttons: [{ label: "High", value: "high", priority: 10 }], }, ], }, capabilities: { limits: { actions: { maxActions: 1, }, }, }, }); expect(presentation.blocks).toEqual([ { type: "context", text: "Actions:\n- Low" }, { type: "buttons", buttons: [{ label: "High", value: "high", priority: 10 }], }, ]); }); it("keeps link targets when overflowed buttons become fallback text", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "buttons", buttons: [{ label: "One", value: "one" }], }, { type: "buttons", buttons: [{ label: "Docs", url: "https://docs.example.test" }], }, ], }, capabilities: { limits: { actions: { maxActions: 1, maxLabelLength: 4, }, }, }, }); expect(presentation.blocks).toEqual([ { type: "buttons", buttons: [{ label: "One", value: "one" }], }, { type: "context", text: "Actions:\n- Docs: https://docs.example.test" }, ]); }); it("preserves callback button values when actions do not declare a value limit", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "buttons", buttons: [{ label: "Approve", value: "x".repeat(180) }], }, ], }, capabilities: { limits: { actions: { maxActions: 5, maxActionsPerRow: 5, }, }, }, }); expect(presentation.blocks).toEqual([ { type: "buttons", buttons: [{ label: "Approve", value: "x".repeat(180) }], }, ]); }); it.each([false, true])( "reserves a select row with rejected leading options: %s", (hasRejected) => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "buttons", buttons: [ { label: "One", value: "one" }, { label: "Two", value: "two" }, { label: "Three", value: "three" }, ], }, { type: "select", placeholder: "Extra", options: [ ...(hasRejected ? [{ label: "Rejected", value: "too-long" }] : []), { label: "Four", value: "four" }, ], }, ], }, capabilities: { limits: { actions: { maxActionsPerRow: 2, maxRows: 2, }, selects: { maxOptions: 1, maxValueBytes: 4, }, }, }, }); expect(presentation.blocks).toEqual([ { type: "buttons", buttons: [ { label: "One", value: "one" }, { label: "Two", value: "two" }, ], }, { type: "context", text: "Actions:\n- Three" }, { type: "select", placeholder: "Extra", options: [{ label: "Four", value: "four" }], }, ...(hasRejected ? [{ type: "context", text: "Extra:\n- Rejected" }] : []), ]); }, ); it("preserves authored button precedence when only action rows are bounded", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "buttons", buttons: [{ label: "First", value: "first" }] }, { type: "select", placeholder: "Target", options: [{ label: "Later", value: "later" }], }, ], }, capabilities: { limits: { actions: { maxRows: 1 } } }, }); expect(presentation.blocks).toEqual([ { type: "buttons", buttons: [{ label: "First", value: "first" }] }, { type: "context", text: "Target:\n- Later" }, ]); }); it("splits button blocks by per-row limits even when rows are unlimited", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "buttons", buttons: [ { label: "One", value: "one" }, { label: "Two", value: "two" }, { label: "Three", value: "three" }, { label: "Four", value: "four" }, { label: "Five", value: "five" }, { label: "Six", value: "six" }, ], }, ], }, capabilities: { limits: { actions: { maxActions: 20, maxActionsPerRow: 5, }, }, }, }); expect(presentation.blocks).toEqual([ { type: "buttons", buttons: [ { label: "One", value: "one" }, { label: "Two", value: "two" }, { label: "Three", value: "three" }, { label: "Four", value: "four" }, { label: "Five", value: "five" }, ], }, { type: "buttons", buttons: [{ label: "Six", value: "six" }], }, ]); }); it("counts selects against the shared action capacity", () => { const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "select", placeholder: "Target", options: [{ label: "Canary", value: "canary" }], }, { type: "buttons", buttons: [ { label: "One", value: "one" }, { label: "Two", value: "two" }, { label: "Three", value: "three" }, ], }, ], }, capabilities: { limits: { actions: { maxActions: 3, maxActionsPerRow: 5, maxRows: 5, }, }, }, }); expect(presentation.blocks).toEqual([ { type: "select", placeholder: "Target", options: [{ label: "Canary", value: "canary" }], }, { type: "buttons", buttons: [ { label: "One", value: "one" }, { label: "Two", value: "two" }, ], }, { type: "context", text: "Actions:\n- Three" }, ]); }); it("resolves page size from available action capacity", () => { expect( presentationPageSize( { limits: { actions: { maxActionsPerRow: 5, maxRows: 2 }, }, }, 1, 20, ), ).toBe(9); }); it("keeps charts only for channels that explicitly advertise native support", () => { const chart = { type: "chart" as const, chartType: "bar" as const, title: "Quarterly revenue", categories: ["Q1", "Q2"], series: [{ name: "Revenue", values: [120, 145] }], }; expect( adaptMessagePresentationForChannel({ presentation: { blocks: [chart] }, capabilities: { charts: true }, }).blocks, ).toEqual([chart]); expect( adaptMessagePresentationForChannel({ presentation: { blocks: [chart] }, capabilities: { context: true }, }).blocks, ).toEqual([ { type: "context", text: "Quarterly revenue (bar chart)\n- Revenue: Q1: 120; Q2: 145", }, ]); expect( adaptMessagePresentationForChannel({ presentation: { blocks: [chart] }, capabilities: { context: false }, }).blocks[0]?.type, ).toBe("text"); }); it("splits chart fallback without losing the final series or category", () => { const categories = Array.from( { length: 20 }, (_, index) => `Category-${String(index).padStart(2, "0")}`, ); const series = Array.from({ length: 12 }, (_, seriesIndex) => ({ name: `Series-${String(seriesIndex).padStart(2, "0")}`, values: categories.map((_category, categoryIndex) => seriesIndex * 100 + categoryIndex), })); const chart = { type: "chart" as const, chartType: "line" as const, title: "Quarterly revenue", categories, series, }; const maxLength = 500; const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [chart] }, capabilities: { context: true, limits: { text: { maxLength, encoding: "characters" } }, }, }); const fallbackBlocks = presentation.blocks.map((block) => { expect(block.type).toBe("context"); return block.type === "context" ? block.text : ""; }); expect(fallbackBlocks.length).toBeGreaterThan(1); expect(fallbackBlocks.every((text) => Array.from(text).length <= maxLength)).toBe(true); const fallbackText = fallbackBlocks.join(""); expect(fallbackText).toBe(renderMessagePresentationChartFallbackText(chart)); expect(fallbackText).toContain("Category-19: 1119"); }); it("keeps tables only for channels that explicitly advertise native support", () => { const table = { type: "table" as const, caption: "Pipeline report", headers: ["Account", "Stage", "ARR"], rows: [ ["Acme", "Won", 125000], ["Globex", "Review", 82000], ], rowHeaderColumnIndex: 0, }; expect( adaptMessagePresentationForChannel({ presentation: { blocks: [table] }, capabilities: { tables: true }, }).blocks, ).toEqual([table]); expect( adaptMessagePresentationForChannel({ presentation: { blocks: [table] }, capabilities: { context: true }, }).blocks, ).toEqual([ { type: "context", text: [ "Pipeline report (table)", "- Account: Acme; Stage: Won; ARR: 125000", "- Account: Globex; Stage: Review; ARR: 82000", ].join("\n"), }, ]); expect( adaptMessagePresentationForChannel({ presentation: { blocks: [table] }, capabilities: { context: false }, }).blocks[0]?.type, ).toBe("text"); }); it.each([ { encoding: "characters" as const, length: (value: string) => Array.from(value).length, }, { encoding: "utf8-bytes" as const, length: (value: string) => Buffer.byteLength(value, "utf8"), }, { encoding: "utf16-units" as const, length: (value: string) => value.length, }, ])( "splits table fallback by line without losing rows for $encoding limits", ({ encoding, length }) => { const rows = Array.from({ length: 12 }, (_, index) => [ `Account-${String(index).padStart(2, "0")}`, `Stage-${index}`, ]); const maxLength = 64; const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "table", caption: "Pipeline report", headers: ["Account", "Stage"], rows, }, ], }, capabilities: { context: true, limits: { text: { maxLength, encoding } }, }, }); const fallbackBlocks = presentation.blocks.map((block) => { expect(block.type).toBe("context"); return block.type === "context" ? block.text : ""; }); expect(fallbackBlocks.length).toBeGreaterThan(1); expect(fallbackBlocks.every((text) => length(text) <= maxLength)).toBe(true); expect(fallbackBlocks.join("")).toBe( [ "Pipeline report (table)", ...rows.map(([account, stage]) => `- Account: ${account}; Stage: ${stage}`), ].join("\n"), ); }, ); it("hard-splits an oversized table row without breaking UTF-16 surrogate pairs", () => { const value = "😀".repeat(20); const maxLength = 10; const presentation = adaptMessagePresentationForChannel({ presentation: { blocks: [ { type: "table", caption: "R", headers: ["Value"], rows: [[value]], }, ], }, capabilities: { context: false, limits: { text: { maxLength, encoding: "utf16-units" } }, }, }); const fallbackBlocks = presentation.blocks.map((block) => { expect(block.type).toBe("text"); return block.type === "text" ? block.text : ""; }); expect(fallbackBlocks.every((text) => text.length <= maxLength)).toBe(true); expect( fallbackBlocks.every((text) => { const first = text.charCodeAt(0); const last = text.charCodeAt(text.length - 1); return !(first >= 0xdc00 && first <= 0xdfff) && !(last >= 0xd800 && last <= 0xdbff); }), ).toBe(true); expect(fallbackBlocks[0]).toBe("R (table)\n"); expect(fallbackBlocks.slice(1).join("")).toBe(`- Value: ${value}`); }); }); /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */