Files
deepseek-harness/packages/client/web-react/tests/use-projection.spec.tsx
T
imccyu e2791107c4 ci: clear the snapshots-and-artifacts lane — lint sweep and TUI snapshot re-record
Lint: eslint --fix over the merge-crossed projection/command files (arrow
parens, trailing commas, unnecessary assertions), Extract<> replaces the
keyof-map & string intersections no-redundant-type-constituents rejects,
the fold-adapter's merge loop drops its non-null assertions for a
bounds-carrying cursor, one JSDoc line wrapped under max-len (api-catalog
regenerated). Snapshots: the four TUI goldens re-recorded for the merged
event-count shift (the durable command lifecycle adds one event to the
seeded diagnostics log). The headless advanced-toolchain snapshot passes
on CI and fails locally in this sandbox both with and without these
changes (30s child timeout — environment-bound, tracked in the ledger).
2026-07-28 11:48:01 +08:00

128 lines
5.0 KiB
TypeScript

// @vitest-environment jsdom
/**
* useProjection standard-kit delivery (session-projection RFC): the fifth
* framework hook seat rides the same provide channel as useSession — a
* session slot component receives `useProjection` in its kit, key-addressed
* over the bundle's projection face; unresolved keys (no value, no face, no
* session) uniformly read `undefined`; live value changes re-render; the
* selector overload runs over the whole value.
*/
import { describe, expect, it } from 'vitest'
import { act, render } from '@testing-library/react'
import type { StoredEntry } from '@deepseek-ai/dsh-client-ui-slots'
import { createSlotRenderer, type SlotRendererHost } from '@deepseek-ai/dsh-client-web-react'
function observable<T>(initial: T) {
let value = initial
const subs = new Set<() => void>()
return {
getSnapshot: () => value,
subscribe: (fn: () => void) => { subs.add(fn); return () => { subs.delete(fn) } },
set: (next: T) => { value = next; for (const fn of [...subs]) fn() },
}
}
type UseProjectionProp = (key: string, selector?: (v: unknown) => unknown) => unknown
function makeHost() {
const current = observable<string | undefined>(undefined)
const cells = new Map<string, ReturnType<typeof observable<unknown>>>()
/** Store-parallel face: always defined per key; an unseen key snapshots undefined. */
const absent = { getSnapshot: () => undefined, subscribe: () => () => {} }
const sessionEntries: StoredEntry[] = []
let withFace = true
const rootEntry: StoredEntry = {
component: (props: { renderSlot: (key: string, owner: object) => React.ReactNode }) =>
<>{props.renderSlot('k.session', {})}</>,
options: {},
children: { 'k.session': { kind: 'single', scope: 'session' } },
}
const info = (id: string) => ({
sessionId: id,
hooks: { session: { getSnapshot: () => ({ sid: id }), subscribe: () => () => {} } },
props: {},
...(withFace ? { projections: { faceOf: (key: string) => cells.get(key) ?? absent } } : {}),
})
const host: SlotRendererHost = {
subscribe: () => () => {},
getVersion: () => 0,
entriesOf: key => key === 'root' ? [rootEntry] : sessionEntries,
specOf: key => key === 'k.session' ? { kind: 'single', scope: 'session' } : undefined,
isLive: () => true,
storeOf: () => undefined,
sessions: {
list: observable<unknown>({ ids: [] }),
current,
provideInfo: id => info(id),
maybeProvideInfo: id => (id === undefined
? { sessionId: undefined, hooks: { session: undefined }, props: {} }
: info(id)),
},
workspaces: { list: observable<unknown>({ items: [] }) },
}
return {
host, current, cells,
dropFace: () => { withFace = false },
registerSession: (entry: StoredEntry) => { sessionEntries.push(entry) },
}
}
describe('useProjection standard-kit delivery', () => {
it('reads the projected value through the kit, undefined for unresolved keys, and follows live changes', () => {
const h = makeHost()
const cell = observable<unknown>({ marks: ['a'] })
h.cells.set('test/marks', cell)
const reads: Record<string, unknown>[] = []
h.registerSession({
component: (props: { useProjection: UseProjectionProp }) => {
reads.push({
marks: props.useProjection('test/marks'),
ghost: props.useProjection('test/ghost'),
})
return null
},
options: {},
})
render(<>{createSlotRenderer().renderRoot(h.host, {})}</>)
act(() => { h.current.set('s1') })
expect(reads.at(-1)).toEqual({ marks: { marks: ['a'] }, ghost: undefined })
// Live change re-renders with the new whole value.
act(() => { cell.set({ marks: ['a', 'b'] }) })
expect(reads.at(-1)).toEqual({ marks: { marks: ['a', 'b'] }, ghost: undefined })
})
it('runs the selector overload over the whole value (and over undefined when absent)', () => {
const h = makeHost()
h.cells.set('test/marks', observable<unknown>({ marks: ['x', 'y'] }))
const reads: unknown[] = []
h.registerSession({
component: (props: { useProjection: UseProjectionProp }) => {
reads.push(props.useProjection('test/marks', v => (v as { marks: string[] } | undefined)?.marks.length ?? -1))
reads.push(props.useProjection('test/ghost', v => (v === undefined ? 'absent' : 'present')))
return null
},
options: {},
})
render(<>{createSlotRenderer().renderRoot(h.host, {})}</>)
act(() => { h.current.set('s1') })
expect(reads.slice(-2)).toEqual([2, 'absent'])
})
it('treats a bundle without the projections face as all-absent (capability absence)', () => {
const h = makeHost()
h.cells.set('test/marks', observable<unknown>({ marks: ['a'] }))
h.dropFace()
const reads: unknown[] = []
h.registerSession({
component: (props: { useProjection: UseProjectionProp }) => {
reads.push(props.useProjection('test/marks'))
return null
},
options: {},
})
render(<>{createSlotRenderer().renderRoot(h.host, {})}</>)
act(() => { h.current.set('s1') })
expect(reads.at(-1)).toBeUndefined()
})
})