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 { 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() 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 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>() }) }) 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']) }) })