| 1 | import assert from "node:assert/strict"; |
| 2 | import { act } from "react"; |
| 3 | import { createTranscriptHarness } from "./transcript-dom-harness"; |
| 4 | import type { Item } from "../lib/useController"; |
| 5 | import type { LocalSubmission } from "../lib/localSubmissionState"; |
| 6 | import { initialState, reducer } from "../lib/useController"; |
| 7 | import { orderedLocalSubmissions } from "../lib/localSubmissionState"; |
| 8 | |
| 9 | const harness = await createTranscriptHarness({ deterministic: true }); |
| 10 | const pending: LocalSubmission = { submissionId: "send", localId: "optimistic", text: "unique question", createdAt: 1, sequence: 0, status: "sending" }; |
| 11 | const confirmed: Item = { kind: "user", id: "m:durable", messageId: "durable", submissionId: "send", text: "unique question" }; |
| 12 | try { |
| 13 | await harness.loadModule("/src/components/ChatToolBody.tsx"); |
| 14 | await harness.render([], { tabId: "identity-dom", localSubmissions: [pending], localSubmissionSendRevision: 1, running: true }); |
| 15 | await harness.settle(); |
| 16 | const selector = '[data-chat-kind="user"]'; |
| 17 | const before = harness.container.querySelector(selector); |
| 18 | await harness.render([confirmed], { tabId: "identity-dom", localSubmissions: [], localSubmissionSendRevision: 1, running: true }); |
| 19 | await harness.settle(); |
| 20 | assert.equal(harness.container.textContent?.split("unique question").length, 2, "one bubble after durable handoff"); |
| 21 | assert.ok(before); |
| 22 | assert.equal(harness.container.querySelector(selector), before, "mounted user identity remains stable"); |
| 23 | // React may render only the final state of identity binding + formal install. |
| 24 | let batched = reducer({ ...initialState, transcriptProtocol: 2 }, { type: "user", seq: 0, text: "batched question", submissionId: "batched" }); |
| 25 | const answer: Item = { kind: "assistant", id: "m:answer", text: "answer", reasoning: "process", streaming: false, turnFinal: true }; |
| 26 | await harness.render([answer], { tabId: "batched-dom", localSubmissions: orderedLocalSubmissions(batched), running: true }); |
| 27 | await harness.settle(); |
| 28 | const mounted = ["user", "process", "tail"].map(kind => harness.container.querySelector(`[data-chat-kind="${kind}"]`)); |
| 29 | batched = reducer(batched, { type: "event", e: { kind: "user_message", source: "executor", messageId: "bound", submissionId: "batched" } }); |
| 30 | batched = reducer(batched, { type: "transcript_records", confirmedUsers: [{ messageId: "bound" }], projection: { |
| 31 | items: [{ kind: "user", id: "m:bound", messageId: "bound", text: "batched question" }, answer], removeIds: [], |
| 32 | startTurn: 1, endTurn: 1, totalTurns: 1, hasOlder: false, hasNewer: false, revision: 1, revisionKnown: true, digest: "cut", |
| 33 | } }); |
| 34 | await harness.render(batched.items, { tabId: "batched-dom", localSubmissions: [], visibleSubmissionHandoffs: batched.visibleSubmissionHandoffs, running: true }); |
| 35 | await harness.settle(); |
| 36 | for (const [index, kind] of ["user", "process", "tail"].entries()) { |
| 37 | assert.ok(mounted[index], `${kind} was mounted`); |
| 38 | assert.equal(harness.container.querySelector(`[data-chat-kind="${kind}"]`), mounted[index], `${kind} survives a batched message-ID-only handoff`); |
| 39 | } |
| 40 | await harness.render([confirmed], { tabId: "identity-dom", localSubmissions: [{ ...pending, localId: "another", submissionId: "send-again", sequence: 1 }], localSubmissionSendRevision: 2, running: true }); |
| 41 | await harness.settle(); |
| 42 | assert.equal(harness.container.textContent?.split("unique question").length, 3, "same text sent twice remains visible twice"); |
| 43 | await harness.render([ |
| 44 | { ...confirmed, id: "m:first-shared", messageId: "first-shared", submissionId: "shared" }, |
| 45 | { ...confirmed, id: "m:second-shared", messageId: "second-shared", submissionId: "shared" }, |
| 46 | ], { tabId: "identity-dom", localSubmissions: [], localSubmissionSendRevision: 2, running: false }); |
| 47 | await harness.settle(); |
| 48 | assert.equal(harness.container.querySelectorAll(selector).length, 2, "different durable messages never share a DOM key even when submission ids collide"); |
| 49 | const oldPage: Item = { kind: "user", id: "m:old-page", messageId: "old-page", text: "old page question" }; |
| 50 | const pendingLatest = { ...pending, text: "latest pending question", submissionId: "latest-pending", localId: "latest-local" }; |
| 51 | await harness.render([oldPage], { tabId: "identity-dom", localSubmissions: [pendingLatest], localSubmissionSendRevision: 3, |
| 52 | hasNewerHistory: true, running: true }); |
| 53 | await harness.settle(); |
| 54 | assert.equal(harness.container.textContent?.includes("latest pending question"), false, "reading an old page does not insert the latest local echo"); |
| 55 | await harness.render([{ kind: "user", id: "m:latest", messageId: "latest", submissionId: "latest-pending", text: "latest pending question" }], |
| 56 | { tabId: "identity-dom", localSubmissions: [], localSubmissionSendRevision: 3, hasNewerHistory: false, running: false }); |
| 57 | await harness.settle(); |
| 58 | assert.equal(harness.container.textContent?.split("latest pending question").length, 2, "returning to latest renders one canonical message"); |
| 59 | for (const state of ["timed_out", "cancelled", "completed"] as const) { |
| 60 | const shell: Item = { kind: "tool", id: state, name: "bash", args: '{"command":"echo test"}', output: "test", status: "done", |
| 61 | execution: { kind: "shell", supportsAndAnd: true, state, exitCode: 0 } }; |
| 62 | await harness.render([shell], { tabId: "identity-dom", geometrySessionKey: state, localSubmissions: [pending] }); |
| 63 | await harness.settle(); |
| 64 | const row = harness.container.querySelector<HTMLElement>(".chat-tool")!; |
| 65 | const dot = row.querySelector(".dsh-StateDot-dot")?.getAttribute("data-state"); |
| 66 | assert.equal(dot, state === "completed" ? "done" : state === "cancelled" ? "warning" : "error"); |
| 67 | await act(async () => row.querySelector<HTMLElement>("[data-disclosure-row]")!.click()); |
| 68 | await harness.settle(); |
| 69 | assert.equal(row.dataset.state, state === "completed" ? "done" : state === "cancelled" ? "stopped" : "error"); |
| 70 | assert.ok(row.querySelector("[data-terminal]"), "structured shell detail is mounted"); |
| 71 | const dots = [...row.querySelectorAll(".dsh-StateDot-dot")]; |
| 72 | assert.equal(dots.length, 1, "expanded disclosure replaces its dot with a chevron"); |
| 73 | assert.ok(dots.every(node => node.getAttribute("data-state") === dot)); |
| 74 | } |
| 75 | console.log("DOM: same-frame identity dedup and shared shell presentation passed"); |
| 76 | } finally { await harness.unmount(); await harness.close(); } |
| 77 |