refactor(core): fold initiator scope into agents

This commit is contained in:
Tianyi Cui
2026-07-19 13:30:45 +08:00
parent 536cb6f985
commit c23214be56
79 changed files with 1883 additions and 1003 deletions

View File

@@ -0,0 +1,163 @@
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,
}
}
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('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)
})
})

View File

@@ -6,7 +6,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { collectEvents, collectServices } from '../../../../scripts/gen-cordis-catalog.ts'
import { collectEvents, collectServices, renderEvents, renderServices } from '../../../../scripts/gen-cordis-catalog.ts'
/** Write a fixture package exposing one `interface Events` block and return the
* scan root to hand `collectEvents`. */
@@ -58,6 +58,8 @@ describe('gen-cordis-catalog collectEvents', () => {
))
expect(events).toHaveLength(1)
expect(events[0]).toMatchObject({ name: 'fix/happened', scope: 'fix', mode: 'emit', doc: 'A thing happened.' })
expect(events[0]?.jsDoc).toBe('/**\n * A thing happened.\n * @param id - which thing.\n * @mode emit\n */')
expect(renderEvents(events)).toContain("```ts cordis-catalog\n/**\n * A thing happened.\n * @param id - which thing.\n * @mode emit\n */\n'fix/happened'(id: string): void\n```")
})
it('classifies a trailing-next signature as a waterfall', () => {
@@ -158,26 +160,11 @@ export class FixService {
expect(services).toHaveLength(1)
expect(services[0]).toMatchObject({ key: 'fix', type: 'FixService', abstract: false, doc: 'Fixture service.' })
expect(services[0]?.methods).toHaveLength(3)
})
it('extracts an interface service as an abstract seam', () => {
const services = collectServices(makeService(`/** Fixture service interface. */
export interface FixService {
/**
* Do the thing.
* @param id - which thing to do.
* @returns the outcome of doing it.
*/
run(id: string): string
}`))
expect(services).toHaveLength(1)
expect(services[0]).toMatchObject({
key: 'fix',
type: 'FixService',
abstract: true,
doc: 'Fixture service interface.',
expect(services[0]?.methods[0]).toEqual({
signature: 'run(id: string): string',
jsDoc: '/**\n * Do the thing.\n * @param id - which thing to do.\n * @returns the outcome of doing it.\n */',
})
expect(services[0]?.methods).toEqual(['run(id: string): string'])
expect(renderServices(services)).toContain('```ts cordis-catalog\n/**\n * Do the thing.\n * @param id - which thing to do.\n * @returns the outcome of doing it.\n */\nrun(id: string): string\n\n/** Fire and forget (void needs no @returns). */\npoke(): void')
})
it('hard-errors on a public method with no JSDoc at all', () => {