|
|
@@ -11,6 +11,7 @@ import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
|
|
|
import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
|
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
|
|
|
import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
|
|
|
+import { SessionId } from '@deepseek-ai/dsh-session'
|
|
|
import { apply as settingsApply, inject as settingsInject } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
|
import { apply, inject } from '@deepseek-ai/dsh-client-ui-agent-preset/client'
|
|
|
import { AgentPresetLabel } from '../src/client/AgentPresetLabel.tsx'
|
|
|
@@ -21,6 +22,7 @@ import { AgentPresetSection } from '../src/client/AgentPresetSection.tsx'
|
|
|
import type { AgentPresetSectionInjected } from '../src/client/AgentPresetSection.tsx'
|
|
|
import { AgentPresetSeat } from '../src/client/AgentPresetSeat.tsx'
|
|
|
import type { AgentPresetSeatInjected } from '../src/client/AgentPresetSeat.tsx'
|
|
|
+import { AgentPresetSeatController } from '../src/client/seat-store.ts'
|
|
|
|
|
|
// These specs assert the shipped Chinese copy. The lane has no jsdom `window`,
|
|
|
// so browser-language detection never runs and a fresh LocaleRuntime opens on
|
|
|
@@ -155,7 +157,11 @@ function uiWorkspaceDouble() {
|
|
|
/** A sessions double whose list can be moved and whose changes are pushed. */
|
|
|
function sessionsDouble(state: {
|
|
|
current?: string
|
|
|
- byId: Record<string, { id: string; blank: boolean; agentPreset?: string }>
|
|
|
+ byId: Record<string, {
|
|
|
+ id: string
|
|
|
+ blank: boolean
|
|
|
+ projectionValues?: { agentPreset?: string | null }
|
|
|
+ }>
|
|
|
}) {
|
|
|
const listeners = new Set<() => void>()
|
|
|
return {
|
|
|
@@ -166,12 +172,6 @@ function sessionsDouble(state: {
|
|
|
return () => listeners.delete(fn)
|
|
|
},
|
|
|
},
|
|
|
- noteAgentPreset: (sessionId: string, agentPreset: string) => {
|
|
|
- const summary = state.byId[sessionId]
|
|
|
- if (summary === undefined || summary.agentPreset === agentPreset) return
|
|
|
- summary.agentPreset = agentPreset
|
|
|
- for (const fn of listeners) fn()
|
|
|
- },
|
|
|
/** Push a list change the way the runtime's store does. */
|
|
|
notify: () => { for (const fn of listeners) fn() },
|
|
|
}
|
|
|
@@ -354,24 +354,6 @@ describe('ui-agent-preset apply', () => {
|
|
|
conversation()
|
|
|
})
|
|
|
|
|
|
- it('folds a remote preset commit into the shared session row', async () => {
|
|
|
- const { ctx, slots, remote } = await bench()
|
|
|
- declareRoot(slots)
|
|
|
- declareConversation(slots)
|
|
|
- ctx.provide('conversation', {} as never)
|
|
|
- const state = {
|
|
|
- current: 's1',
|
|
|
- byId: { s1: { id: 's1', blank: true, agentPreset: 'standard' } },
|
|
|
- }
|
|
|
- ctx.provide('sessions', sessionsDouble(state) as never)
|
|
|
- ctx.provide('uiWorkspace', uiWorkspaceDouble() as never)
|
|
|
- await ctx.plugin({ inject: [...inject, 'conversation', 'sessions', 'uiWorkspace'], apply }).await()
|
|
|
-
|
|
|
- remote.emit('agent-preset/selected', ['s1', 'minimal'])
|
|
|
-
|
|
|
- expect(state.byId.s1.agentPreset).toBe('minimal')
|
|
|
- })
|
|
|
-
|
|
|
it('offers a just-authored preset on the new-session chip', async () => {
|
|
|
const { ctx, slots } = await bench()
|
|
|
declareRoot(slots)
|
|
|
@@ -409,7 +391,11 @@ describe('ui-agent-preset apply', () => {
|
|
|
ctx.provide('conversation', {} as never)
|
|
|
const state: {
|
|
|
current?: string
|
|
|
- byId: Record<string, { id: string; blank: boolean; agentPreset?: string }>
|
|
|
+ byId: Record<string, {
|
|
|
+ id: string
|
|
|
+ blank: boolean
|
|
|
+ projectionValues?: { agentPreset?: string | null }
|
|
|
+ }>
|
|
|
} = { byId: {} }
|
|
|
const sessions = sessionsDouble(state)
|
|
|
ctx.provide('sessions', sessions as never)
|
|
|
@@ -424,7 +410,9 @@ describe('ui-agent-preset apply', () => {
|
|
|
expect(calls).not.toContain('select:minimal')
|
|
|
|
|
|
state.current = 's1'
|
|
|
- state.byId['s1'] = { id: 's1', blank: true, agentPreset: 'standard' }
|
|
|
+ state.byId['s1'] = {
|
|
|
+ id: 's1', blank: true, projectionValues: { agentPreset: 'standard' },
|
|
|
+ }
|
|
|
sessions.notify()
|
|
|
|
|
|
// Connecting a workspace produced the session; the stage reaches it there.
|
|
|
@@ -461,7 +449,9 @@ describe('ui-agent-preset apply', () => {
|
|
|
ctx.provide('conversation', {} as never)
|
|
|
const state = {
|
|
|
current: 's1',
|
|
|
- byId: { s1: { id: 's1', blank: true, agentPreset: 'standard' } },
|
|
|
+ byId: {
|
|
|
+ s1: { id: 's1', blank: true, projectionValues: { agentPreset: 'standard' } },
|
|
|
+ },
|
|
|
}
|
|
|
const sessions = sessionsDouble(state)
|
|
|
ctx.provide('sessions', sessions as never)
|
|
|
@@ -543,7 +533,11 @@ describe('ui-agent-preset apply', () => {
|
|
|
ctx.provide('conversation', {} as never)
|
|
|
const state: {
|
|
|
current?: string
|
|
|
- byId: Record<string, { id: string; blank: boolean; agentPreset?: string }>
|
|
|
+ byId: Record<string, {
|
|
|
+ id: string
|
|
|
+ blank: boolean
|
|
|
+ projectionValues?: { agentPreset?: string | null }
|
|
|
+ }>
|
|
|
} = { byId: {} }
|
|
|
const sessions = sessionsDouble(state)
|
|
|
ctx.provide('sessions', sessions as never)
|
|
|
@@ -562,7 +556,9 @@ describe('ui-agent-preset apply', () => {
|
|
|
// The chip mounts with the flow's session, so its roster load can land
|
|
|
// AFTER the stage was consumed; the session's own composition is what
|
|
|
// the display must keep — not the deployment default.
|
|
|
- state.byId['s1'] = { id: 's1', blank: true, agentPreset: 'cordis' }
|
|
|
+ state.byId['s1'] = {
|
|
|
+ id: 's1', blank: true, projectionValues: { agentPreset: 'cordis' },
|
|
|
+ }
|
|
|
await seat.load()
|
|
|
|
|
|
expect(seat.hooks.agentPresetSeat.getSnapshot().current).toBe('cordis')
|
|
|
@@ -581,3 +577,48 @@ describe('ui-agent-preset apply', () => {
|
|
|
expect(section.startCreatorDraft).toBeUndefined()
|
|
|
})
|
|
|
})
|
|
|
+
|
|
|
+describe('AgentPresetSeatController reconciliation', () => {
|
|
|
+ it('uses the deployment default without a Session and clears it for an uncomposed Session', async () => {
|
|
|
+ const state: { current?: { id: SessionId; blank: boolean } } = {}
|
|
|
+ const controller = new AgentPresetSeatController({
|
|
|
+ agentPresets: {
|
|
|
+ list: () => Promise.resolve(ROSTER_ONE),
|
|
|
+ },
|
|
|
+ } as never, () => state.current)
|
|
|
+
|
|
|
+ await controller.load()
|
|
|
+ await controller.apply()
|
|
|
+ expect(controller.store.getSnapshot().current).toBe('standard')
|
|
|
+
|
|
|
+ state.current = { id: SessionId('uncomposed'), blank: true }
|
|
|
+ await controller.apply()
|
|
|
+ expect(controller.store.getSnapshot().current).toBe('')
|
|
|
+ })
|
|
|
+
|
|
|
+ it.each([
|
|
|
+ {
|
|
|
+ name: 'RPC rejection',
|
|
|
+ select: () => Promise.resolve({
|
|
|
+ rpcId: 'r',
|
|
|
+ result: { ok: false as const, error: { code: 'failed', message: 'selection rejected', details: {} } },
|
|
|
+ }),
|
|
|
+ message: 'selection rejected',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: 'transport failure',
|
|
|
+ select: () => Promise.reject(new Error('transport failed')),
|
|
|
+ message: 'transport failed',
|
|
|
+ },
|
|
|
+ ])('restores an empty current value after $name for an uncomposed Session', async ({ select, message }) => {
|
|
|
+ const controller = new AgentPresetSeatController({
|
|
|
+ agentPresets: { select },
|
|
|
+ } as never, () => ({ id: SessionId('uncomposed'), blank: true }))
|
|
|
+
|
|
|
+ await controller.select('minimal')
|
|
|
+
|
|
|
+ expect(controller.store.getSnapshot()).toMatchObject({
|
|
|
+ busy: false, current: '', error: message,
|
|
|
+ })
|
|
|
+ })
|
|
|
+})
|