mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
256 lines
10 KiB
TypeScript
256 lines
10 KiB
TypeScript
/**
|
|
* @vitest-environment jsdom
|
|
*
|
|
* Guard facade account: the whitelist a dynamic plugin's `apply` sees, the
|
|
* automatic shadowing priority on the slots seat, the theme seat's pinned
|
|
* override source and fiber-owned disposer, and the Context denial that keeps a
|
|
* dynamic package from reaching a foreign context. Registrations ride the
|
|
* CALLING fiber, so disposing it must remove them (HMR safety).
|
|
*/
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import type { FC } from 'react'
|
|
import type {
|
|
CordisDynamicPackageId,
|
|
CordisDynamicPluginId,
|
|
CordisDynamicPluginRunId,
|
|
DynamicCordisPackage,
|
|
} from '@deepseek-ai/dsh-api-remotes/client'
|
|
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
|
|
import { dynamicCordisContext } from '../src/client/guard.ts'
|
|
import type { DynamicCordisSlotLedgerRow } from '../src/client/guard.ts'
|
|
|
|
const C: FC<object> = () => null
|
|
|
|
/** The exact running package carried by a Client dispatch. */
|
|
function pkg(): DynamicCordisPackage {
|
|
return {
|
|
pluginId: 'dyn-1' as CordisDynamicPluginId,
|
|
packageId: 'pkg-1' as CordisDynamicPackageId,
|
|
pluginRunId: 'run-1' as CordisDynamicPluginRunId,
|
|
name: 'demo',
|
|
}
|
|
}
|
|
|
|
/** Erased facade view: a dynamic package reads services off plain properties. */
|
|
type Facade = Record<string, unknown> & { get(name: string): unknown }
|
|
|
|
interface Bench {
|
|
ctx: Context
|
|
slots: SlotsService
|
|
facade: Facade
|
|
ledger: DynamicCordisSlotLedgerRow[]
|
|
/** Components the facade claimed for the package, in registration order. */
|
|
claimed: unknown[]
|
|
dispose: () => Promise<void>
|
|
overrideTokens: ReturnType<typeof vi.fn>
|
|
themeLayerDispose: ReturnType<typeof vi.fn>
|
|
}
|
|
|
|
/**
|
|
* Mount a dynamic-plugin fiber declaring `inject`, and capture the facade its
|
|
* apply receives (the real product path: the facade wraps the fiber's own ctx).
|
|
*/
|
|
async function boot(inject: string[], extras: Record<string, unknown> = {}): Promise<Bench> {
|
|
const ctx = new Context()
|
|
await ctx.plugin(SlotsService)
|
|
const themeLayerDispose = vi.fn()
|
|
const overrideTokens = vi.fn(() => themeLayerDispose)
|
|
ctx.reflect.provide('theme', {
|
|
overrideTokens,
|
|
getTheme: () => ({ preference: 'light' }),
|
|
reload: () => Promise.resolve('reloaded'),
|
|
revision: 3,
|
|
escape: () => new Context(),
|
|
escapeLater: () => Promise.resolve(new Context()),
|
|
})
|
|
for (const [name, value] of Object.entries(extras)) ctx.reflect.provide(name, value)
|
|
const ledger: DynamicCordisSlotLedgerRow[] = []
|
|
const claimed: unknown[] = []
|
|
let nextPriority = 0
|
|
let facade: Facade | undefined
|
|
const fiber = ctx.plugin({
|
|
name: 'dyn/dyn-1',
|
|
inject,
|
|
apply: (own: Context) => {
|
|
facade = dynamicCordisContext(own, {
|
|
pkg: pkg(),
|
|
ledger,
|
|
claim: (component) => { claimed.push(component) },
|
|
allocatePriority: () => --nextPriority,
|
|
reportFailure: () => {},
|
|
}) as unknown as Facade
|
|
},
|
|
})
|
|
await fiber
|
|
if (facade === undefined) throw new Error('facade was not captured')
|
|
return {
|
|
ctx,
|
|
slots: ctx.slots,
|
|
facade,
|
|
ledger,
|
|
claimed,
|
|
dispose: async () => { await fiber.dispose() },
|
|
overrideTokens,
|
|
themeLayerDispose,
|
|
}
|
|
}
|
|
|
|
describe('facade surface', () => {
|
|
it('forwards whitelisted lifecycle verbs to the real ctx', async () => {
|
|
const bench = await boot([])
|
|
const seen: string[] = []
|
|
const on = bench.facade.on as (event: string, listener: (key: string) => void) => void
|
|
on('slots/changed', key => seen.push(key))
|
|
bench.ctx.emit('slots/changed', 'root')
|
|
expect(seen).toEqual(['root'])
|
|
})
|
|
|
|
it('teaches the object form when an existing service was not declared', async () => {
|
|
const bench = await boot([])
|
|
expect(() => bench.facade.slots).toThrow(/service "slots" is not declared by your plugin/)
|
|
expect(() => bench.facade.slots).toThrow(/a plain `function` has no declaration site/)
|
|
})
|
|
|
|
it('withholds framework internals with a teaching list', async () => {
|
|
const bench = await boot([])
|
|
expect(() => bench.facade.registry).toThrow(/dynamic ctx does not expose "registry"/)
|
|
expect(() => bench.facade.registry).toThrow(/any service your returned plugin declared in inject/)
|
|
})
|
|
|
|
it('answers `get` and `has` over the same whitelist, and refuses writes', async () => {
|
|
const bench = await boot(['slots'])
|
|
expect(typeof bench.facade.get('slots')).toBe('object')
|
|
expect('get' in bench.facade).toBe(true)
|
|
expect('on' in bench.facade).toBe(true)
|
|
expect('slots' in bench.facade).toBe(true)
|
|
expect('registry' in bench.facade).toBe(false)
|
|
expect(Symbol.iterator in bench.facade).toBe(false)
|
|
expect((bench.facade as unknown as Record<symbol, unknown>)[Symbol.iterator]).toBeUndefined()
|
|
expect(() => { bench.facade.slots = 1 }).toThrow(/dynamic ctx is read-only/)
|
|
})
|
|
|
|
it('denies a service value or return that is a cordis Context', async () => {
|
|
const bench = await boot(['leaky'], {
|
|
leaky: { escape: () => new Context(), later: () => Promise.resolve(new Context()), plain: 7 },
|
|
})
|
|
const leaky = bench.facade.leaky as { escape(): unknown; later(): Promise<unknown>; plain: number }
|
|
expect(() => leaky.escape()).toThrow(/returned a cordis Context/)
|
|
await expect(leaky.later()).rejects.toThrow(/returned a cordis Context/)
|
|
expect(leaky.plain).toBe(7)
|
|
})
|
|
|
|
it('passes a primitive service through untouched', async () => {
|
|
const bench = await boot(['flag'], { flag: 'on' })
|
|
expect(bench.facade.flag).toBe('on')
|
|
})
|
|
})
|
|
|
|
describe('slots seat', () => {
|
|
it('assigns a descending shadowing priority per registration and ledgers it', async () => {
|
|
const bench = await boot(['slots'])
|
|
const slots = bench.facade.slots as { register(options: object, component: unknown): () => void }
|
|
slots.register({ name: 'root' }, C)
|
|
slots.register({ name: 'root' }, C)
|
|
expect(bench.ledger).toEqual([
|
|
{ slot: 'root', priority: -1 },
|
|
{ slot: 'root', priority: -2 },
|
|
])
|
|
// Newest-wins ordering is what "registering IS shadowing" means.
|
|
const priorities = bench.slots.entries('root').map(entry => entry.options.priority)
|
|
expect(priorities).toContain(-1)
|
|
expect(priorities).toContain(-2)
|
|
})
|
|
|
|
it('keeps an explicit priority when the target elects its own order', async () => {
|
|
const bench = await boot(['slots'])
|
|
const slots = bench.facade.slots as { register(options: object, component: unknown): () => void }
|
|
const spec = vi.spyOn(bench.slots, 'spec').mockReturnValue({ kind: 'chain', scope: 'root' })
|
|
slots.register({ name: 'root', priority: 5 }, C)
|
|
spec.mockRestore()
|
|
expect(bench.ledger).toEqual([{ slot: 'root', priority: 5 }])
|
|
})
|
|
|
|
it('rejects a malformed register call before touching the registry', async () => {
|
|
const bench = await boot(['slots'])
|
|
const slots = bench.facade.slots as { register(options: unknown, component: unknown): () => void }
|
|
expect(() => slots.register(null, C)).toThrow(/needs an options object with a `name`/)
|
|
expect(() => slots.register({}, C)).toThrow(/need a string `name`/)
|
|
expect(bench.slots.entries('root')).toHaveLength(0)
|
|
})
|
|
|
|
it('forwards non-register slot methods through the generic guard', async () => {
|
|
const bench = await boot(['slots'])
|
|
const slots = bench.facade.slots as {
|
|
register(options: object, component: unknown): () => void
|
|
entries(key: string): readonly unknown[]
|
|
}
|
|
slots.register({ name: 'root' }, C)
|
|
expect(slots.entries('root')).toHaveLength(1)
|
|
})
|
|
|
|
it('denies a non-callable slots member that would hand out a context', async () => {
|
|
const bench = await boot(['slots'])
|
|
const slots = bench.facade.slots as { ctx: unknown }
|
|
// The service's own ctx is the classic escape route out of the facade.
|
|
expect(() => slots.ctx).toThrow(/service "slots" returned a cordis Context/)
|
|
})
|
|
|
|
it('removes its registrations when the calling fiber unloads (HMR safety)', async () => {
|
|
const bench = await boot(['slots'])
|
|
const slots = bench.facade.slots as { register(options: object, component: unknown): () => void }
|
|
slots.register({ name: 'root' }, C)
|
|
expect(bench.slots.entries('root')).toHaveLength(1)
|
|
await bench.dispose()
|
|
expect(bench.slots.entries('root')).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe('theme seat', () => {
|
|
it('pins the override source to the package id whatever the caller passes', async () => {
|
|
const bench = await boot(['theme'])
|
|
const theme = bench.facade.theme as { overrideTokens(source: unknown, tokens: unknown): () => void }
|
|
const tokens = { '--dsw-alias-x': { light: '#fff', dark: '#000' } }
|
|
theme.overrideTokens('pretend-to-be-someone-else', tokens)
|
|
expect(bench.overrideTokens).toHaveBeenCalledWith('dyn-1.pkg-1', tokens)
|
|
})
|
|
|
|
it('teaches the two-argument shape when the token map arrives first', async () => {
|
|
const bench = await boot(['theme'])
|
|
const theme = bench.facade.theme as { overrideTokens(source: unknown, tokens?: unknown): () => void }
|
|
expect(() => theme.overrideTokens({ '--x': { light: 'a', dark: 'b' } }))
|
|
.toThrow(/takes two arguments; source is replaced with your package id/)
|
|
expect(bench.overrideTokens).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('hangs the layer disposer on the fiber while still returning it', async () => {
|
|
const bench = await boot(['theme'])
|
|
const theme = bench.facade.theme as { overrideTokens(source: unknown, tokens: unknown): () => void }
|
|
const handle = theme.overrideTokens('mine', {})
|
|
expect(handle).toBe(bench.themeLayerDispose)
|
|
expect(bench.themeLayerDispose).not.toHaveBeenCalled()
|
|
// Model code cannot be trusted to keep the handle: unload must restore.
|
|
await bench.dispose()
|
|
expect(bench.themeLayerDispose).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('forwards other theme methods, including asynchronous ones', async () => {
|
|
const bench = await boot(['theme'])
|
|
const theme = bench.facade.theme as {
|
|
getTheme(): { preference: string }
|
|
reload(): Promise<string>
|
|
revision: number
|
|
}
|
|
expect(theme.getTheme().preference).toBe('light')
|
|
await expect(theme.reload()).resolves.toBe('reloaded')
|
|
expect(theme.revision).toBe(3)
|
|
})
|
|
|
|
it('denies a Context a theme method hands back, synchronously or awaited', async () => {
|
|
const bench = await boot(['theme'])
|
|
const theme = bench.facade.theme as { escape(): unknown; escapeLater(): Promise<unknown> }
|
|
expect(() => theme.escape()).toThrow(/service "theme" returned a cordis Context/)
|
|
await expect(theme.escapeLater()).rejects.toThrow(/service "theme" returned a cordis Context/)
|
|
})
|
|
})
|