Files
deepseek-harness/packages/test-support/client-runtime/src/settings-scope.ts
T
Yichen Jiang 1570eeeaee feat(web): manage plugins and per-preset settings from the browser
Add the Manage plugins tab (ui-settings-plugin-manager): package cards
with enable/disable switches, retry and uninstall, an install dialog
streaming pnpm output, and the selected preset's composition with per-row
switches and removal; the read-only list gains provenance facts.

Settings cards edit one scope at a time: ui-settings keeps one describe
mirror per settings scope (SettingsMirrorRegistry) and binds
{ namespace, scope }; ui-settings-plugins stages forms per scope, marks
inherited fields, and adds the skill-filesystem roots card.

Fixes surfaced by driving the browser end to end: the client gateway now
lets a call omit trailing parameters that accept undefined (PR3's
optional scope arguments failed the strict arity check), the plugins
install verb is `add` because the client namespace service reserves
`install`/`remove` (guarded by remote-method-names.host.spec), a plugin
module offers its main export as an addable `.`, and describing a settings
scope no owner registered resolves over the global instance's base and
never throws.

The web e2e scaffold can mount a profile runtime over fixture packages;
plugin-manager.e2e drives the tab and writes one field under a preset
scope; plugin-config and settings-chrome goldens are refreshed.
2026-09-04 04:32:31 +08:00

63 lines
2.1 KiB
TypeScript

/** Test double for the client settings-scope seam. */
import { vi } from 'vitest'
import type {
SettingsScope, SettingsScopeSnapshot,
} from '@deepseek-ai/dsh-client-ui-settings/client'
/** Handle over one stubbed scope: the scope, its write spy, and publication controls. */
export interface StubSettingsScope<T> {
/** The scope face handed to the service under test. */
scope: SettingsScope<T>
/** Spy behind `scope.set`; resolves immediately. */
set: ReturnType<typeof vi.fn>
/** Spy behind `scope.mutate`; resolves immediately. */
mutate: ReturnType<typeof vi.fn>
/** Spy behind `scope.unset`; resolves immediately. */
unset: ReturnType<typeof vi.fn>
/** @returns how many listeners are currently subscribed (disposal assertions). */
listenerCount(): number
/**
* Replace part of the snapshot and notify subscribers, as a Host
* acceptance would.
* @param next - snapshot fields to replace.
*/
publish(next: Partial<SettingsScopeSnapshot<T>>): void
}
/**
* Build an in-memory settings scope for service specs: starts in the host
* loading state, records writes, and lets the test publish Host acceptances.
* @returns the stub handle.
*/
export function stubSettingsScope<T>(): StubSettingsScope<T> {
let snapshot: SettingsScopeSnapshot<T> = {
status: 'loading', value: undefined, base: undefined, user: undefined,
revision: undefined, writable: false, mode: 'host',
scope: undefined, registered: false, inherited: undefined,
}
const listeners = new Set<() => void>()
const set = vi.fn(() => Promise.resolve())
const mutate = vi.fn(() => Promise.resolve())
const unset = vi.fn(() => Promise.resolve())
return {
scope: {
getSnapshot: () => snapshot,
subscribe: (listener) => {
listeners.add(listener)
return () => { listeners.delete(listener) }
},
mutate,
set,
unset,
},
set,
mutate,
unset,
listenerCount: () => listeners.size,
publish: (next) => {
snapshot = { ...snapshot, ...next }
for (const listener of [...listeners]) listener()
},
}
}