// 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, and // drives the "chat opened from the main sidebar while split view is showing" path. // Needs a React install: REACT_DIR= (skips otherwise). import test from 'node:test' import assert from 'node:assert/strict' import fs from 'node:fs' import path from 'node:path' import { createRequire } from 'node:module' import { fileURLToPath } from 'node:url' const here = path.dirname(fileURLToPath(import.meta.url)) const candidates = [ process.env.REACT_DIR, '/home/deepseek-harness/rizzy-wt/m3-history/node_modules/.pnpm/react-dom@19.3.0_react@19.3.0/node_modules', ].filter(Boolean) const reactDir = candidates.find((d) => fs.existsSync(path.join(d, 'react')) && fs.existsSync(path.join(d, 'react-dom'))) 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()) } } } /** Load client.js and apply it against fakes; returns everything the test needs. */ 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 } 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', 'sub'], byId: { a: { id: 'a', displayTitle: 'Alpha chat', updatedAt: 3, retainedBy: {} }, b: { id: 'b', displayTitle: 'Beta chat', updatedAt: 2, retainedBy: {} }, c: { id: 'c', displayTitle: 'Gamma chat', updatedAt: 1, retainedBy: {} }, sub: { id: 'sub', displayTitle: 'child', updatedAt: 9, parentId: 'a', origin: 'subagent', retainedBy: {} }, }, }) const retained = [] const sessions = { list, retain: (id, opts) => { const r = { sessionId: id, source: opts.source, released: false, release() { this.released = true } }; retained.push(r); return r } } const panel = snapshotStore({ activePanelId: null }) const layout = { panelInfo: panel, selectPanel: (id) => panel.set({ activePanelId: id }) } const regs = {} const effects = [] const ctx = { sessions, layout, effect: (fn) => { const d = fn(); effects.push(d) }, 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']) plugin.apply(ctx) return { React, ReactDOMServer, regs, list, panel, layout, retained } } test('registers panel, main entry with a session-scope child, menu item and settings', { skip }, () => { const { regs } = load() assert.equal(regs['sidebar.panellist'].opts.id, 'dsh-chat-split') assert.equal(regs['sidebar.panellist'].opts.label, 'Split view') const main = regs['main:dsh-chat-split'] assert.ok(main, 'main panel registered under key dsh-chat-split') 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('menu item includes/excludes, panel renders windows with top bars and own chat lists', { skip }, () => { const { React, ReactDOMServer, regs } = load() const h = React.createElement const render = (el) => ReactDOMServer.renderToString(el) const Menu = regs['sidebar.workspaces.session.menu.item'].Comp const useMenuOpenState = () => [true, () => {}] assert.match(render(h(Menu, { sessionId: 'a', useMenuOpenState })), /Include in split view/) const Panel = regs['main:dsh-chat-split'].Comp const empty = render(h(Panel, { renderSlot: () => null, SessionProvider: ({ children }) => children })) assert.match(empty, /No chats in split view/) assert.match(empty, /\+ Alpha chat/) assert.doesNotMatch(empty, /child/, 'subagent children are not offered') }) test('layout: side by side while each keeps the readability width, then wraps', { skip }, () => { const { React, ReactDOMServer, regs } = load() const h = React.createElement const Settings = regs['settings.section'].Comp const html = ReactDOMServer.renderToString(h(Settings, { close() {} })) assert.match(html, /Minimum window height/) assert.match(html, /396px/) assert.match(html, /Readability width/) assert.match(html, /Preview/) // window.innerWidth 1600 → preview width 1300. A window needs 420 (conversation) + 180 (its // chat list) = 600 px, so 3 chats do not fit side by side: 2 columns, wrapping to a 2nd row. assert.match(html, /2 columns × 2 rows/) assert.match(html, /too narrow for all chats side by side/) // Real chat titles appear in the preview windows. assert.match(html, /Alpha chat/) }) test('layout: a wide screen keeps all chats in one row', { skip }, () => { const { React, ReactDOMServer, regs } = load() window.innerWidth = 2400 // preview 2100 px → 3 × 600 fits const html = ReactDOMServer.renderToString(React.createElement(regs['settings.section'].Comp, { close() {} })) assert.match(html, /3 columns × 1 row/) assert.match(html, /all chats side by side/) }) test('choosing an open chat in the main sidebar keeps split view and glows it', { skip }, async () => { const { React, ReactDOMServer, regs, list, panel, layout } = load() const h = React.createElement const Menu = regs['sidebar.workspaces.session.menu.item'].Comp // Capture the onSelect handler of the menu item by rendering its element tree shallowly. let onSelect const fakeMenuButton = Menu.length // keep lint quiet void fakeMenuButton // Use the plugin's own store through the panel: include 'b' via a synthetic menu render. const MenuWithCapture = (props) => { const el = Menu(props) onSelect = el.props.onSelect || el.props.onClick return el } ReactDOMServer.renderToString(h(MenuWithCapture, { sessionId: 'b', useMenuOpenState: () => [true, () => {}] })) onSelect() const Panel = regs['main:dsh-chat-split'].Comp const html = ReactDOMServer.renderToString(h(Panel, { renderSlot: () => h('i', null, 'conv'), SessionProvider: ({ children }) => children })) assert.match(html, /data-session-id="b"/) assert.match(html, /Beta chat/) assert.match(html, /Close window/) assert.match(html, /Hide this window's chat list/) // Split view showing → user clicks 'b' in the stock sidebar: mainView retain changes, then panel → null. layout.selectPanel('dsh-chat-split') const s = list.getSnapshot() list.set({ ...s, byId: { ...s.byId, b: { ...s.byId.b, retainedBy: { mainView: 1 } } } }) layout.selectPanel(null) await new Promise((r) => setTimeout(r, 10)) assert.equal(panel.getSnapshot().activePanelId, 'dsh-chat-split', 'returned to split view') const glowing = ReactDOMServer.renderToString(h(Panel, { renderSlot: () => null, SessionProvider: ({ children }) => children })) assert.match(glowing, /dcs-win glow/) // Leaving split view without opening a chat (no mainView change) stays left. await new Promise((r) => setTimeout(r, 900)) layout.selectPanel('dsh-chat-split') layout.selectPanel(null) await new Promise((r) => setTimeout(r, 10)) assert.equal(panel.getSnapshot().activePanelId, null) // Opening a chat that is NOT in the split leaves split view normally. layout.selectPanel('dsh-chat-split') const s2 = list.getSnapshot() list.set({ ...s2, byId: { ...s2.byId, b: { ...s2.byId.b, retainedBy: {} }, c: { ...s2.byId.c, retainedBy: { mainView: 1 } } } }) layout.selectPanel(null) await new Promise((r) => setTimeout(r, 10)) assert.equal(panel.getSnapshot().activePanelId, null) })