import { describe, expect, it } from 'vitest' import type { Context, Fiber } from 'cordis' import { FiberState } from '../src/fiber-state.ts' import { describeApi, describeEvents, describePlugins, describeServices } from '../src/inspect.ts' import { call, LISTENER_CODE, setup, text } from './helpers.ts' /** * The `cordis_inspect` sections: rendered against the real runtime through the * tool, plus direct renderer calls for the states a minimal harness cannot * reach (empty service store, same-named sibling fibers, a fully-live catalog). */ describe('cordis_inspect', () => { it('reports all six sections by default', async () => { const ctx = await setup() const result = await call(ctx, 'cordis_inspect', {}) expect(result.isError).toBe(false) const report = text(result) if (result.isError) throw new Error('expected cordis_inspect success') expect(result.value).toBe(report) for (const heading of ['services', 'plugins', 'tools', 'dynamic', 'api', 'events']) { expect(report).toContain(`## ${heading}`) } // The services section sees the real providers; the plugins list shows // this plugin and its dynamic group flat; the tools section lists the // cordis tools. expect(report).toContain('- tools (provided by ToolRegistry)') expect(report).toContain('- tool-cordis [active]') expect(report).toContain('- cordis-dynamic [active]') expect(report).toContain('- cordis_mount') expect(report).toContain('(no dynamic plugins mounted)') }) it('limits the report to one section via `what`', async () => { const ctx = await setup() const result = await call(ctx, 'cordis_inspect', { what: 'tools' }) const report = text(result) expect(report).toContain('## tools') expect(report).not.toContain('## services') expect(report).not.toContain('## plugins') }) it('shows a mount in the dynamic section and in the flat plugins list', async () => { const ctx = await setup() await call(ctx, 'cordis_mount', { code: LISTENER_CODE }) const report = text(await call(ctx, 'cordis_inspect', {})) expect(report).toContain('- dyn-1: change-logger [active]') expect(report).toContain('- change-logger [active]') }) it('renders the api section from the generated catalog intersected with the LIVE runtime', async () => { const ctx = await setup() const report = text(await call(ctx, 'cordis_inspect', { what: 'api' })) // Live catalogued services render summary + signatures. expect(report).toContain('- tools — ') expect(report).toContain('register(definition: ToolDefinition)') expect(report).toContain('- systemPrompt — ') // Catalogued services with no live provider are listed tersely. expect(report).toMatch(/not running \(loadable services with no live provider\): .*bash/) // The type shapes the LIVE signatures reference follow (closure over the // generated TYPE_API — a consumer can see field types, not just names). expect(report).toContain('type shapes (referenced by the signatures above') expect(report).toContain('export interface ToolExecution') expect(report).toContain('export class Session') expect(report).toContain('export interface SessionSurface') // A type only reachable through a NOT-live service (e.g. bash) is scoped out. expect(report).not.toContain('export interface BashRunResult') // The inherited ctx surface closes the section. expect(report).toContain('inherited ctx API:') expect(report).toContain('- ctx.effect — ') // The broad report stays compact; exact-name lookup owns full JSDoc. expect(report).not.toContain('/**') expect(report).not.toContain('@param definition') }) it('adds original method JSDoc only for an exact live api name', async () => { const ctx = await setup() const report = text(await call(ctx, 'cordis_inspect', { what: 'api', name: 'tools' })) expect(report).toContain('## api') expect(report).toContain('- tools — Tool registry and execution pipeline.') expect(report).toContain('/**') expect(report).toContain('Register globally or in the calling agent scope.') expect(report).toContain('@param definition - tool schema, execution, and optional finalization/presentation callbacks') expect(report).toContain('@returns the exact disposer') expect(report).toContain('register(definition: ToolDefinition)') expect(report).toContain('type shapes (referenced by the signatures above') expect(report).not.toContain('not running (loadable services') expect(report).not.toContain('inherited ctx API:') }) it('renders the events section with mode badges, signatures, and the waterfall caution', async () => { const ctx = await setup() const report = text(await call(ctx, 'cordis_inspect', { what: 'events' })) expect(report).toContain('- tools/change [emit]') expect(report).toContain('- tools/pre-execute [waterfall]') expect(report).toMatch(/'agent\/status'\(/) expect(report).toContain('returning without next() vetoes the chain') expect(report).not.toContain('/**') expect(report).not.toContain('@mode waterfall') }) it('adds original event JSDoc only for an exact event name', async () => { const ctx = await setup() const report = text(await call(ctx, 'cordis_inspect', { what: 'events', name: 'tools/pre-execute' })) expect(report).toContain('## events') expect(report).toContain('- tools/pre-execute [waterfall]') expect(report).toContain('/**') expect(report).toContain('Allow, deny, or ask before dispatch.') expect(report).toContain('@param exec - the pending call') expect(report).toContain('@mode waterfall') expect(report).not.toContain('- tools/change [emit]') }) it('fails loud for incompatible, unknown, and non-running names', async () => { const ctx = await setup() const incompatible = await call(ctx, 'cordis_inspect', { what: 'tools', name: 'tools' }) expect(incompatible.isError).toBe(true) expect(text(incompatible)).toContain('name is valid only with what:"api" or what:"events"') const unknownService = await call(ctx, 'cordis_inspect', { what: 'api', name: 'not-a-service' }) expect(unknownService.isError).toBe(true) expect(text(unknownService)).toContain('no catalogued service named "not-a-service"') const nonRunning = await call(ctx, 'cordis_inspect', { what: 'api', name: 'bash' }) expect(nonRunning.isError).toBe(true) expect(text(nonRunning)).toContain('catalogued service "bash" is not running') const unknownEvent = await call(ctx, 'cordis_inspect', { what: 'events', name: 'not/an-event' }) expect(unknownEvent.isError).toBe(true) expect(text(unknownEvent)).toContain('no catalogued event named "not/an-event"') }) }) describe('inspect renderers (direct)', () => { it('describeServices reports an empty store as such, and labels a non-active provider', () => { const empty = { reflect: { store: {} } } as unknown as Context expect(describeServices(empty)).toEqual(['(no services provided)']) const pendingFiber = { state: FiberState.PENDING, name: 'half-loaded' } as unknown as Fiber const store: Record = {} store[Symbol('impl')] = { name: 'thing', fiber: pendingFiber } const ctx = { reflect: { store } } as unknown as Context expect(describeServices(ctx)).toEqual(['- thing (provided by half-loaded, pending)']) }) it('describePlugins lists every fiber flat, sorted by name, one line per instance', () => { const fiber = (name: string): Fiber => ({ name, state: FiberState.ACTIVE }) as unknown as Fiber const ctx = { registry: { values: () => [{ fibers: [fiber('beta'), fiber('alpha')] }, { fibers: [fiber('alpha')] }] }, } as unknown as Context expect(describePlugins(ctx)).toEqual([ '- alpha [active]', '- alpha [active]', '- beta [active]', ]) }) it('describeApi omits the not-running line and type shapes when nothing applies', async () => { const ctx = await setup() const lines = describeApi(ctx, [{ key: 'tools', summary: 'The registry.', methods: [{ signature: 'register(x): void', jsDoc: '/** Register x. */' }], }], [], []) expect(lines[0]).toBe('- tools — The registry.') expect(lines[1]).toBe(' register(x): void') expect(lines.join('\n')).not.toContain('not running') expect(lines.join('\n')).not.toContain('type shapes') }) it('describeEvents renders an empty catalog as just the waterfall caution', () => { expect(describeEvents([])).toEqual([ 'waterfall listeners receive a trailing next() and MUST call it to delegate — returning without next() vetoes the chain.', ]) }) })