feat(client): inject slot declaration lifetimes

This commit is contained in:
imccyu
2026-08-05 23:38:29 +08:00
parent bb53e25ed0
commit 86965b053c
100 changed files with 1065 additions and 837 deletions
@@ -14,7 +14,7 @@ import { Context } from 'cordis'
import { describe, expect, it, vi } from 'vitest'
import { cleanup, render } from '@testing-library/react'
import { afterEach } from 'vitest'
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
import { SlotsService, type SessionId } from '@deepseek-ai/dsh-client-runtime/client'
import type { GoalProjection } from '@deepseek-ai/dsh-goal/client'
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
@@ -45,7 +45,7 @@ function makeProjection(revision = 3): GoalProjection {
}
/** Boot the plugin over fake faces; goals verbs record payloads and answer per the script. */
function bench(options: { projection?: GoalProjection | null | undefined; failWith?: { code: string; message: string } } = {}) {
async function bench(options: { projection?: GoalProjection | null | undefined; failWith?: { code: string; message: string } } = {}) {
const ctx = new Context()
const calls: { method: string; payload: unknown }[] = []
function answer<T>(method: string, value: T) {
@@ -65,14 +65,10 @@ function bench(options: { projection?: GoalProjection | null | undefined; failWi
resume: answer('goal.resume', { ref }),
clear: answer('goal.clear', { cleared: true as const }),
} } })
const entries = new Map<string, { id?: string; order?: number; locale?: string; inject?: (sessionId: SessionId) => GoalBarActions }>()
ctx.provide('slots', {
register(reg: { name: string; id?: string; order?: number; locale?: string; inject?: (sessionId: SessionId) => GoalBarActions }) {
entries.set(reg.name, reg)
return () => { entries.delete(reg.name) }
},
})
ctx.provide('conversation', {})
await ctx.plugin(SlotsService).await()
ctx.slots.register({
name: 'root', children: { 'conversation.input.dock': { kind: 'list', scope: 'session' } },
} as never, (() => null) as never)
ctx.provide('locale', new LocaleService(ctx))
ctx.provide('sessions', {
binding: (id: SessionId) => ({
@@ -89,20 +85,28 @@ function bench(options: { projection?: GoalProjection | null | undefined; failWi
ctx,
fiber,
calls,
entry: () => entries.get('conversation.input.dock'),
entry: () => {
const entry = ctx.slots.entries('conversation.input.dock')[0]
if (entry === undefined) return undefined
return {
...entry.options,
locale: entry.locale,
inject: entry.inject as unknown as ((sessionId: SessionId) => GoalBarActions) | undefined,
}
},
}
}
describe('ui-goal browser plugin', () => {
it('registers the GoalBar dock entry with the documented id and order', async () => {
const b = bench()
const b = await bench()
await b.fiber.await()
expect(b.entry()).toMatchObject({ id: 'goal', order: 10, locale: 'goal' })
expect(b.entry()?.inject).toBeTypeOf('function')
})
it('verbs read the CAS ref from the current projected value at call time', async () => {
const b = bench({ projection: makeProjection(5) })
const b = await bench({ projection: makeProjection(5) })
await b.fiber.await()
const verbs = b.entry()!.inject!(sid('s1'))
expect(await verbs.onEdit('New objective')).toEqual({ ok: true })
@@ -119,7 +123,7 @@ describe('ui-goal browser plugin', () => {
it('a null or absent projection short-circuits every verb without touching the wire', async () => {
for (const projection of [null, undefined]) {
const b = bench({ projection })
const b = await bench({ projection })
await b.fiber.await()
const verbs = b.entry()!.inject!(sid('s1'))
for (const result of [await verbs.onEdit('x'), await verbs.onPause(), await verbs.onResume(), await verbs.onClear()]) {
@@ -130,14 +134,14 @@ describe('ui-goal browser plugin', () => {
})
it('maps a settled RPC error onto the inline-render shape', async () => {
const b = bench({ projection: makeProjection(), failWith: { code: 'internal', message: 'stale revision' } })
const b = await bench({ projection: makeProjection(), failWith: { code: 'internal', message: 'stale revision' } })
await b.fiber.await()
const verbs = b.entry()!.inject!(sid('s1'))
expect(await verbs.onEdit('x')).toEqual({ ok: false, error: { code: 'internal', message: 'stale revision' } })
})
it('drops the dock entry when the plugin fiber unloads (HMR safety)', async () => {
const b = bench()
const b = await bench()
await b.fiber.await()
expect(b.entry()).toBeDefined()
await b.fiber.dispose()