import React from "react"; (globalThis as Record).React = React; import { renderToStaticMarkup } from "react-dom/server"; import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { DraftShell, startDraftSession } from "../components/new-session.tsx"; /** * The sessionless draft view (/chat/new, owner 2026-09-13): "+ New session" * opens the draft — NOTHING is created until the first message is sent, and * that message becomes the session's title. These pins hold the two seams: * startDraftSession (the create-on-send contract) and DraftShell (the * visible surface: composer blob + stats bar + the fresh-chat empty state). */ /** A fetch double: records calls, answers /api/chats with a canned id. */ function fakeFetch(id: string) { const calls: { url: string; method: string; body: unknown }[] = []; const f = (async (url: string, init?: RequestInit) => { calls.push({ url: String(url), method: init?.method ?? "GET", body: init?.body ? JSON.parse(String(init.body)) : null }); return new Response(JSON.stringify({ id }), { status: 200 }); }) as typeof fetch; return { f, calls }; } describe("startDraftSession — the create-on-send contract", () => { it("creates the session titled with the first message, returns the prompt redirect", async () => { const { f, calls } = fakeFetch("c_new"); const target = await startDraftSession("teach me orbital mechanics", f); assert.equal(target, "/chat/c_new?prompt=teach%20me%20orbital%20mechanics"); assert.equal(calls.length, 1); assert.equal(calls[0]!.url, "/api/chats"); assert.equal(calls[0]!.method, "POST"); assert.deepEqual(calls[0]!.body, { title: "teach me orbital mechanics" }); }); it("no settings PUT on create — the server stamps the newborn (per-session models, owner 2026-09-28)", async () => { const { f, calls } = fakeFetch("c_3"); await startDraftSession("hello", f); assert.equal(calls.length, 1, "a single create call — the birth stamp is server-side"); }); it("empty or whitespace text never creates anything", async () => { const { f, calls } = fakeFetch("never"); assert.equal(await startDraftSession("", f), null); assert.equal(await startDraftSession(" ", f), null); assert.equal(calls.length, 0); }); }); describe("DraftShell — the sessionless surface", () => { it("renders the composer blob, the stats bar, and the fresh-chat empty state", () => { const html = renderToStaticMarkup( {}} onSend={() => {}} />, ); // The chat composer, ready for the first message. assert.ok(html.includes('data-testid="composer"')); // The draft's model control binds the strong slot — the // new-session default (per-session models, owner 2026-09-28). assert.ok(html.includes('data-testid="stats-model"')); // No CTA by default: a configured surface stays quiet. assert.ok(!html.includes('data-testid="setup-cta"')); // The same empty state a fresh chat shows. assert.ok(html.includes("The teacher will open by probing what you already know.")); // The pointer-events pattern (953b22b): the wrapper stays inert… assert.ok(html.includes("pointer-events-none")); // …and the blob re-enables it. assert.ok(/rounded-2xl[^>]*pointer-events-auto|pointer-events-auto[^>]*rounded-2xl/.test(html)); }); it("renders the setup CTA above the composer blob when passed (fix wave chunk B: /chat/new mirrors home)", () => { const html = renderToStaticMarkup( {}} onSend={() => {}} setupCta={ } />, ); // The same affordance home carries, above the draft shell's blob. assert.ok(html.includes('data-testid="setup-cta"')); // Above the composer: the CTA's markup precedes the blob's. const cta = html.indexOf('data-testid="setup-cta"'); const blob = html.search(/rounded-2xl[^>]*pointer-events-auto|pointer-events-auto[^>]*rounded-2xl/); assert.ok(cta !== -1 && blob !== -1 && cta < blob, "CTA above the blob"); }); it("no chat id anywhere — nothing in the markup references a session", () => { const html = renderToStaticMarkup( {}} onSend={() => {}} />, ); assert.ok(!/\/chat\/c[a-z0-9_]+/.test(html)); }); });