mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Mechanical --fix output over the newly linted .tsx files (indent, arrow-parens, comma-dangle, member-delimiter-style, unnecessary type assertions), plus the three generic-arrow test hooks converted to function declarations up front: the comma-dangle fixer strips the <T,> disambiguation comma and turns them into parse errors otherwise.
139 lines
5.0 KiB
TypeScript
139 lines
5.0 KiB
TypeScript
// @vitest-environment jsdom
|
|
/**
|
|
* A retained render binding dies with its entry. Re-registering the same key
|
|
* creates a new binding rather than reviving the stale closure.
|
|
*/
|
|
import { describe, expect, it } from 'vitest'
|
|
import { act, render } from '@testing-library/react'
|
|
import type { ReactNode } from 'react'
|
|
import type { SlotEntryDef, SlotSpec, StoredEntry } from '@deepseek-ai/dsh-client-ui-slots'
|
|
import {
|
|
createSlotRenderer, StaleAuthorizationError,
|
|
type RenderOpts, type SlotRendererHost,
|
|
} from '@deepseek-ai/dsh-client-web-react'
|
|
|
|
type RenderSlotFn = (key: string, owner: object, opts?: RenderOpts) => ReactNode
|
|
type DeclaredSpec = SlotSpec<SlotEntryDef>
|
|
|
|
/** Ledger-shaped fake: add/dispose maintain the live set the way the runtime ledger does. */
|
|
function makeHost() {
|
|
const entries = new Map<string, StoredEntry[]>()
|
|
const versions = new Map<string, number>()
|
|
const subs = new Map<string, Set<() => void>>()
|
|
const live = new Set<StoredEntry>()
|
|
const bump = (key: string) => {
|
|
versions.set(key, (versions.get(key) ?? 0) + 1)
|
|
for (const fn of [...(subs.get(key) ?? [])]) fn()
|
|
}
|
|
const host: SlotRendererHost = {
|
|
subscribe: (key, fn) => {
|
|
const set = subs.get(key) ?? new Set()
|
|
set.add(fn)
|
|
subs.set(key, set)
|
|
return () => { set.delete(fn) }
|
|
},
|
|
getVersion: key => versions.get(key) ?? 0,
|
|
entriesOf: key => entries.get(key) ?? [],
|
|
specOf: () => ({ kind: 'single', scope: 'root' }),
|
|
isLive: entry => live.has(entry),
|
|
storeOf: () => undefined,
|
|
sessions: {
|
|
list: { getSnapshot: () => ({}), subscribe: () => () => {} },
|
|
current: { getSnapshot: () => undefined, subscribe: () => () => {} },
|
|
provideInfo: () => undefined,
|
|
maybeProvideInfo: () => ({ sessionId: undefined, hooks: {}, props: {} }),
|
|
},
|
|
workspaces: {
|
|
list: { getSnapshot: () => ({}), subscribe: () => () => {} },
|
|
},
|
|
}
|
|
return {
|
|
host,
|
|
add: (key: string, entry: StoredEntry) => {
|
|
entries.set(key, [...(entries.get(key) ?? []), entry])
|
|
live.add(entry)
|
|
bump(key)
|
|
return () => {
|
|
entries.set(key, (entries.get(key) ?? []).filter(e => e !== entry))
|
|
live.delete(entry)
|
|
bump(key)
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
const CHILD: DeclaredSpec = { kind: 'single', scope: 'root' }
|
|
|
|
/**
|
|
* Mount a root entry that leaks its binding to the test, then render. The
|
|
* returned dispose unmounts the view FIRST: an empty 'root' makes the live
|
|
* root outlet rethrow its boot-order failure (fail-loud, covered in the
|
|
* scoped-slots suite); the retained-closure scenario under test here is a
|
|
* dead entry whose binding outlives the tree.
|
|
*/
|
|
function mountCapturing(h: ReturnType<typeof makeHost>) {
|
|
let captured: RenderSlotFn | undefined
|
|
const entry: StoredEntry = {
|
|
component: (props: { renderSlot: RenderSlotFn }) => {
|
|
captured = props.renderSlot
|
|
return null
|
|
},
|
|
options: {},
|
|
children: { 'k.child': CHILD },
|
|
}
|
|
const disposeEntry = h.add('root', entry)
|
|
const view = render(<>{createSlotRenderer().renderRoot(h.host, {})}</>)
|
|
return {
|
|
binding: captured!,
|
|
entry,
|
|
dispose: () => {
|
|
view.unmount()
|
|
disposeEntry()
|
|
},
|
|
}
|
|
}
|
|
|
|
describe('stale authorization', () => {
|
|
it('a live binding renders; the same closure throws after its entry is disposed', () => {
|
|
const h = makeHost()
|
|
const { binding, dispose } = mountCapturing(h)
|
|
expect(binding('k.child', {})).not.toBeUndefined() // live: returns an element
|
|
act(() => { dispose() })
|
|
expect(() => binding('k.child', {})).toThrow(StaleAuthorizationError)
|
|
expect(() => binding('k.child', {})).toThrow(/disposed registration/)
|
|
})
|
|
|
|
it('stale check precedes the ownership check: a dead binding throws stale even for undeclared keys', () => {
|
|
const h = makeHost()
|
|
const { binding, dispose } = mountCapturing(h)
|
|
act(() => { dispose() })
|
|
// Were ownership checked first this would be SlotOwnershipError; the dead
|
|
// entry must fail on liveness regardless of the key asked for.
|
|
expect(() => binding('k.undeclared', {})).toThrow(StaleAuthorizationError)
|
|
})
|
|
|
|
it('HMR reload (same key, new entry) mints a fresh binding; the old one stays dead', () => {
|
|
const h = makeHost()
|
|
const first = mountCapturing(h)
|
|
act(() => { first.dispose() })
|
|
|
|
// Reload: a new entry object for the same slot key (new registration identity).
|
|
let secondBinding: RenderSlotFn | undefined
|
|
const secondEntry: StoredEntry = {
|
|
component: (props: { renderSlot: RenderSlotFn }) => {
|
|
secondBinding = props.renderSlot
|
|
return null
|
|
},
|
|
options: {},
|
|
children: { 'k.child': CHILD },
|
|
}
|
|
h.add('root', secondEntry)
|
|
render(<>{createSlotRenderer().renderRoot(h.host, {})}</>)
|
|
|
|
expect(secondBinding).toBeDefined()
|
|
expect(secondBinding).not.toBe(first.binding) // new identity, no revival
|
|
expect(secondBinding!('k.child', {})).not.toBeUndefined() // new binding is live
|
|
expect(() => first.binding('k.child', {})).toThrow(StaleAuthorizationError) // old stays dead
|
|
})
|
|
})
|