// 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; 3 chats at 420+8 → 3 columns side by side.
assert.match(html, /3 columns × 1 row/)
assert.match(html, /all chats side by side/)
// Real chat titles appear in the preview windows.
assert.match(html, /Alpha chat/)
})
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)
})