| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- import { describe, expect, it } from 'vitest'
- import { Context } from 'cordis'
- import { runInNewContext } from 'node:vm'
- import AgentRegistry from '@deepseek-ai/dsh-agent'
- import type { Agent } from '@deepseek-ai/dsh-agent'
- import { SessionId } from '@deepseek-ai/dsh-session'
- function agent(id: string): Agent {
- return { id: SessionId(id) } as Agent
- }
- async function harness(): Promise<{
- ctx: Context
- service: AgentRegistry
- dispose: () => Promise<void>
- }> {
- const ctx = new Context()
- const fiber = await ctx.plugin(AgentRegistry)
- return {
- ctx,
- service: ctx.agents,
- dispose: fiber.dispose,
- }
- }
- /** Fail a lifecycle regression promptly instead of waiting for Vitest's suite timeout. */
- async function promptly<T>(task: Promise<T>): Promise<T> {
- const timeout = Promise.withResolvers<never>()
- const timer = setTimeout(() => { timeout.reject(new Error('initiator teardown did not settle promptly')) }, 1000)
- try {
- return await Promise.race([task, timeout.promise])
- } finally {
- clearTimeout(timer)
- }
- }
- describe('AgentRegistry initiator scope', () => {
- it('reports an absent initiator and requires an active boundary', async () => {
- const { service, dispose } = await harness()
- expect(service.currentInitiator()).toBeUndefined()
- expect(() => service.requireInitiator()).toThrow('no initiating agent is active')
- await dispose()
- })
- it('preserves exact synchronous and Promise return identities across await', async () => {
- const { service, dispose } = await harness()
- const initiator = agent('identity')
- const value = { result: true }
- expect(service.withInitiator(initiator, () => {
- expect(service.requireInitiator()).toBe(initiator)
- return value
- })).toBe(value)
- const promise = service.withInitiator(initiator, async () => {
- expect(service.requireInitiator()).toBe(initiator)
- await Promise.resolve()
- expect(service.requireInitiator()).toBe(initiator)
- return value
- })
- expect(service.withInitiator(initiator, () => promise)).toBe(promise)
- await expect(promise).resolves.toBe(value)
- expect(service.currentInitiator()).toBeUndefined()
- await dispose()
- })
- it('tracks a branded Promise without calling its overridable then property', async () => {
- const { service, dispose } = await harness()
- const initiator = agent('overridden-then')
- const release = Promise.withResolvers<boolean>()
- void Object.defineProperty(release.promise, 'then', {
- value: () => { throw new Error('overridden then called') },
- })
- const pending = service.withInitiator(initiator, () => release.promise)
- expect(pending).toBe(release.promise)
- let disposed = false
- const disposal = dispose().then(() => { disposed = true })
- await Promise.resolve()
- expect(disposed).toBe(false)
- release.resolve(true)
- await new Promise<void>((resolve, reject) => {
- void Promise.prototype.then.call(pending, resolve, reject)
- })
- await disposal
- expect(disposed).toBe(true)
- })
- it('preserves a settled branded Promise when its species blocks observer construction', async () => {
- const { service, dispose } = await harness()
- const initiator = agent('invalid-species')
- const promise = Promise.resolve()
- const constructor = {}
- Object.defineProperty(constructor, Symbol.species, {
- get: () => { throw new Error('invalid species') },
- })
- void Object.defineProperty(promise, 'constructor', { value: constructor })
- expect(service.withInitiator(initiator, () => promise)).toBe(promise)
- await dispose()
- })
- it('isolates overlapping initiators', async () => {
- const { service, dispose } = await harness()
- const a = agent('a')
- const b = agent('b')
- const bothStarted = Promise.withResolvers<boolean>()
- const release = Promise.withResolvers<boolean>()
- let starts = 0
- const run = (initiator: Agent): Promise<void> => service.withInitiator(initiator, async () => {
- expect(service.requireInitiator()).toBe(initiator)
- starts += 1
- if (starts === 2) bothStarted.resolve(true)
- await release.promise
- expect(service.requireInitiator()).toBe(initiator)
- })
- const pending = [run(a), run(b)]
- await bothStarted.promise
- expect(service.currentInitiator()).toBeUndefined()
- release.resolve(true)
- await Promise.all(pending)
- await dispose()
- })
- it('restores nested and explicitly cleared boundaries', async () => {
- const { service, dispose } = await harness()
- const parent = agent('parent')
- const child = agent('child')
- service.withInitiator(parent, () => {
- expect(service.requireInitiator()).toBe(parent)
- service.withInitiator(child, () => { expect(service.requireInitiator()).toBe(child) })
- expect(service.requireInitiator()).toBe(parent)
- service.withoutInitiator(() => {
- expect(service.currentInitiator()).toBeUndefined()
- expect(() => service.requireInitiator()).toThrow('no initiating agent is active')
- })
- expect(service.requireInitiator()).toBe(parent)
- })
- expect(service.currentInitiator()).toBeUndefined()
- await dispose()
- })
- it('restores the parent after synchronous throws and rejected operations', async () => {
- const { service, dispose } = await harness()
- const parent = agent('parent')
- const child = agent('child')
- const syncError = new Error('sync failure')
- const asyncError = new Error('async failure')
- service.withInitiator(parent, () => {
- expect(() => service.withInitiator(child, () => { throw syncError })).toThrow(syncError)
- expect(service.requireInitiator()).toBe(parent)
- })
- await expect(service.withInitiator(child, async () => {
- await Promise.resolve()
- throw asyncError
- })).rejects.toBe(asyncError)
- expect(service.currentInitiator()).toBeUndefined()
- await dispose()
- })
- it('stops new boundaries, drains active Promises, and invalidates retained references', async () => {
- const { ctx, service, dispose } = await harness()
- const initiator = agent('draining')
- const release = Promise.withResolvers<boolean>()
- const pending = service.withInitiator(initiator, async () => {
- await release.promise
- expect(service.requireInitiator()).toBe(initiator)
- })
- let disposed = false
- const disposal = dispose().then(() => { disposed = true })
- await Promise.resolve()
- expect(() => service.withInitiator(initiator, () => 1)).toThrow('agent initiator scope is disposed')
- expect(() => service.withoutInitiator(() => 1)).toThrow('agent initiator scope is disposed')
- expect(disposed).toBe(false)
- expect(ctx.get('agents')).toBeUndefined()
- release.resolve(true)
- await pending
- await disposal
- expect(() => service.currentInitiator()).toThrow('agent initiator scope is disposed')
- expect(() => service.requireInitiator()).toThrow('agent initiator scope is disposed')
- })
- it('drains cross-realm Promise boundaries before disposal', async () => {
- const { service, dispose } = await harness()
- const initiator = agent('cross-realm')
- const release = Promise.withResolvers<boolean>()
- const operation = runInNewContext(
- '(async () => { await release; inspect() })',
- {
- release: release.promise,
- inspect: () => { expect(service.requireInitiator()).toBe(initiator) },
- },
- ) as () => Promise<void>
- const pending = service.withInitiator(initiator, operation)
- expect(pending).not.toBeInstanceOf(Promise)
- let disposed = false
- const disposal = dispose().then(() => { disposed = true })
- await Promise.resolve()
- expect(disposed).toBe(false)
- release.resolve(true)
- await pending
- await disposal
- expect(disposed).toBe(true)
- })
- it('does not self-deadlock when a boundary returns service disposal', async () => {
- const { service, dispose } = await harness()
- const initiator = agent('service-disposer')
- const returned = service.withInitiator(initiator, dispose)
- await promptly(returned)
- expect(() => service.currentInitiator()).toThrow('agent initiator scope is disposed')
- })
- it('does not self-deadlock when nested boundaries return ancestor disposal', async () => {
- const { ctx, service } = await harness()
- const parent = agent('parent-disposer')
- const child = agent('child-disposer')
- let disposal: Promise<void> | undefined
- const returned = service.withInitiator(parent, () => service.withInitiator(child, () => {
- disposal = ctx.fiber.dispose()
- return disposal
- }))
- expect(returned).toBe(disposal)
- await promptly(returned)
- expect(() => service.currentInitiator()).toThrow('agent initiator scope is disposed')
- })
- it('excludes an asynchronous teardown initiator while draining unrelated boundaries', async () => {
- const { ctx, service } = await harness()
- const initiator = agent('async-disposer')
- const unrelated = agent('unrelated')
- const release = Promise.withResolvers<boolean>()
- const pending = service.withInitiator(unrelated, async () => {
- await release.promise
- expect(service.requireInitiator()).toBe(unrelated)
- })
- const returned = service.withInitiator(initiator, async () => {
- await Promise.resolve()
- await ctx.fiber.dispose()
- })
- let disposed = false
- void returned.then(() => { disposed = true })
- await Promise.resolve()
- await Promise.resolve()
- expect(disposed).toBe(false)
- release.resolve(true)
- await pending
- await promptly(returned)
- expect(() => service.currentInitiator()).toThrow('agent initiator scope is disposed')
- })
- })
|