Files
learnharness/tests/session-context.test.tsx
T
amosblomqvist 1c27694dbc Wave 6 chunk A: sidebar context menu (Rename/Delete) + the two-option delete dialog
- Right-click any session row: MenuShell context menu, exactly two actions
  (owner ruling): Rename and Delete… — MenuShell discipline (placement,
  focus, arrow cycling, lh:menu-open announcements), agent-section's
  outside-click/Esc contract with [data-lh-menu] carve-outs.
- Rename is INLINE in the row (owner ruling): select-all entry, Enter/blur
  commits through the existing plumbing — PUT /api/chats/:id then
  lh:chat-renamed. The open chat header now LISTENS for the event too, so
  renaming the active session from the sidebar updates the header in place
  (it only dispatched before).
- Delete opens the two-option dialog (locked design, DELETE_OPTIONS pure
  seam for render pins): "Delete the session only" (preserve-graph — the
  graph keeps every contribution) vs "Also remove its graph traces"
  (delete-nodes — shared topics survive, only-this-session topics die).
  409 turn_in_flight/jobs_running and 503 extraction_winding_down render
  the server's line verbatim with a same-mode Try-again.
- Success: the sidebar patches its snapshot instantly, the active session
  navigates home (chat unmounts — drafts die with it), and lh:graph-revision
  announces the returned revision — the graph view refetches the snapshot
  AND costs (deletion is the one writer the status poll can't see).
- ChatDeleteMode type re-exported through the core barrel (the shell's one
  needed seam from core/graph-deletion.ts).

Gates: tsc clean, 909/909, lint 0, build ok. Live-verified on :3001:
context menu open/Esc, inline rename (row + API + active header), both
delete modes on scratch sessions, simulated 409 + retry-then-succeed,
active-session navigation home, graph+costs refetch deltas on home.
2026-09-23 12:45:12 +03:00

68 lines
2.8 KiB
TypeScript

import React from "react";
(globalThis as Record<string, unknown>).React = React;
import { renderToStaticMarkup } from "react-dom/server";
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { RenameRow, SessionRow } from "../components/sidebar.tsx";
import { DELETE_OPTIONS, DeleteSessionDialog } from "../components/delete-session-dialog.tsx";
import type { ChatListItem } from "../src/core/index.ts";
/**
* Wave 6 chunk A — the deletion + context-menu seams on the sidebar:
* SessionRow's right-click seam (always carries the row testid now), the
* inline RenameRow (same geometry, select-all entry), and the two-option
* delete dialog's plain-language copy (the locked design: both options
* name their reach, the graph-touching one says so).
*/
const chat: ChatListItem = {
id: "c1",
title: "Wave mechanics",
updatedAt: new Date().toISOString(),
busy: false,
} as unknown as ChatListItem;
describe("SessionRow — the right-click seam", () => {
it("rows carry the session-row testid (context menu anchors to it)", () => {
const html = renderToStaticMarkup(<SessionRow chat={chat} active={false} onOpen={() => {}} />);
assert.ok(html.includes('data-testid="session-row"'));
});
});
describe("RenameRow — the inline rename surface", () => {
it("renders the draft input with the current title, keeping the meta line", () => {
const html = renderToStaticMarkup(
<RenameRow chat={chat} onCommit={() => {}} onCancel={() => {}} />,
);
assert.ok(html.includes('data-testid="session-rename-input"'));
// controlled draft starts from the current title
assert.ok(html.includes(`value="${chat.title}"`));
// the meta line rides along (row never changes height mid-edit)
assert.ok(html.includes("session-rename-row"));
});
});
describe("DeleteSessionDialog — the locked two-option copy (portals no-op in SSR; pin the pure seam)", () => {
it("closed (chat=null) renders nothing", () => {
const html = renderToStaticMarkup(
<DeleteSessionDialog chat={null} onClose={() => {}} onDeleted={() => {}} />,
);
assert.equal(html, "");
});
it("exactly two options, both modes covered, consequences in plain language", () => {
assert.equal(DELETE_OPTIONS.length, 2);
const modes = DELETE_OPTIONS.map((o) => o.mode).sort();
assert.deepEqual(modes, ["delete-nodes", "preserve-graph"]);
});
it("the graph-touching option says so; the session-only option promises the graph stays", () => {
const preserve = DELETE_OPTIONS.find((o) => o.mode === "preserve-graph");
const nodes = DELETE_OPTIONS.find((o) => o.mode === "delete-nodes");
assert.ok(preserve?.body.includes("graph stays"));
assert.ok(nodes?.body.includes("leave the graph"));
// shared-topics survival is stated on the destructive option
assert.ok(nodes?.body.includes("survive"));
});
});