Files
deepseek-harness/packages/client/ui-input-trigger/tests/apply.client.spec.ts
T
Chinesezjc 9de06952ab fix(locale): persist an explicit pick of the provisional locale
setLocale returned early when the id already matched the active locale, so
choosing the language already on screen wrote nothing. That value may be a
provisional browser-derived or fallback resolution nothing has stored, so a
different browser sharing the DSH home still resolved on its own. Write
unconditionally; keep the render publish conditional.

Broaden the dictionary parity gate to every workspace package, pair zh/en
across sibling files and inline registrations, and fail when a dictionary has
no counterpart. It previously scanned only packages/client and packages/
extensions, compared within a single module, and silently skipped unpaired
dictionaries -- so the split locales/zh.ts + en.ts common pair, the inline
directory-picker-browse dictionary, and session-log-export were unchecked.
Normalize paths at ingestion so the sweep does not narrow on Windows.

Regenerate the client API catalog and update the locale README pair: both
described the old zh fallback direction.

Add the English fallback dialog golden, and drop a dead afterEach plus the
blank lines left where the dead browser-language pins were removed.
2026-08-18 12:18:03 +08:00

104 lines
4.8 KiB
TypeScript

/**
* apply wiring on a real cordis Context + SlotRegistry: InputTriggerService mounts
* as ctx.inputTriggers once its sessions dependency is up; the MenuView overlay
* registration follows the slot declaration, resolves the per-session controller from the slot's
* sessionId, and unregisters on fiber teardown.
*/
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it } from 'vitest'
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
import { createScope, scopeOf, SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
import { apply, inject, InputTriggerService } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
import type { MenuViewInjected } from '@deepseek-ai/dsh-client-ui-input-trigger/client'
const sid = (k: string): SessionId => k as SessionId
async function bench() {
const ctx = new Context()
await ctx.plugin(SlotRegistry).await()
const slots = ctx.get('slots') as SlotRegistry
// Stand-in for the ui-conversation composer entry: declare the overlay
// slot without providing ConversationController, which is not its lifecycle
// signal.
slots.register(
{ name: 'root', children: { 'conversation.input.overlay': { kind: 'list', scope: 'session' } } } as never,
() => null,
)
// Sessions face: mint one real scope for session 'a' and resolve it by id.
const scope = createScope(ctx, sid('a'))
ctx.provide('sessions', {
scope: (id: SessionId) => (id === sid('a') ? scope.ctx : undefined),
scopeOf: (c: Context) => scopeOf(c),
})
const locale = new LocaleRuntime(ctx)
// These specs assert the shipped Chinese copy. There is no jsdom `window`
// in this lane, so browser-language detection never runs and the locale
// comes from FALLBACK_LOCALE (en): state the asserted locale explicitly.
locale.setLocale('zh')
ctx.provide('locale', locale)
return { ctx, slots, locale }
}
describe('apply', () => {
it('declares the sessions and locale dependencies (scope tree + localized menu copy)', () => {
expect(inject).toEqual(['sessions', 'locale'])
})
it('registers the bilingual menu dictionaries (group titles by source name + the pending row)', async () => {
const { ctx, locale } = await bench()
await ctx.plugin({ inject: [...inject], apply }).await()
const t = locale.bind('slash.menu')
expect(t('command')).toBe('命令')
locale.setLocale('en')
expect(t('skill')).toBe('Skills')
expect(t('subagent')).toBe('Subagents')
expect(t('loading')).toBe('Loading…')
})
it('mounts ctx.inputTriggers once sessions is up, before any conversation service exists', async () => {
const { ctx } = await bench()
await ctx.plugin({ inject: [...inject], apply }).await()
expect(ctx.get('inputTriggers')).toBeInstanceOf(InputTriggerService)
})
it('registers MenuView into the overlay and resolves the per-session controller by slot sessionId', async () => {
const { ctx, slots } = await bench()
await ctx.plugin({ inject: [...inject], apply }).await()
expect(slots.entries('conversation.input.overlay')).toHaveLength(1)
const entries = slots.entries('conversation.input.overlay')
expect(entries[0]!.options.id).toBe('slash-menu')
// Copy rides the standard locale seat, not the business face.
expect(entries[0]!.locale).toBe('slash.menu')
const inputTriggers = ctx.get('inputTriggers') as InputTriggerService
// StoredEntry.inject is declaration-typed ((...args: never[]) shape);
// the erased registration widens it past a direct cast, so hop unknown.
const injectEntry = entries[0]!.inject as unknown as (sessionId: SessionId) => MenuViewInjected
const injected = injectEntry(sid('a'))
const controller = inputTriggers.sessionOf(
(ctx.get('sessions') as { scope(id: SessionId): Context }).scope(sid('a')),
)
expect(injected.menu).toBe(controller.menu)
// The pick face routes into the controller pipeline (closed menu → no-op).
injected.onPick('command', 0)
expect(controller.menu.getSnapshot().open).toBe(false)
// The dismiss face routes into the controller too (closed menu → no-op).
injected.onDismiss()
expect(controller.menu.getSnapshot().open).toBe(false)
// An unknown session id fails loud (no silent scope miss).
expect(() => injectEntry(sid('ghost'))).toThrow(/resolved no scope/)
})
it('fiber teardown removes the overlay entry', async () => {
const { ctx, slots } = await bench()
const fiber = ctx.plugin({ inject: [...inject], apply })
await fiber.await()
expect(slots.entries('conversation.input.overlay')).toHaveLength(1)
await fiber.dispose()
expect(slots.entries('conversation.input.overlay')).toHaveLength(0)
expect(ctx.get('inputTriggers')).toBeUndefined()
})
})