chore(gen1): 第一代版本封板 — DSH 2.0.17 兼容 + settings API 迁移 + React #130 修复
- peerDeps 升级到 0.2.0-rc.2 - settings API 迁移: register() → describe() + settings/document-updated - turnTail slots.register 补 id 字段 - EditorHost viewer.component undefined guard - 56/56 测试全绿, typecheck/build 通过 tag: v1.2.0-gen1
This commit is contained in:
+75
-49
@@ -11,6 +11,8 @@ import { join, resolve as resolvePath } from 'node:path'
|
||||
import { SettingsConflictError, type SettingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import { apply, mediaTypeForPath, wsCloseReasonOf } from '../src/index.ts'
|
||||
import { SidebarError } from '../src/wire.ts'
|
||||
import { PrefsSchema } from '../src/config.ts'
|
||||
import { SIDEBAR_PREFS_DEFAULTS } from '../src/prefs-shared.ts'
|
||||
import { encodeHtmlUrl } from '../src/html-route.ts'
|
||||
import * as git from '../src/git.ts'
|
||||
import { listDirectory } from '../src/fs-tree.ts'
|
||||
@@ -828,29 +830,31 @@ describe('session cwd resolution over the API route', () => {
|
||||
})
|
||||
|
||||
describe('side card settings routes', () => {
|
||||
/** A minimal settings seam: register/describe/update with the revision guard. */
|
||||
/** A minimal settings seam: describe/update with the revision guard (0.2.0-rc.2 API). */
|
||||
const createFakeSettings = (pre?: Record<string, Record<string, unknown>>) => {
|
||||
const namespaces = new Map<string, {
|
||||
schema: unknown
|
||||
value: Record<string, unknown> | undefined
|
||||
revision: number
|
||||
}>()
|
||||
// 0.2.0-rc.2: the plugin's Config schema is auto-registered by the loader.
|
||||
// The mock pre-seeds the sidebar namespace so describe() returns it even
|
||||
// when the plugin no longer calls register() explicitly.
|
||||
if (!pre || !('dsh-apes-sidebar' in pre)) {
|
||||
// Use SIDEBAR_PREFS_DEFAULTS as the resolved value for the sidebar ns:
|
||||
// the real SettingsForms service applies the Config schema defaults
|
||||
// when no stored value exists, which is exactly what this constant is.
|
||||
namespaces.set('dsh-apes-sidebar', { schema: (input: unknown) => input ?? SIDEBAR_PREFS_DEFAULTS, value: undefined, revision: 0 })
|
||||
}
|
||||
for (const [ns, value] of Object.entries(pre ?? {})) {
|
||||
namespaces.set(ns, { schema: (input: unknown) => input, value, revision: 0 })
|
||||
namespaces.set(ns, { schema: (input: unknown) => input ?? {}, value, revision: 0 })
|
||||
}
|
||||
const resolve = (entry: { schema: unknown; value: Record<string, unknown> | undefined }): unknown => {
|
||||
const schema = entry.schema as (input: unknown) => unknown
|
||||
return entry.value === undefined ? schema(undefined) : schema(entry.value)
|
||||
}
|
||||
return {
|
||||
register(ns: string, schema: unknown) {
|
||||
// Preserve a pre-seeded value: tests stage prefs through the `pre`
|
||||
// map before the plugin mounts and registers the same namespace.
|
||||
const existing = namespaces.get(ns)
|
||||
namespaces.set(ns, { schema, value: existing?.value ?? undefined, revision: 0 })
|
||||
return { get: () => ({}), watch: () => () => {}, update: async () => {}, replace: async () => {} }
|
||||
},
|
||||
describe() {
|
||||
describe(_options?: { redactSecrets?: boolean }) {
|
||||
return [...namespaces.entries()].map(([ns, entry]) => ({
|
||||
ns,
|
||||
value: resolve(entry),
|
||||
@@ -864,7 +868,9 @@ describe('side card settings routes', () => {
|
||||
if (expectedRevision !== undefined && expectedRevision !== entry.revision) {
|
||||
throw new SettingsConflictError(ns as SettingsNamespace, expectedRevision, entry.revision)
|
||||
}
|
||||
entry.value = { ...entry.value, ...patch }
|
||||
// The real SettingsForms re-applies the full schema (with defaults)
|
||||
// on every update, so the stored value is always a complete object.
|
||||
entry.value = { ...SIDEBAR_PREFS_DEFAULTS, ...entry.value, ...patch }
|
||||
entry.revision += 1
|
||||
},
|
||||
}
|
||||
@@ -872,6 +878,9 @@ describe('side card settings routes', () => {
|
||||
|
||||
const mountWithSettings = (settings?: unknown): SidebarWebRoute => {
|
||||
const routes: SidebarWebRoute[] = []
|
||||
// Event bus: the 0.2.0-rc.2 settings service emits
|
||||
// `settings/document-updated` on the plugin context after each update.
|
||||
const eventHandlers = new Map<string, Array<(...args: unknown[]) => void>>()
|
||||
const ctx = {
|
||||
webRuntime: { trustedHosts: [] },
|
||||
webServer: {
|
||||
@@ -882,11 +891,30 @@ describe('side card settings routes', () => {
|
||||
tools: { register: () => () => {} },
|
||||
effect: (fn: () => void | (() => void)) => { fn() },
|
||||
inject: (deps: string[], callback: (sctx: { settings: unknown }) => void) => {
|
||||
if (deps.includes('settings') && settings !== undefined) callback({ settings })
|
||||
if (deps.includes('settings') && settings !== undefined) {
|
||||
const s = settings as { update: Function; describe: Function }
|
||||
// Wrap update to emit `settings/document-updated` after each write,
|
||||
// mirroring the real SettingsForms service behavior.
|
||||
const origUpdate = s.update.bind(s)
|
||||
s.update = async (ns: string, ...rest: unknown[]) => {
|
||||
await origUpdate(ns, ...rest)
|
||||
const handlers = eventHandlers.get('settings/document-updated') ?? []
|
||||
for (const h of handlers) h(ns)
|
||||
}
|
||||
callback({ settings: s })
|
||||
}
|
||||
return () => {}
|
||||
},
|
||||
// The session/agent event feeds: nothing emits in these tests.
|
||||
on: () => () => {},
|
||||
on: (event: string, handler: (...args: unknown[]) => void) => {
|
||||
const existing = eventHandlers.get(event) ?? []
|
||||
existing.push(handler)
|
||||
eventHandlers.set(event, existing)
|
||||
return () => {
|
||||
const handlers = eventHandlers.get(event) ?? []
|
||||
const idx = handlers.indexOf(handler)
|
||||
if (idx >= 0) handlers.splice(idx, 1)
|
||||
}
|
||||
},
|
||||
// No jobs/agents services: the jobs routes degrade to a 503.
|
||||
get: () => undefined,
|
||||
}
|
||||
@@ -1002,6 +1030,9 @@ describe('side card settings routes', () => {
|
||||
workspaceFence: true,
|
||||
terminalShell: '',
|
||||
terminalShellArgs: '',
|
||||
titleBarScheme: 'auto',
|
||||
titleBarPresetId: '',
|
||||
customCss: '',
|
||||
titleBarCompat: false,
|
||||
titleBarStripPx: 40,
|
||||
htmlViewerNoSandbox: false,
|
||||
@@ -1144,23 +1175,18 @@ describe('agent terminal tool gating', () => {
|
||||
it('injects the eight tools only when the side-card setting is enabled (default off)', () => {
|
||||
let registered = 0
|
||||
let disposed = 0
|
||||
// The tools currently registered (registered minus disposed).
|
||||
const live = (): number => registered - disposed
|
||||
// A ref container: the watch callback is only assigned inside a closure,
|
||||
// which TypeScript's control-flow analysis ignores (the bare variable
|
||||
// would narrow to null and refuse the optional call).
|
||||
const watcherRef: { current: (() => void) | null } = { current: null }
|
||||
// 0.2.0-rc.2: no more register()/watch(). The plugin reads settings via
|
||||
// describe() and listens to `settings/document-updated` on ctx.on().
|
||||
let enabled = false
|
||||
const handlerRef: { current: ((ns: string) => void) | null } = { current: null }
|
||||
const settings = {
|
||||
register() {
|
||||
return {
|
||||
get: () => ({ agentTerminalTools: enabled }),
|
||||
watch: (callback: () => void) => { watcherRef.current = callback; return () => {} },
|
||||
update: async () => {},
|
||||
replace: async () => {},
|
||||
}
|
||||
},
|
||||
describe: () => [],
|
||||
describe: () => [{
|
||||
ns: 'dsh-apes-sidebar',
|
||||
value: { ...SIDEBAR_PREFS_DEFAULTS, agentTerminalTools: enabled },
|
||||
applies: 'live' as const,
|
||||
revision: 0,
|
||||
}],
|
||||
async update() {},
|
||||
}
|
||||
const ctx = {
|
||||
@@ -1176,9 +1202,10 @@ describe('agent terminal tool gating', () => {
|
||||
if (deps.includes('settings')) callback({ settings })
|
||||
return () => {}
|
||||
},
|
||||
// The session/agent event feeds: nothing emits in these tests.
|
||||
on: () => () => {},
|
||||
// No jobs/agents services: the jobs routes degrade to a 503.
|
||||
on: (event: string, handler: (...args: unknown[]) => void) => {
|
||||
if (event === 'settings/document-updated') handlerRef.current = handler as (ns: string) => void
|
||||
return () => { handlerRef.current = null }
|
||||
},
|
||||
get: () => undefined,
|
||||
}
|
||||
apply(ctx as never)
|
||||
@@ -1186,18 +1213,18 @@ describe('agent terminal tool gating', () => {
|
||||
expect(live()).toBe(0)
|
||||
// Flipping the setting on registers all eight tools.
|
||||
enabled = true
|
||||
watcherRef.current?.()
|
||||
handlerRef.current?.('dsh-apes-sidebar')
|
||||
expect(live()).toBe(8)
|
||||
expect(disposed).toBe(0)
|
||||
// Flipping it back off unregisters them (and releases any agent terminals).
|
||||
enabled = false
|
||||
watcherRef.current?.()
|
||||
handlerRef.current?.('dsh-apes-sidebar')
|
||||
expect(live()).toBe(0)
|
||||
expect(disposed).toBe(8)
|
||||
// And a redundant toggle registers them fresh (no double-registration per
|
||||
// flip: the guard only skips when the tools are already live).
|
||||
enabled = true
|
||||
watcherRef.current?.()
|
||||
handlerRef.current?.('dsh-apes-sidebar')
|
||||
expect(live()).toBe(8)
|
||||
expect(registered).toBe(16)
|
||||
})
|
||||
@@ -1208,18 +1235,15 @@ describe('agent sidebar-open tool gating', () => {
|
||||
let registered = 0
|
||||
let disposed = 0
|
||||
const live = (): number => registered - disposed
|
||||
const watcherRef: { current: (() => void) | null } = { current: null }
|
||||
let enabled = false
|
||||
const handlerRef: { current: ((ns: string) => void) | null } = { current: null }
|
||||
const settings = {
|
||||
register() {
|
||||
return {
|
||||
get: () => ({ agentOpenTools: enabled, tabsEnabled: {} }),
|
||||
watch: (callback: () => void) => { watcherRef.current = callback; return () => {} },
|
||||
update: async () => {},
|
||||
replace: async () => {},
|
||||
}
|
||||
},
|
||||
describe: () => [],
|
||||
describe: () => [{
|
||||
ns: 'dsh-apes-sidebar',
|
||||
value: { ...SIDEBAR_PREFS_DEFAULTS, agentOpenTools: enabled },
|
||||
applies: 'live' as const,
|
||||
revision: 0,
|
||||
}],
|
||||
async update() {},
|
||||
}
|
||||
const ctx = {
|
||||
@@ -1235,8 +1259,10 @@ describe('agent sidebar-open tool gating', () => {
|
||||
if (deps.includes('settings')) callback({ settings })
|
||||
return () => {}
|
||||
},
|
||||
// The session/agent event feeds: nothing emits in these tests.
|
||||
on: () => () => {},
|
||||
on: (event: string, handler: (...args: unknown[]) => void) => {
|
||||
if (event === 'settings/document-updated') handlerRef.current = handler as (ns: string) => void
|
||||
return () => { handlerRef.current = null }
|
||||
},
|
||||
get: () => undefined,
|
||||
}
|
||||
apply(ctx as never)
|
||||
@@ -1244,17 +1270,17 @@ describe('agent sidebar-open tool gating', () => {
|
||||
expect(live()).toBe(0)
|
||||
// Flipping the setting on registers the single sidebar_open tool.
|
||||
enabled = true
|
||||
watcherRef.current?.()
|
||||
handlerRef.current?.('dsh-apes-sidebar')
|
||||
expect(live()).toBe(1)
|
||||
expect(disposed).toBe(0)
|
||||
// Flipping it back off unregisters it (and drains the undelivered queue).
|
||||
enabled = false
|
||||
watcherRef.current?.()
|
||||
handlerRef.current?.('dsh-apes-sidebar')
|
||||
expect(live()).toBe(0)
|
||||
expect(disposed).toBe(1)
|
||||
// And a redundant toggle registers it fresh (no double-registration).
|
||||
enabled = true
|
||||
watcherRef.current?.()
|
||||
handlerRef.current?.('dsh-apes-sidebar')
|
||||
expect(live()).toBe(1)
|
||||
expect(registered).toBe(2)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user