Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`, `verify-translation-pairing --write` for the touched bilingual pairs, `gen-doc-graphs`, and one typert snapshot whose ids embed character offsets. `pnpm run rescope-vendor --check` verifies the result. Renames nine vendored packages (cordis, cosmokit, schemastery and the six @cordisjs plugins) and every reference that resolves them: manifest names and dependency keys, module specifiers including declare-module merges, cordis.yml plugin names, tsconfig paths, every Markdown fence, and `docs/` prose. Directory names, upstream versions, and dependency ranges are unchanged, so vendor/README.md still reads as an upstream snapshot; its manifest table gains an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed at each fork's origin. The tutorial tier follows the rename end to end: its yaml fences named plugins the Loader can no longer resolve, its `ts ignore-check` fences disagreed with the compiled fences beside them, and its prose quoted both. The contracts that told readers to keep upstream names — the root convention and the vendoring cookbook's tree comment and manifest invariant — now say to rescope instead. Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle purity gate now names the vendored libraries a browser bundle inlines, and the files where a bare `cordis` is an agent-preset id keep that product data.
223 lines
8.4 KiB
TypeScript
223 lines
8.4 KiB
TypeScript
import { describe, expect, expectTypeOf, it } from 'vitest'
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
import { bindScopeParent, carrierKeyOf, createScope, isScopeCarrier, scopeChainOf, scopeOf, scopeParentOf, scopeTarget } from '@deepseek-ai/dsh-scope'
|
|
import type { Scope, Scoped } from '@deepseek-ai/dsh-scope'
|
|
|
|
declare module '@deepseek-ai/cordis' {
|
|
interface Events {
|
|
/**
|
|
* Test-only event for scope-filtered dispatch.
|
|
* @param value - opaque payload recorded by listeners.
|
|
* @mode emit
|
|
*/
|
|
'scope-test/ping'(value: string): void
|
|
}
|
|
}
|
|
|
|
/** Mount a host plugin and mint a scope inside it. */
|
|
async function mintScope(ctx: Context, key: object): Promise<Scope> {
|
|
let scope!: Scope
|
|
await ctx.plugin((inner: Context) => { scope = createScope(inner, key) })
|
|
return scope
|
|
}
|
|
|
|
describe('createScope', () => {
|
|
it('tags contexts and derived contexts, with the nearest tag winning', async () => {
|
|
const ctx = new Context()
|
|
const outerKey = { name: 'outer' }
|
|
const innerKey = { name: 'inner' }
|
|
const outer = await mintScope(ctx, outerKey)
|
|
const inner = createScope(outer.ctx, innerKey)
|
|
|
|
expect(scopeOf(ctx)).toBeUndefined()
|
|
expect(scopeOf(outer.ctx)).toBe(outerKey)
|
|
expect(scopeOf(outer.ctx.extend({}))).toBe(outerKey)
|
|
expect(scopeOf(inner.ctx)).toBe(innerKey)
|
|
|
|
await inner.dispose()
|
|
await outer.dispose()
|
|
})
|
|
|
|
it('is usable synchronously before the backing fiber activates', async () => {
|
|
const ctx = new Context()
|
|
const events: string[] = []
|
|
let scope!: Scope
|
|
await ctx.plugin((inner: Context) => {
|
|
scope = createScope(inner, { name: 'sync' })
|
|
scope.ctx.effect(() => () => void events.push('disposed'))
|
|
events.push('registered')
|
|
})
|
|
expect(events).toEqual(['registered'])
|
|
await scope.dispose()
|
|
expect(events).toEqual(['registered', 'disposed'])
|
|
})
|
|
|
|
it('shares quiescence across repeat and raw-disposer-first calls', async () => {
|
|
const ctx = new Context()
|
|
const scope = await mintScope(ctx, { name: 'quiescence' })
|
|
const gate = Promise.withResolvers<undefined>()
|
|
let finished = false
|
|
scope.ctx.effect(() => async () => {
|
|
await gate.promise
|
|
finished = true
|
|
})
|
|
|
|
const raw = Promise.resolve(scope.rawDispose())
|
|
const publicDispose = scope.dispose()
|
|
await Promise.resolve()
|
|
expect(finished).toBe(false)
|
|
gate.resolve(undefined)
|
|
await Promise.all([raw, publicDispose, scope.dispose()])
|
|
expect(finished).toBe(true)
|
|
})
|
|
|
|
it('exposes the exact raw disposer for ordered composite teardown', async () => {
|
|
const ctx = new Context()
|
|
const order: string[] = []
|
|
let dispose!: () => Promise<void> | void
|
|
await ctx.plugin((inner: Context) => {
|
|
dispose = inner.effect(function* () {
|
|
yield () => void order.push('outer')
|
|
const scope = createScope(inner, { name: 'nested' })
|
|
scope.ctx.effect(() => () => void order.push('scope'))
|
|
yield scope.rawDispose
|
|
yield () => void order.push('inner')
|
|
})
|
|
})
|
|
await dispose()
|
|
expect(order).toEqual(['inner', 'scope', 'outer'])
|
|
})
|
|
})
|
|
|
|
describe('scopeTarget', () => {
|
|
it('routes scoped listeners by key while untagged listeners remain global', async () => {
|
|
const ctx = new Context()
|
|
const keyA = { name: 'A' }
|
|
const keyB = { name: 'B' }
|
|
const scopeA = await mintScope(ctx, keyA)
|
|
const scopeB = await mintScope(ctx, keyB)
|
|
const heard: string[] = []
|
|
ctx.on('scope-test/ping', value => void heard.push(`global:${value}`))
|
|
scopeA.ctx.on('scope-test/ping', value => void heard.push(`A:${value}`))
|
|
scopeB.ctx.on('scope-test/ping', value => void heard.push(`B:${value}`))
|
|
|
|
ctx.emit(scopeTarget(ctx, keyA), 'scope-test/ping', 'a')
|
|
ctx.emit(scopeTarget(ctx, keyB), 'scope-test/ping', 'b')
|
|
ctx.emit(scopeTarget(ctx, undefined), 'scope-test/ping', 'none')
|
|
|
|
expect(heard).toEqual(['global:a', 'A:a', 'global:b', 'B:b', 'global:none'])
|
|
await Promise.all([scopeA.dispose(), scopeB.dispose()])
|
|
})
|
|
|
|
it('preserves a base Cordis filter and its receiver', async () => {
|
|
const ctx = new Context()
|
|
const key = { name: 'A' }
|
|
const scope = await mintScope(ctx, key)
|
|
const heard: string[] = []
|
|
ctx.on('scope-test/ping', value => void heard.push(`global:${value}`))
|
|
scope.ctx.on('scope-test/ping', value => void heard.push(`A:${value}`))
|
|
let receiverMatches = false
|
|
const base = {
|
|
[Context.filter](this: object): boolean {
|
|
receiverMatches = this === base
|
|
return false
|
|
},
|
|
}
|
|
|
|
ctx.emit(scopeTarget(base, key), 'scope-test/ping', 'vetoed')
|
|
expect(heard).toEqual([])
|
|
expect(receiverMatches).toBe(true)
|
|
await scope.dispose()
|
|
})
|
|
|
|
it('{ global: true } listeners retain Cordis global-listener semantics', async () => {
|
|
const ctx = new Context()
|
|
const scope = await mintScope(ctx, { name: 'A' })
|
|
const heard: string[] = []
|
|
scope.ctx.on('scope-test/ping', value => void heard.push(value), { global: true })
|
|
ctx.emit(scopeTarget(ctx, { name: 'other' }), 'scope-test/ping', 'foreign')
|
|
ctx.emit(scopeTarget(ctx, undefined), 'scope-test/ping', 'none')
|
|
expect(heard).toEqual(['foreign', 'none'])
|
|
await scope.dispose()
|
|
})
|
|
|
|
it('uses an opaque branded carrier with a separately tracked key', () => {
|
|
const key = { name: 'key' }
|
|
const subject = { value: 1 }
|
|
const carrier = scopeTarget(subject, key)
|
|
expect(isScopeCarrier(carrier)).toBe(true)
|
|
expect(carrierKeyOf(carrier)).toBe(key)
|
|
expect(isScopeCarrier(subject)).toBe(false)
|
|
expect(carrierKeyOf(subject)).toBeUndefined()
|
|
expect('value' in carrier).toBe(false)
|
|
expectTypeOf(carrier).toEqualTypeOf<Scoped<typeof subject>>()
|
|
})
|
|
})
|
|
|
|
describe('scope parent chain', () => {
|
|
it('links at mint, walks to the root, and rejects cycles', () => {
|
|
const ctx = new Context()
|
|
const preset = { kind: 'preset' }
|
|
const agent = { kind: 'agent' }
|
|
createScope(ctx, preset)
|
|
createScope(ctx, agent, { parent: preset })
|
|
|
|
expect(scopeParentOf(agent)).toBe(preset)
|
|
expect(scopeParentOf(preset)).toBeUndefined()
|
|
expect(scopeChainOf(agent)).toEqual([agent, preset])
|
|
expect(scopeChainOf(undefined)).toEqual([])
|
|
expect(() => { bindScopeParent(preset, agent) }).toThrow(/cycle/)
|
|
expect(() => { bindScopeParent(preset, preset) }).toThrow(/cycle/)
|
|
})
|
|
|
|
it('re-links only through the binding held by the original binder', () => {
|
|
const ctx = new Context()
|
|
const presetA = { id: 'a' }
|
|
const presetB = { id: 'b' }
|
|
const agent = { id: 'agent' }
|
|
createScope(ctx, presetA)
|
|
createScope(ctx, presetB)
|
|
const binding = bindScopeParent(agent, presetA)
|
|
createScope(ctx, agent)
|
|
|
|
// A bound key cannot be re-bound from the outside; only the binding moves it.
|
|
expect(() => bindScopeParent(agent, presetB)).toThrow(/already bound/)
|
|
binding.rebind(presetB)
|
|
|
|
expect(scopeChainOf(agent)).toEqual([agent, presetB])
|
|
// The rebind keeps the cycle check: a parent may not adopt its ancestor.
|
|
const child = { id: 'child' }
|
|
const childBinding = bindScopeParent(child, agent)
|
|
void childBinding
|
|
expect(() => { binding.rebind(child) }).toThrow(/cycle/)
|
|
})
|
|
|
|
it('admits an ancestor-tagged listener for a descendant dispatch, never the reverse', () => {
|
|
const ctx = new Context()
|
|
const preset = { kind: 'preset' }
|
|
const agent = { kind: 'agent' }
|
|
const other = { kind: 'other-preset' }
|
|
const presetScope = createScope(ctx, preset)
|
|
const agentScope = createScope(ctx, agent, { parent: preset })
|
|
const otherScope = createScope(ctx, other)
|
|
|
|
const seen: string[] = []
|
|
ctx.on('probe/event' as never, ((): void => { seen.push('untagged') }) as never)
|
|
presetScope.ctx.on('probe/event' as never, ((): void => { seen.push('preset') }) as never)
|
|
agentScope.ctx.on('probe/event' as never, ((): void => { seen.push('agent') }) as never)
|
|
otherScope.ctx.on('probe/event' as never, ((): void => { seen.push('other') }) as never)
|
|
|
|
const emit = ctx as unknown as { emit: (carrier: object, type: string) => void }
|
|
// Dispatch at the AGENT key: its own tag and its ancestor's admit; a
|
|
// sibling root does not.
|
|
emit.emit(scopeTarget({}, agent), 'probe/event')
|
|
expect(seen.sort()).toEqual(['agent', 'preset', 'untagged'])
|
|
|
|
// Dispatch at the PRESET key: the agent-tagged listener sits BELOW the
|
|
// dispatch key and stays excluded — events flow up the chain, not down.
|
|
seen.length = 0
|
|
emit.emit(scopeTarget({}, preset), 'probe/event')
|
|
expect(seen.sort()).toEqual(['preset', 'untagged'])
|
|
})
|
|
})
|