Files
apes-sidebar/tests/plugin-shape.spec.ts
T
Menghuan1918 5912274e41 feat: yield the terminal and browser to DSH 0.1.6's own sidebar types
DSH 0.1.6-alpha.2 ships a right-Sidebar terminal and a right-Sidebar browser
of its own. Both were things this plugin had built from scratch, so keeping
them meant two implementations of one surface plus a plugin that shadows a
host type the product is actively developing.

Terminal: the plugin's whole PTY stack goes — the UI-tab manager, the
agent-owned registry, the lazy node-pty loader, the eight model-facing
terminal_* tools, the xterm view and its font/link/scrollback helpers, and
the cross-session 'pinned terminal' rail that existed only to keep a terminal
alive across a session switch. The host's terminal kind becomes the only
provider, which also retires the node-pty dependency, its pnpm allowBuilds
entry, and the installer's -Repair mode (issue #140's whole class of
'node-pty failed to load' support).

Model-facing capability note: the plugin used to inject terminal_* tools
(off by default). Those are the model's only cross-call persistent terminal,
and DSH's own equivalent (@deepseek-ai/dsh-tool-terminal) is not mounted by
any shipped bundle, so removing them leaves the model with the one-shot
bash/pwsh tools only. That is the accepted cost of not maintaining a second
PTY surface; the README documents how to enable the upstream toolset.

Browser: the plugin's browser tab type is dropped, so the host's own browser
kind owns embedded pages. The DOM-level link takeover STAYS (it is the only
thing that can route an https link to the system browser by protocol, which
the host does not do) but now delegates to the host's browser tab via
ctx.sidebarRight.openTab('browser', …) with a window.open fallback, and the
two settings that only ever configured the plugin's own iframe (sandbox
escape hatch, loopback allowlist) go with it — along with the host-side
browser.probe route they fed.

Cards: the empty-pane welcome cards adopt the host's guide-capsule recipe
(0.5px l4 hairline, 24px radius, layer-1 fill, 56px floor, 14px/20px padding)
so the plugin's 'pick what opens here' surface reads as the same design as
DSH's own. A theme.spec guard pins the geometry and the token-only paint —
including that the upstream-only --dsw-alias-bg-l1/-l2 spellings (defined
nowhere, so their declarations are silently dropped upstream) never appear
here.

Also drops the dead state the removed features left behind (SidebarTab.pin,
nextTerminal, agentWaits, bottomOpenedOnce, the agent-tab helpers, the
pinned-tab memo key), re-points every test fixture that used them at
surviving equivalents, and prunes the specs of the deleted features.
2026-09-21 20:35:11 +08:00

95 lines
5.0 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import Loader from '@cordisjs/plugin-loader'
import * as sidebar from '../src/index.ts'
/**
* Run the real namespace export through `Loader.unwrapExports`; a stray
* default would discard `name`, `inject`, `Config`, and `apply`. Same guard
* the official plugin repos ship (dsh-external/turtle-ui,
* packages/ui/jsonrpc).
*/
describe('dsh-better-sidebar plugin export shape', () => {
it('has the namespace-plugin export shape (no stray default) so the Loader keeps name/inject/Config/apply', () => {
expect('default' in sidebar).toBe(false)
expect(typeof sidebar.apply).toBe('function')
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(sidebar) as Record<string, unknown>
expect(unwrapped).toBe(sidebar)
expect(unwrapped.name).toBe('dsh-better-sidebar')
expect(unwrapped.inject).toEqual(['webServer', 'sessions', 'webRuntime', 'tools'])
expect(unwrapped.Config).toBeDefined()
expect(typeof unwrapped.apply).toBe('function')
})
it('exports the schemastery Config with the documented tunable fields', () => {
const schema = sidebar.Config
expect(schema).toBeDefined()
// The resolved defaults mirror the pre-config constants.
const resolved = (schema as unknown as {
(input: Record<string, unknown> | undefined): Record<string, unknown>
})(undefined)
expect(resolved.readLimit).toBe(512 * 1024)
expect(resolved.mediaLimit).toBe(20 * 1024 * 1024)
expect(resolved.listLimit).toBe(1000)
const configured = (schema as unknown as {
(input: Record<string, unknown> | undefined): Record<string, unknown>
})({ readLimit: 1024 })
expect(configured.readLimit).toBe(1024)
})
it('registers the side card preferences schema with the documented defaults', async () => {
const { PrefsSchema, SIDEBAR_PREFS_NS, SIDEBAR_PREFS_DEFAULTS } = await import('../src/config.ts')
expect(SIDEBAR_PREFS_NS).toBe('dsh-better-sidebar')
const resolved = (PrefsSchema as unknown as {
(input: Record<string, unknown> | undefined): Record<string, unknown>
})(undefined)
expect(resolved.openByDefault).toBeUndefined()
expect(resolved.defaultWidthPercent).toBeUndefined()
expect(resolved.changesDiffFloat).toBeUndefined()
expect(resolved.autoOpenSubagent).toBe(true)
// A new background job auto-opens the Jobs page too.
expect(resolved.autoOpenJobs).toBe(true)
// The sidebar-open tool defaults OFF (dormant until the user enables it
// in the side card settings).
expect(resolved.agentOpenTools).toBe(false)
// The position-compat scheme is declared WITHOUT a schema default so a
// stored document that predates it resolves without the field — the
// CLIENT parsePrefs then applies the conservative `auto` default (or
// migrates the legacy boolean), which is exactly what makes old
// documents migrate instead of silently flipping to a scheme. The
// legacy strip keeps its schema default of 40px.
expect(resolved.titleBarScheme).toBeUndefined()
expect(resolved.titleBarPresetId).toBeUndefined()
expect(resolved.customCss).toBeUndefined()
expect(resolved.titleBarCompat).toBe(false)
expect(resolved.titleBarStripPx).toBe(40)
// The enable-switch maps resolve to {} (everything on) for old documents.
expect(resolved.tabsEnabled).toEqual({})
expect(resolved.viewersEnabled).toEqual({})
// The separate file-window mode is the default (each file opens its own
// tab; the merged editor-explorer is opt-in).
expect(resolved.editorExplorer).toBe(false)
// The workspace fence (containment over the sidebar fs routes) defaults
// ON — the safe default never depends on the stored document.
expect(resolved.workspaceFence).toBe(true)
// A stored overridden value resolves through (the range contract is
// enforced by the settings service on write); the new pref keeps its
// default when the stored document predates it.
const overridden = (PrefsSchema as unknown as {
(input: Record<string, unknown> | undefined): Record<string, unknown>
})({ openByDefault: false, defaultWidthPercent: 45, changesDiffFloat: true })
// Schemastery's object schema is OPEN: a document written by an older
// plugin version still carrying the retired keys resolves them through
// verbatim. They are inert — the typed value the client consumes
// (parsePrefs) drops them (tests/prefs.spec.ts) — and the defaults no
// longer declare them.
// titleBarScheme / titleBarPresetId / customCss are declared WITHOUT a
// schema default (the client's parsePrefs supplies them), so they are
// absent from a resolved document that never stored them.
const { titleBarScheme, titleBarPresetId, customCss, ...schemaDefaults } = SIDEBAR_PREFS_DEFAULTS
void titleBarScheme; void titleBarPresetId; void customCss
expect(overridden).toEqual({ ...schemaDefaults, openByDefault: false, defaultWidthPercent: 45, changesDiffFloat: true })
})
})