// Headless checks for client.js: loads it through a fake __ModuleLoader__, captures the slot // registrations, renders the components with react-dom/server against fake DSH services // (sessions list, layout panels, workspace navigation) and drives the user flows: // include a chat beside the current one, pick chats in the main sidebar, close back to one. // Needs a React install: REACT_DIR=, otherwise one is // searched for under the home directory; the tests skip when none is found. import test from 'node:test' import assert from 'node:assert/strict' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { createRequire } from 'node:module' import { fileURLToPath } from 'node:url' const here = path.dirname(fileURLToPath(import.meta.url)) function findReact() { const ok = (d) => d && fs.existsSync(path.join(d, 'react', 'package.json')) && fs.existsSync(path.join(d, 'react-dom', 'server.js')) if (ok(process.env.REACT_DIR)) return process.env.REACT_DIR const roots = [path.join(here, '..', 'node_modules'), path.join(os.homedir(), 'rizzy-wt'), path.join(os.homedir(), 'Python')] const seen = new Set() const walk = (dir, depth) => { if (depth < 0 || seen.has(dir)) return null seen.add(dir) if (ok(dir)) return dir let ents = [] try { ents = fs.readdirSync(dir, { withFileTypes: true }) } catch { return null } for (const e of ents) { if (!e.isDirectory() || e.name === '.git') continue const pnpm = e.name === '.pnpm' if (pnpm) { for (const p of fs.readdirSync(path.join(dir, e.name))) if (p.startsWith('react-dom@')) { const d = path.join(dir, e.name, p, 'node_modules'); if (ok(d)) return d } continue } const r = walk(path.join(dir, e.name), depth - 1) if (r) return r } return null } for (const r of roots) { const d = walk(r, 4); if (d) return d } return null } const reactDir = findReact() const skip = reactDir ? false : 'no React install found (set REACT_DIR)' function memStorage() { const m = new Map() return { getItem: (k) => (m.has(k) ? m.get(k) : null), setItem: (k, v) => m.set(k, String(v)), removeItem: (k) => m.delete(k) } } function snapshotStore(initial) { let v = initial; const subs = new Set() return { getSnapshot: () => v, subscribe: (f) => { subs.add(f); return () => subs.delete(f) }, set: (n) => { v = n; [...subs].forEach((f) => f()) } } } const tick = (ms = 0) => new Promise((r) => setTimeout(r, ms)) /** Load client.js and apply it against fakes that behave like DSH's services. */ function load() { const req = createRequire(path.join(reactDir, 'react', 'package.json')) const React = req('react') const ReactDOMServer = req('react-dom/server') globalThis.localStorage = memStorage() globalThis.window = { innerWidth: 1600, innerHeight: 1000 } globalThis.document = { head: { appendChild() {} }, createElement: () => ({ remove() {} }), querySelector: () => null, addEventListener() {}, removeEventListener() {} } globalThis.ResizeObserver = class { observe() {} disconnect() {} } let mod window.__ModuleLoader__ = { load: (m) => { mod = m } } new Function(fs.readFileSync(path.join(here, '..', 'client.js'), 'utf8'))() const plugin = mod.factory((name) => (name === 'react' ? React : {})) const list = snapshotStore({ phase: 'ready', ids: ['a', 'b', 'c', 'd', 'sub'], byId: { a: { id: 'a', displayTitle: 'Alpha chat', updatedAt: 4, retainedBy: {} }, b: { id: 'b', displayTitle: 'Beta chat', updatedAt: 3, retainedBy: {} }, c: { id: 'c', displayTitle: 'Gamma chat', updatedAt: 2, retainedBy: {} }, d: { id: 'd', displayTitle: 'Delta chat', updatedAt: 1, retainedBy: {} }, sub: { id: 'sub', displayTitle: 'child', updatedAt: 9, parentId: 'a', origin: 'subagent', retainedBy: {} }, }, }) const setMainView = (id, count = 1) => { const s = list.getSnapshot() const byId = Object.fromEntries(Object.entries(s.byId).map(([k, v]) => [k, { ...v, retainedBy: k === id ? { mainView: count } : {} }])) list.set({ ...s, byId }) } const sessions = { list, retain: (id, opts) => ({ sessionId: id, source: opts.source, release() {} }) } const panel = snapshotStore({ activePanelId: null }) const layout = { panelInfo: panel, selectPanel: (id) => panel.set({ activePanelId: id }) } /** Like the stock workspace: retain as mainView (sync), release the old one, back to the conversation view. */ const uiWorkspace = { opened: [], openSession(id) { this.opened.push(id); setMainView(id, 2); setMainView(id, 1); layout.selectPanel(null) }, } const regs = {} const ctx = { sessions, layout, uiWorkspace, effect: (fn) => { fn() }, slots: { inject: (name, make) => make(), register: (opts, Comp) => { regs[opts.key ? `${opts.name}:${opts.key}` : opts.name] = { opts, Comp }; return () => {} }, }, } assert.deepEqual(plugin.inject, ['slots', 'sessions', 'layout', 'uiWorkspace']) plugin.apply(ctx) const h = React.createElement const renderPanel = () => ReactDOMServer.renderToString(h(regs['main:dsh-chat-split'].Comp, { renderSlot: () => null, SessionProvider: ({ children }) => children })) /** Click "Include in / Exclude from split view" in a chat's "…" menu. */ const menu = (sessionId) => { let onSelect let label const Menu = regs['sidebar.workspaces.session.menu.item'].Comp const Capture = (props) => { const el = Menu(props); onSelect = el.props.onSelect || el.props.onClick; label = String(el.props.children); return el } ReactDOMServer.renderToString(h(Capture, { sessionId, useMenuOpenState: () => [true, () => {}] })) return { label, click: () => onSelect() } } const windows = (html) => [...html.matchAll(/data-session-id="([^"]+)"/g)].map((m) => m[1]) return { React, ReactDOMServer, regs, list, panel, layout, uiWorkspace, setMainView, renderPanel, menu, windows } } test('no separate split button: only the main area, the window slot, the "…" menu item and settings', { skip }, () => { const { regs } = load() assert.equal(regs['sidebar.panellist'], undefined, 'no panel-list entry') const main = regs['main:dsh-chat-split'] assert.ok(main) assert.deepEqual(main.opts.children, { 'dsh-chat-split.window': { kind: 'single', scope: 'session' } }) assert.ok(regs['dsh-chat-split.window']) assert.ok(regs['sidebar.workspaces.session.menu.item']) assert.equal(regs['settings.section'].opts.label, 'Chat split') }) test('"Include in split view" puts the chat beside the one being viewed, in place', { skip }, async () => { const t = load() t.setMainView('a') // viewing Alpha normally const m = t.menu('c') assert.equal(m.label, 'Include in split view') m.click() assert.equal(t.panel.getSnapshot().activePanelId, 'dsh-chat-split', 'the conversation area turned into the split') await tick(200) const html = t.renderPanel() assert.deepEqual(t.windows(html), ['a', 'c'], 'viewed chat first, included chat beside it') assert.match(html, /dcs-win focused glow" data-session-id="c"/, 'the new window is focused and glows') assert.match(html, /Show only this chat/) assert.match(html, /Open another chat beside/) assert.equal(t.menu('c').label, 'Exclude from split view') }) test('picking chats in the main sidebar while split: already open → glow, otherwise opens in the last used window', { skip }, async () => { const t = load() t.setMainView('a'); t.menu('b').click(); await tick(150) // split: a | b (b focused) // Pick Alpha (already open) in the sidebar → stays split, Alpha glows. t.uiWorkspace.openSession('a'); await tick(10) assert.equal(t.panel.getSnapshot().activePanelId, 'dsh-chat-split') let html = t.renderPanel() assert.deepEqual(t.windows(html), ['a', 'b']) assert.match(html, /glow" data-session-id="a"/) // Pick Delta (not open) → it replaces the last used window (Alpha, just focused by the pick). t.uiWorkspace.openSession('d'); await tick(200) assert.equal(t.panel.getSnapshot().activePanelId, 'dsh-chat-split') html = t.renderPanel() assert.deepEqual(t.windows(html), ['d', 'b']) assert.match(html, /glow" data-session-id="d"/) // Leaving to another panel and coming back without picking a chat changes nothing. t.layout.selectPanel('plugins'); await tick(900); t.layout.selectPanel(null); await tick(10) assert.equal(t.panel.getSnapshot().activePanelId, 'dsh-chat-split') assert.deepEqual(t.windows(t.renderPanel()), ['d', 'b']) }) test('closing windows down to one returns to the normal single-chat view with that chat', { skip }, async () => { const t = load() t.setMainView('a'); t.menu('b').click(); await tick(150) t.menu('c').click(); await tick(150) assert.deepEqual(t.windows(t.renderPanel()), ['a', 'b', 'c']) t.menu('a').click() // "Exclude from split view" assert.deepEqual(t.windows(t.renderPanel()), ['b', 'c']) t.menu('c').click() await tick(10) assert.deepEqual(t.uiWorkspace.opened.at(-1), 'b', 'the remaining chat opens normally') assert.equal(t.panel.getSnapshot().activePanelId, null, 'normal conversation view') assert.equal(t.menu('b').label, 'Include in split view') }) test('layout: windows wrap into a grid when the conversation would get narrower than the readability width', { skip }, () => { const { React, ReactDOMServer, regs } = load() const h = React.createElement const html = ReactDOMServer.renderToString(h(regs['settings.section'].Comp, { close() {} })) assert.match(html, /Minimum window height/) assert.match(html, /396px/) // Preview 1300 px wide: a window needs 420 (conversation) + 180 (its chat list) → 2 columns × 2 rows for 3 chats. assert.match(html, /2 columns × 2 rows/) assert.match(html, /too narrow for all chats side by side/) assert.match(html, /Alpha chat/, 'preview uses real chat titles') window.innerWidth = 2400 // 2100 px → 3 × 600 fits in one row assert.match(ReactDOMServer.renderToString(h(regs['settings.section'].Comp, { close() {} })), /3 columns × 1 row/) })