Files
deepseek-harness/packages/client/ui-theme/tests/appearance-row.spec.tsx
T
Yichen Jiang eab0aeb9db feat(web): list background tasks in the session header
The task registry has run every background bash, pwsh, pty-send, and
one-shot subagent since it landed, but only the model could read it: a
human at the Web client could not see that a build was running, tell a
finished task from a stuck one, or find its outcome anywhere but the
`run_in_background` tool card that printed an id and never updated.

Task state now reaches the browser as one whole-snapshot `session/tasks`
mux frame per session, pushed at every registry commit that changes what
that session can see. `TaskService` gains `onTasksChanged`, which is
owner-granular because owner-disposal removal is a change no per-task
record can express. The carrier reads the exact owner the listener hands
it, so a push stays correct while that scope tears down, and reads the
baseline through the non-resuming `ctx.agents.get` so listing never
revives a cold session. The client keeps a last-wins mirror on
`SessionListState`, and a new `dsh-client-ui-task` package renders it
beside the subagent catalog — rendering nothing at all until the session
has a task, so an ordinary conversation grows no new chrome.

Streamed per-task output and human-initiated cancellation are separate
phases; the note records why neither has to undo this channel, and why
no Web path may call the consuming `ctx.tasks.read()`.
2026-08-08 23:29:41 +08:00

76 lines
3.0 KiB
TypeScript

// @vitest-environment jsdom
/** AppearanceRow behavior: three cubes, selection follows the persisted
* preference, clicks drive setTheme. */
import { afterEach, describe, expect, it, vi } from 'vitest'
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import { createSnapshotStore, type SessionListState, type WorkspaceListState } from '@deepseek-ai/dsh-client-runtime/client'
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
import { AppearanceRow } from '../src/client/AppearanceRow.tsx'
import type { AppearanceRowComponentProps } from '../src/client/AppearanceRow.tsx'
import { createAppearanceRowStore } from '../src/client/settings-store.ts'
import type { ThemePreference } from '../src/client/index.ts'
afterEach(cleanup)
const COPY: Record<string, string> = {
'appearance.title': 'Appearance',
'appearance.light': 'Light',
'appearance.dark': 'Dark',
'appearance.system': 'System',
}
/** Empty global standard-kit hooks (the row reads neither). */
function emptySessions() {
const store = createSnapshotStore<SessionListState>(
{ ids: [], byId: {}, current: undefined, phase: 'ready', subagentsByParent: {}, tasksBySession: {}, currentAddress: undefined })
return bindSnapshotSelector(store)
}
function emptyWorkspaces() {
const store = createSnapshotStore<WorkspaceListState>({
items: [], archivedSessionIds: [], state: 'idle', phase: 'ready', error: null,
baselinesReady: true, recentWorkspaceId: undefined,
})
return bindSnapshotSelector(store)
}
function mount(preference: ThemePreference = 'system') {
// Real store instance — the sanctioned zero-machinery path for tests.
const store = createAppearanceRowStore().create()
store.actions.sync(preference, 0)
const setTheme = vi.fn()
const props: AppearanceRowComponentProps = {
useSessions: emptySessions(),
useWorkspaces: emptyWorkspaces(),
useStore: bindSnapshotSelector(store),
actions: store.actions,
t: (key: string) => COPY[key] ?? key,
setTheme,
}
render(<AppearanceRow {...props} />)
return { store, setTheme }
}
const pressed = (name: RegExp): string | null =>
screen.getByRole('button', { name }).getAttribute('aria-pressed')
describe('AppearanceRow', () => {
it('renders the title and three cubes with the preference cube selected', () => {
mount('dark')
expect(screen.getByText('Appearance')).toBeDefined()
expect(pressed(/Dark/)).toBe('true')
expect(pressed(/Light/)).toBe('false')
expect(pressed(/System/)).toBe('false')
})
it('click drives setTheme; selection follows the store mirror, not the click echo', () => {
const b = mount('dark')
fireEvent.click(screen.getByRole('button', { name: /Light/ }))
expect(b.setTheme).toHaveBeenCalledWith('light')
// No store write yet: selection is unchanged.
expect(pressed(/Dark/)).toBe('true')
act(() => { b.store.actions.sync('light', 1) })
expect(pressed(/Light/)).toBe('true')
expect(pressed(/Dark/)).toBe('false')
})
})