The section knows no namespace: it declares `settings.plugin.item` and renders whatever cards were registered into it, so a plugin that ships a browser half owns its card and its controls. The three cards here cover the host-plane sections this deployment exposes. A field shows its effective value and, when the raw user layer carries it, an override badge and a reset that clears it back to the composition layer. Controls commit on blur and Enter rather than per keystroke, which would burn namespace revisions and race its own reads. The search key is the one value that never rides a response: the card reports only whether one is configured and writes it through the credentials domain, addressed by the reference the section names. A card renders nothing while its namespace is unavailable — a deployment that does not compose the owning plugin should show no trace of it rather than a disabled card the user cannot act on.
101 lines
3.9 KiB
TypeScript
101 lines
3.9 KiB
TypeScript
/** What the browser half registers, and that it all leaves with the fiber. */
|
|
|
|
import { Context } from 'cordis'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
|
|
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
|
|
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
|
|
import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
|
|
import { apply, inject } from '@deepseek-ai/dsh-client-ui-plugin-config/client'
|
|
|
|
// The service reads its initial locale from the browser; these specs assert
|
|
// the shipped Chinese copy, so they state the browser they assume.
|
|
usePinnedBrowserLanguages('zh-CN')
|
|
|
|
async function bench() {
|
|
const ctx = new Context()
|
|
await ctx.plugin(SlotsService).await()
|
|
const locale = new LocaleService(ctx)
|
|
ctx.provide('locale', locale)
|
|
ctx.provide('connection', {
|
|
isLoopback: true,
|
|
api: {
|
|
settings: { describe: vi.fn(() => Promise.resolve({ rpcId: 's', result: { ok: false, error: {} } })) },
|
|
credentials: { describe: vi.fn(() => Promise.resolve({ rpcId: 'c', result: { ok: false, error: {} } })) },
|
|
},
|
|
} as never)
|
|
return { ctx, slots: ctx.get('slots') as SlotsService }
|
|
}
|
|
|
|
function declareRoot(slots: SlotsService): () => void {
|
|
return slots.register({
|
|
name: 'root',
|
|
children: { 'settings.section': { kind: 'list', scope: 'root' } },
|
|
} as never, () => null)
|
|
}
|
|
|
|
describe('ui-plugin-config apply', () => {
|
|
it('declares the services it uses', () => {
|
|
expect(inject).toEqual(['slots', 'locale', 'connection'])
|
|
})
|
|
|
|
it('registers the section and declares the per-plugin card slot', async () => {
|
|
const { ctx, slots } = await bench()
|
|
declareRoot(slots)
|
|
|
|
await ctx.plugin({ inject: [...inject], apply }).await()
|
|
|
|
const section = slots.entries('settings.section')[0]!
|
|
expect(section.options).toMatchObject({ id: 'plugins', order: 30 })
|
|
// The nav label is a locale-following thunk; owners resolve it at read time.
|
|
expect(resolveSlotLabel(section.options.label)).toBe('插件')
|
|
expect(slots.spec('settings.plugin.item')).toMatchObject({ kind: 'list', scope: 'root' })
|
|
})
|
|
|
|
it('registers one card per host-plane section it ships, in a stable order', async () => {
|
|
const { ctx, slots } = await bench()
|
|
declareRoot(slots)
|
|
|
|
await ctx.plugin({ inject: [...inject], apply }).await()
|
|
|
|
expect(slots.entries('settings.plugin.item').map(entry => entry.options.id))
|
|
.toEqual(['bash', 'agent-loop', 'web-search'])
|
|
})
|
|
|
|
it('injects a live card count and one business face per card', async () => {
|
|
const { ctx, slots } = await bench()
|
|
declareRoot(slots)
|
|
await ctx.plugin({ inject: [...inject], apply }).await()
|
|
|
|
const section = slots.entries('settings.section')[0]!
|
|
expect((section as { inject?: () => unknown }).inject?.()).toEqual({ cardCount: 3 })
|
|
for (const entry of slots.entries('settings.plugin.item')) {
|
|
const face = (entry as { inject?: () => unknown }).inject?.() as { hooks: Record<string, unknown> }
|
|
// Each card injects exactly one snapshot store plus its own actions.
|
|
expect(Object.keys(face.hooks)).toHaveLength(1)
|
|
}
|
|
})
|
|
|
|
it('registers into a declaration that arrives after apply', async () => {
|
|
const { ctx, slots } = await bench()
|
|
await ctx.plugin({ inject: [...inject], apply }).await()
|
|
|
|
declareRoot(slots)
|
|
|
|
await vi.waitFor(() => { expect(slots.entries('settings.section')).toHaveLength(1) })
|
|
})
|
|
|
|
it('collapses every contribution on teardown', async () => {
|
|
const { ctx, slots } = await bench()
|
|
declareRoot(slots)
|
|
const fiber = ctx.plugin({ inject: [...inject], apply })
|
|
await fiber.await()
|
|
expect(slots.entries('settings.plugin.item')).toHaveLength(3)
|
|
|
|
await fiber.dispose()
|
|
|
|
expect(slots.entries('settings.section')).toHaveLength(0)
|
|
expect(slots.spec('settings.plugin.item')).toBeUndefined()
|
|
})
|
|
})
|