mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
A first visit resolved to Chinese regardless of the browser: LocaleService read `dsh.locale` and fell straight back to `zh` when nothing was stored, ignoring the languages the browser already states it reads. The initial locale now resolves through three ordered sources — the persisted preference, then `navigator` (first entry of the ordered language list whose primary subtag names a shipped locale, so `zh-Hans-CN` -> zh and `en-GB` -> en), then `FALLBACK_LOCALE`. An explicit choice still wins and nothing writes the detected locale back to storage, so "has the user chosen?" stays a question only the stored value answers. Specs asserting the shipped Chinese copy now state the browser they assume: the web e2e scenarios open their page with `locale: ZH_BROWSER_LOCALE`, and package specs pin it through the new `pinBrowserLanguages` test helper. `settings-chrome.e2e.ts` gains an English-browser scenario as the assembled-app proof.
120 lines
6.2 KiB
TypeScript
120 lines
6.2 KiB
TypeScript
// @vitest-environment jsdom
|
|
// apply wiring: the conversation service provided, the chat view registered
|
|
// as the first 'conversation.view' ring entry declaring the keyed toolview
|
|
// hole, the slot registrations land against a root entry's children
|
|
// declarations (the AppFrame role), the shared store handle rides all strict
|
|
// session entries, and the bash sample + todo row mount through the
|
|
// load-order seam as keyed entries. Full-chain rendering belongs to the
|
|
// machinery spec (chat-toolview-slot.spec.tsx) and the shell e2e; this spec
|
|
// stops at the assembly surface.
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { SlotTestRuntime, pinBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
|
|
import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
|
|
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
|
|
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
|
import { apply, inject } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
|
|
// The service reads its initial locale from the browser; these specs assert
|
|
// the shipped Chinese copy, so they state the browser they assume.
|
|
let restoreLanguages: () => void
|
|
beforeEach(() => { restoreLanguages = pinBrowserLanguages('zh-CN') })
|
|
afterEach(() => { restoreLanguages() })
|
|
|
|
const ROOT = 'root-1' as SessionId
|
|
const CHILD = 'child-1' as SessionId
|
|
|
|
async function bench() {
|
|
const runtime = await SlotTestRuntime.create()
|
|
await runtime.sessions.add({ id: ROOT, summary: { title: 'R', displayTitle: 'R' } }, { current: false })
|
|
await runtime.sessions.add(
|
|
{ id: CHILD, summary: { title: 'C', displayTitle: 'C', parentId: ROOT } }, { current: false })
|
|
runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
|
|
const locale = new LocaleService(runtime.ctx)
|
|
runtime.provide('locale', locale)
|
|
runtime.slots.installLocale(locale)
|
|
|
|
// Declared by ui-layout's root entry in production; the test root declares
|
|
// them here so the contributions land.
|
|
await runtime.root.declare({
|
|
'conversation': { kind: 'single', scope: 'session-maybe' },
|
|
'details': { kind: 'single', scope: 'session' },
|
|
}, (_p: { renderSlot?: unknown }) => null)
|
|
|
|
const feature = await runtime.mount({ inject: [...inject], apply })
|
|
return { runtime, feature, slots: runtime.slots }
|
|
}
|
|
|
|
/** First stored entry for a key (inject/store live directly on StoredEntry). */
|
|
function renderEntryOf(slots: Awaited<ReturnType<typeof bench>>['slots'], key: 'conversation' | 'conversation.session' | 'conversation.view' | 'details') {
|
|
return slots.entries(key)[0] as undefined | { inject?: unknown; store?: unknown }
|
|
}
|
|
|
|
describe('apply wiring', () => {
|
|
it('provides the conversation service', async () => {
|
|
const b = await bench()
|
|
expect(b.runtime.ctx.get('conversation')).toBeDefined()
|
|
await b.runtime.dispose()
|
|
})
|
|
|
|
it('registers the chat view as the first ring entry, declaring the keyed toolview hole', async () => {
|
|
const b = await bench()
|
|
const entries = b.slots.entries('conversation.view')
|
|
expect(entries.map(e => e.options.id)).toEqual(['chat'])
|
|
// Label is a locale thunk resolving through the zh dictionary.
|
|
expect(resolveSlotLabel(entries[0]?.options.label)).toBe('对话')
|
|
expect(entries[0]?.options.order).toBe(0)
|
|
// Declaring is claiming: the chat entry's registration put the hole on
|
|
// the ledger with the contract's kind/scope.
|
|
expect(b.slots.spec('conversation.chat.toolview')).toEqual({ kind: 'keyed', scope: 'session' })
|
|
await b.runtime.dispose()
|
|
})
|
|
|
|
it('occupies the slots + the ring; session entries share one store handle', async () => {
|
|
const b = await bench()
|
|
const conversation = renderEntryOf(b.slots, 'conversation')
|
|
const conversationSession = renderEntryOf(b.slots, 'conversation.session')
|
|
const chatView = renderEntryOf(b.slots, 'conversation.view')
|
|
const details = renderEntryOf(b.slots, 'details')
|
|
expect(conversation?.inject).toBeTypeOf('function')
|
|
expect(chatView?.inject).toBeTypeOf('function')
|
|
expect(details?.inject).toBeTypeOf('function')
|
|
// The shared handle: one apply-built store value on ALL session entries
|
|
// (the session-maybe 'conversation' shell carries no store by design).
|
|
expect(conversationSession?.store).toBeDefined()
|
|
expect(details?.store).toBe(conversationSession?.store)
|
|
expect(chatView?.store).toBe(conversationSession?.store)
|
|
// The hero workspace picker hole rides the conversation entry's children
|
|
// declaration (the empty-state occupant is gone).
|
|
expect(b.slots.spec('conversation.hero.workspace')).toEqual({ kind: 'single', scope: 'root' })
|
|
await b.runtime.dispose()
|
|
})
|
|
|
|
it('mounts the bash sample, the file-mutation rows, the web rows, and the product rows as keyed entries through the load-order seam', async () => {
|
|
const b = await bench()
|
|
// Every registrant plugin's inject: ['slots', 'conversation'] resolved — the
|
|
// service being present implies the chat entry declared the hole first. The
|
|
// file-mutation registrant claims both write and edit for the diff card; the
|
|
// web rows register one component under both web tool names.
|
|
const entries = b.slots.entries('conversation.chat.toolview')
|
|
expect(entries.map(e => e.options.key)).toEqual(['bash', 'edit', 'write', 'web_search', 'web_fetch', 'todo_write', 'ask_user_question'])
|
|
// Stats stick with the composer (not inside ChatView).
|
|
expect(b.slots.entries('conversation.composer.dock').map(e => e.options.id)).toEqual(['stats'])
|
|
await b.runtime.dispose()
|
|
})
|
|
|
|
it('plugin fiber disposal collects every registration (unload cascade, ring and hole included)', async () => {
|
|
const b = await bench()
|
|
await b.feature.dispose()
|
|
expect(b.slots.entries('conversation')).toHaveLength(0)
|
|
// The declared ring collapses with its declaring entry, and the chat
|
|
// entry's keyed hole (with the sample's registration) collapses with it.
|
|
expect(b.slots.entries('conversation.view')).toHaveLength(0)
|
|
expect(b.slots.entries('conversation.chat.toolview')).toHaveLength(0)
|
|
expect(b.slots.spec('conversation.chat.toolview')).toBeUndefined()
|
|
expect(b.slots.entries('details')).toHaveLength(0)
|
|
expect(b.runtime.ctx.get('conversation')).toBeUndefined()
|
|
await b.runtime.dispose()
|
|
})
|
|
})
|