import { expect, test } from "bun:test"; import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { Transcript } from "./transcript"; import { childDocumentPath } from "@chopin/protocol/document-url"; import { answerArtifact, evidenceArtifact, settle, setup, } from "../../../server/src/research/test-support"; import { terminalNotices } from "../../../server/src/research/inline.test-fixtures"; import type { Chat } from "@chopin/protocol"; test("system presence entries stay immediate when they arrive", () => { let markup = renderToStaticMarkup( createElement(Transcript, { active: true, entries: [{ author: { kind: "system" }, id: "joined", text: "@sam joined", ts: 1_700_000_000, }], handle: "ana", onWithdraw: () => {}, queued: [], }), ); expect(markup).toContain("Sam joined"); expect(markup).not.toContain("data-motion-feedback"); }); test("queued messages use the standard icon-button glyph", () => { let markup = renderToStaticMarkup( createElement(Transcript, { active: true, entries: [], handle: "ana", onWithdraw: () => {}, queued: [{ handle: "ana", id: "queued", text: "One more thought" }], }), ); expect(markup).toMatch(/aria-label="Withdraw queued message"[^>]*>.*?height="14"/s); expect(markup).toMatch(/aria-label="Withdraw queued message"[^>]*>.*?width="14"/s); expect(markup).toContain(''); expect(markup).not.toContain(">×"); }); test("a ready research notice links to its child document while other notices stay plain text", () => { let childPath = "/documents/octo-org/score/parent/children/research"; let markup = renderToStaticMarkup(createElement(Transcript, { active: true, entries: [ { author: { kind: "system" }, id: "ready", text: `Research is ready. [Open the research document](${childPath}).`, ts: 1_700_000_000, }, { author: { kind: "system" }, id: "failed", text: "Research could not be completed. You can retry it from the research card.", ts: 1_700_000_001, }, ], handle: "ana", onWithdraw: () => {}, queued: [], })); expect(markup).toContain(`Open the research document"); expect(markup).toContain( "Research could not be completed. You can retry it from the research card.", ); expect(markup.match(/data-chat-markdown/g)).toHaveLength(1); }); test("system notices with arbitrary Markdown remain text", () => { let markup = renderToStaticMarkup(createElement(Transcript, { active: true, entries: [{ author: { kind: "system" }, id: "other", text: "[Outside](https://example.com) unsafe", ts: 1_700_000_000, }], handle: "ana", onWithdraw: () => {}, queued: [], })); expect(markup).not.toContain(""); expect(markup).toContain("[Outside](https://example.com) <b>unsafe</b>"); }); test("a ready notice cannot turn an external destination into a system link", () => { let markup = renderToStaticMarkup(createElement(Transcript, { active: true, entries: [{ author: { kind: "system" }, id: "outside", text: "Research is ready. [Open the research document](https://example.com).", ts: 1_700_000_000, }], handle: "ana", onWithdraw: () => {}, queued: [], })); expect(markup).not.toContain(" { let context = await setup(); let started = await context.service.startPlannerInline({ channelId: context.channelId, question: "Check the release", originMessageId: "message-transcript-ready", requestedBy: context.userId, placeReference: async () => "placed", }); let evidence = await settle(context, "research-evidence", evidenceArtifact); await context.service.jobChanged(evidence.job); let answer = await settle(context, "research-answer", answerArtifact); let { service, notices } = terminalNotices(context); await service.jobChanged(answer.job); let request = await service.request(context.channelId, started.request.id); if (request?.stage !== "ready") throw new Error("published research child is not ready"); let path = childDocumentPath( context.channel.repositoryOwner, context.channel.repositoryName, context.channel.slug, request.child.slug, ); expect(notices).toHaveLength(1); let markup = renderToStaticMarkup(createElement(Transcript, { active: true, entries: notices.map((notice): Chat.Entry => ({ ...notice, author: { kind: "system" }, ts: 1, })), handle: "ana", onWithdraw() {}, queued: [], })); expect(markup).toContain(`Open the research document"); expect(markup).toContain('class="chat-markdown min-w-0 break-words text-sm'); expect(markup).not.toContain("[Open the research document]"); }); test.each([ "/documents/octo-org/score/parent", "/documents/octo-org/score/parent/children/", "/documents/octo-org/score/parent/children/%ZZ", "/documents/octo-org/score/parent/children/research?next=https://example.com", "/documents/octo-org/score/parent/children/research#report", "/documents/octo-org/score/parent/children/research/extra", "//example.com/documents/octo-org/score/parent/children/research", "javascript:alert(1)", ])("ready notices with an invalid child route stay text: %s", path => { let text = `Research is ready. [Open the research document](${path}).`; let markup = renderToStaticMarkup(createElement(Transcript, { active: true, entries: [{ author: { kind: "system" }, id: "invalid", text, ts: 1 }], handle: "ana", onWithdraw() {}, queued: [], })); expect(markup).not.toContain(" { let text = "Research is ready. [Open the research document]" + "(/documents/octo-org/score/parent/children/research). [Extra](https://example.com)."; let markup = renderToStaticMarkup(createElement(Transcript, { active: true, entries: [{ author: { kind: "system" }, id: "extra", text, ts: 1 }], handle: "ana", onWithdraw() {}, queued: [], })); expect(markup).not.toContain(" { let span = { turnId: "turn", entryOffset: 0, endOffset: 1, anchorId: "agent" }; let entry: Chat.Entry = { author: { kind: "agent" }, id: "agent", text: "Here is the answer.", ts: 1, }; let render = (tools: Chat.Activity[] | undefined, suspended: boolean) => renderToStaticMarkup(createElement(Transcript, { active: true, entries: [{ ...entry, tools }], handle: "ana", onWithdraw() {}, queued: [], ...(suspended ? { suspendedWork: span } : { completedWork: [span] }), })); let tool: Chat.Activity = { id: "tool", name: "read_plan", status: "done" }; expect(render(undefined, false)).toContain("Chopin turn ended."); expect(render(undefined, true)).toContain("Chopin connection lost."); expect(render([tool], false)).toContain( "Chopin turn ended. Work details remain available.", ); expect(render([tool], true)).toContain( "Chopin connection lost. Work details remain available.", ); });