Files
apes-sidebar/tests/orphaned-tab.spec.tsx
Menghuan1918 223b2545d9 refactor(shell): retire the plugin's own right panel and free windows
DSH 0.1.5 owns the right column (its native Sidebar hosts every tab type
this plugin registers), so the plugin's own right panel, its width drag /
new-session seed prefs, and the free-window feature are gone. The bottom
workbench is the only plugin-owned surface left, and its expand/collapse
control is registered into DSH's session-header utilities list slot (the
header's corner seat belongs to the native sidebar's own control).

- state: one `bottomSplits` tree; `makeDefaultState()` takes no arguments;
  `toggleBottomPanel` / `openTabInBottomPane` are the only landing
  reducers; `setBottomHeight` and `sanitizeState` share the
  `CONVERSATION_MIN` cap (a persisted tall height no longer snaps on the
  first drag).
- service: every landing in the workbench (creation or focus) expands it;
  `SIDEBAR_FEATURES` drops `'floatWindows'`; the open/close/activate tab
  pools cover the single tree.
- shell: `Sidebar.tsx` renders only the bottom workbench; the toggle lives
  in `sidebar/bottom-toggle.tsx`; layout-push pushes height only (no width,
  no `--dsh-sidebar-width`).
- prefs: `openByDefault` / `defaultWidthPercent` / `changesDiffFloat`
  removed from the schema, defaults, client parser, settings page, and all
  20 dictionaries; the retired keys in stored documents are ignored.
- tests: free-window specs deleted, panel/float assertions dropped,
  remaining suites adapted (124 files / 1287 tests); the mount lane asserts
  the header toggle and sweeps the native sidebar's guide page.
- docs: README / README_EN / guide §0,§11 / AGENTS test map.
2026-09-09 15:18:06 +08:00

84 lines
2.9 KiB
TypeScript

/**
* Orphaned-tab tests: a persisted tab whose type is not registered must
* survive sanitize (title/path kept for recovery) and render the graceful
* placeholder instead of crashing — the degradation path of the open
* tab-type set (a contributing plugin not installed / not yet loaded).
*/
import { describe, expect, it } from 'vitest'
import { renderToString } from 'react-dom/server'
import { OrphanedTab } from '../src/client/OrphanedTab.tsx'
import { sanitizeState } from '../src/client/state.ts'
/** Collect every tab type in a sanitized tree (leaf/split recursive). */
function typesOf(node: unknown): string[] {
if (node === null || typeof node !== 'object') return []
const record = node as { kind?: unknown; tabs?: unknown; children?: unknown }
if (record.kind === 'leaf' && Array.isArray(record.tabs)) {
return (record.tabs as Array<{ type: string }>).map(tab => tab.type)
}
if (Array.isArray(record.children)) return record.children.flatMap(typesOf)
return []
}
describe('sanitize keeps unregistered tab types', () => {
it('preserves a persisted tab of an unknown type with its title/path', () => {
const raw = {
nextTerminal: 1,
activePane: null,
expanded: [],
bottomSplits: {
kind: 'leaf',
id: 'pane:1',
tabs: [
{ id: 'tab:1', type: 'my-plugin:db', title: 'Database', path: '/data.sqlite' },
{ id: 'tab:2', type: 'explorer', title: 'Explorer' },
],
active: 'tab:1',
},
}
const clean = sanitizeState(raw)
expect(clean).toBeDefined()
expect(typesOf(clean!.bottomSplits)).toContain('my-plugin:db')
// The kept tab keeps its payload so a later registration can reuse it.
const leaf = clean!.bottomSplits as unknown as { kind: 'leaf'; tabs: Array<Record<string, unknown>> }
const kept = leaf.tabs.find(tab => tab.type === 'my-plugin:db')
expect(kept).toMatchObject({ title: 'Database', path: '/data.sqlite' })
})
it('an unknown type alone in a pane still sanitizes cleanly', () => {
const raw = {
nextTerminal: 1,
activePane: null,
expanded: [],
bottomSplits: {
kind: 'leaf',
id: 'pane:1',
tabs: [{ id: 'tab:1', type: 'some-plugin:page', title: 'Page' }],
active: 'tab:1',
},
}
const clean = sanitizeState(raw)
expect(clean).toBeDefined()
expect(typesOf(clean!.bottomSplits)).toEqual(['some-plugin:page'])
})
})
describe('OrphanedTab placeholder', () => {
const tab = { id: 'tab:1', type: 'my-plugin:db', title: 'Database' }
it('renders the unregistered type and keeps the title', () => {
const html = renderToString((
<OrphanedTab
ctx={undefined as never}
store={undefined as never}
scope={{ sessionId: 's1', cwd: '/p' }}
tab={tab}
visible={false}
/>
))
expect(html).toContain('my-plugin:db')
expect(html).toContain('Database')
expect(html).not.toContain('undefined')
})
})