import { describe, expect, it } from 'vitest' import { ApprovalRequestId } from '../src/index.ts' import type { ApprovalRequestId as ApprovalRequestIdType, CordisDynamicPluginId, } from '../src/types.ts' import { AGENT_A, AGENT_B, CLIENT_CODE, setup, running } from './helpers.ts' /** * The runner's own chain on a real cordis tree: define records without running, * run starts a real host-half fiber and broadcasts one request, the first answer * settles it, and stop/undefine unwind both halves. Only the model and the * browser are stand-ins (code strings and a recording gateway). */ /** A host half that registers one invoke handler and provides a service. */ const HOST_CODE = ` harness.handle('double', async (args) => args.value * 2) return { name: 'doubler', apply(ctx) { ctx.provide('dynDoubler', { ok: true }) }, } ` type Runner = Awaited>['runner'] function define( runner: Runner, request: { sessionId: typeof AGENT_A.id name: string purpose: string host?: string client?: string }, ) { return runner.define({ sessionId: request.sessionId, plugin: { kind: 'new', idPrefix: 'dyn' }, name: request.name, purpose: request.purpose, code: { ...request.host === undefined ? {} : { host: request.host }, ...request.client === undefined ? {} : { client: request.client }, }, }) } describe('dynamic runner definitions', () => { it('lists the whole registry for a global surface, each row carrying its owning session', async () => { const { runner } = await setup() const mine = define(runner, { sessionId: AGENT_A.id, name: 'mine', purpose: 'ours', host: HOST_CODE }) const theirs = define(runner, { sessionId: AGENT_B.id, name: 'theirs', purpose: 'not ours', client: CLIENT_CODE }) // Global by design: a run-control surface that is not inside a session can // still name every package, and each row carries the address later verbs need. expect(runner.inventory()).toEqual([ { pluginId: mine.pluginId, agentId: AGENT_A.id, packages: [{ packageId: mine.packageId, name: 'mine', purpose: 'ours', hasHostHalf: true, hasClientHalf: false, }], }, { pluginId: theirs.pluginId, agentId: AGENT_B.id, packages: [{ packageId: theirs.packageId, name: 'theirs', purpose: 'not ours', hasHostHalf: false, hasClientHalf: true, }], }, ]) // Authority did not move with the listing: acting still needs the owner. await expect(runner.run(AGENT_A, theirs.pluginId, theirs.packageId, 'run')) .resolves.toMatchObject({ ok: false, reason: 'plugin-missing' }) }) it('tells a global surface which definitions even have a browser half to load', async () => { const { runner } = await setup() define(runner, { sessionId: AGENT_A.id, name: 'host only', purpose: 'no UI', host: HOST_CODE }) define(runner, { sessionId: AGENT_A.id, name: 'both halves', purpose: 'UI too', host: HOST_CODE, client: CLIENT_CODE, }) // A host-only package cannot be loaded into a page, so the surface must be // able to tell the two apart from the listing alone. expect(runner.inventory().map(row => [String(row.pluginId), row.packages[0]?.hasClientHalf])).toEqual([ ['dyn-1', false], ['dyn-2', true], ]) }) it('records a definition without running it, and mints ids that are never reused', async () => { const { runner } = await setup() const first = define(runner, { sessionId: AGENT_A.id, name: 'first', purpose: 'do a thing', host: HOST_CODE }) const second = define(runner, { sessionId: AGENT_A.id, name: 'second', purpose: 'do another', client: 'return () => {}' }) expect(first).toEqual({ pluginId: 'dyn-1', packageId: 'pkg-1', name: 'first', purpose: 'do a thing', hasHostHalf: true, hasClientHalf: false, }) expect(second).toEqual({ pluginId: 'dyn-2', packageId: 'pkg-2', name: 'second', purpose: 'do another', hasHostHalf: false, hasClientHalf: true, }) expect(running(runner, AGENT_A)).toEqual([{ id: 'dyn-1', running: false }, { id: 'dyn-2', running: false }]) }) it.each([ [{ name: ' ', purpose: 'p', host: 'return () => {}' }, 'non-empty `name`'], [{ name: 'n', purpose: '', host: 'return () => {}' }, 'non-empty `purpose`'], [{ name: 'n', purpose: 'p' }, 'needs `code.host`, `code.client`, or both'], ])('refuses an incomplete define request: %j', async (request, message) => { const { runner } = await setup() expect(() => define(runner, { sessionId: AGENT_A.id, ...request })).toThrow(message) }) it('keeps unparseable code out of the registry, teaching the TypeScript removal', async () => { const { runner } = await setup() expect(() => define(runner, { sessionId: AGENT_A.id, name: 'broken', purpose: 'p', client: 'return { type: \'text\' as const }', })).toThrow('The sandbox runs plain JavaScript, not TypeScript') expect(running(runner, AGENT_A)).toEqual([]) }) it('hides another session\'s definition, so only its own card can address it', async () => { const { runner } = await setup() const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'owned', purpose: 'p', host: HOST_CODE, }) expect(running(runner, AGENT_B)).toEqual([]) await expect(runner.run(AGENT_B, pluginId, packageId, 'run')) .resolves.toMatchObject({ ok: false, reason: 'plugin-missing' }) await expect(runner.stop(AGENT_B, pluginId)).resolves.toMatchObject({ ok: false, reason: 'plugin-missing' }) }) }) describe('dynamic runner dispatch', () => { it('starts a host-only package immediately, with no request and no approval', async () => { const { ctx, runner, gateway } = await setup() const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'doubler', purpose: 'p', host: HOST_CODE, }) const receipt = await runner.run(AGENT_A, pluginId, packageId, 'run') expect(receipt).toEqual({ ok: true, status: 'running', pluginId, packageId, pluginRunId: 'run-1', waitingFor: [], currentPackageId: packageId, mode: 'run', }) expect(ctx.get('dynDoubler')).toEqual({ ok: true }) // Its own business: the only announcement is the run-state one. expect(gateway.events).toEqual([ ['cordis/dynamic-package', { pluginId, packageId, pluginRunId: 'run-1', name: 'doubler' }], ]) await expect(runner.invoke(pluginId, 'run-1' as never, 'double', { value: 21 })) .resolves.toEqual({ ok: true, value: 42 }) expect(running(runner, AGENT_A)).toEqual([{ id: pluginId, running: true }]) }) it('returns awaiting approval, then records the page activation asynchronously', async () => { const { ctx, runner, gateway } = await setup() gateway.answer = 'approve' gateway.clientWaitingFor = ['slots'] const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'both', purpose: 'p', host: HOST_CODE, client: CLIENT_CODE, }) const receipt = await runner.run(AGENT_A, pluginId, packageId, 'run') expect(receipt).toEqual({ ok: true, status: 'awaiting-approval', pluginId, packageId, pluginRunId: 'run-1', mode: 'run', waitingFor: [], nextPackageId: packageId, }) await gateway.answering expect(ctx.get('dynDoubler')).toEqual({ ok: true }) expect(runner.inventory()[0]?.latestRun).toMatchObject({ status: 'waiting', client: { status: 'waiting', waitingFor: ['slots'] }, }) expect(gateway.events.map(([name]) => name)).toEqual([ 'cordis/request-run', 'cordis/dynamic-package', 'cordis/request-run-resolved', ]) expect(gateway.events.at(-1)?.[1]).toMatchObject({ outcome: 'approved' }) }) it('returns awaiting approval, then records a refusal without starting', async () => { const { ctx, runner, gateway } = await setup() gateway.answer = 'reject' const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'both', purpose: 'p', host: HOST_CODE, client: CLIENT_CODE, }) const receipt = await runner.run(AGENT_A, pluginId, packageId, 'run') expect(receipt).toMatchObject({ ok: true, status: 'awaiting-approval' }) await gateway.answering expect(ctx.get('dynDoubler')).toBeUndefined() expect(running(runner, AGENT_A)).toEqual([{ id: pluginId, running: false }]) expect(gateway.events.at(-1)).toMatchObject(['cordis/request-run-resolved', { outcome: 'rejected' }]) }) it('records an asynchronous Client failure and unwinds the Host half it started', async () => { const { ctx, runner, gateway } = await setup() gateway.answer = { clientFails: 'createElement is not defined' } const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'both', purpose: 'p', host: HOST_CODE, client: CLIENT_CODE, }) const receipt = await runner.run(AGENT_A, pluginId, packageId, 'run') expect(receipt).toMatchObject({ ok: true, status: 'awaiting-approval' }) await gateway.answering expect(runner.inventory()[0]?.latestRun).toMatchObject({ status: 'failed', error: { message: 'createElement is not defined' }, }) // Rollback restores the state the request found: nothing was running before. expect(ctx.get('dynDoubler')).toBeUndefined() expect(running(runner, AGENT_A)).toEqual([{ id: pluginId, running: false }]) expect(gateway.events.at(-1)?.[1]).toMatchObject({ outcome: 'failed' }) }) it('replaces a prior run and records failure when the repeated run cannot load Client code', async () => { const { ctx, runner, gateway } = await setup() const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'both', purpose: 'p', host: HOST_CODE, client: CLIENT_CODE, }) // A first page ran it; a second request finds it already up. gateway.answer = 'approve' await runner.run(AGENT_A, pluginId, packageId, 'run') await gateway.answering gateway.answer = { clientFails: 'this page could not load it' } const receipt = await runner.run(AGENT_A, pluginId, packageId, 'run') expect(receipt).toMatchObject({ ok: true, status: 'starting' }) await gateway.answering expect(runner.inventory()[0]?.latestRun).toMatchObject({ status: 'failed', error: { message: 'this page could not load it' }, }) expect(ctx.get('dynDoubler')).toBeUndefined() expect(running(runner, AGENT_A)).toEqual([{ id: pluginId, running: false }]) }) it('binds a running host half instead of evaluating it twice', async () => { const { runner } = await setup() const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'doubler', purpose: 'p', host: HOST_CODE, }) const first = await runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', null, false) const second = await runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', null, false) expect(first).toEqual({ ok: true, pluginId, packageId, pluginRunId: 'run-1', waitingFor: [], startedHere: true, }) // Re-evaluating would collide on the provided service; binding is what lets // a reloaded page take a live package back. expect(second).toEqual({ ok: true, pluginId, packageId, pluginRunId: 'run-1', waitingFor: [], startedHere: false, }) }) it('shares one activation when two pages start the same Package concurrently', async () => { const { runner } = await setup() const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'doubler', purpose: 'p', host: HOST_CODE, }) const [first, second] = await Promise.all([ runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', null, false), runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', null, false), ]) expect(first).toMatchObject({ ok: true, pluginRunId: 'run-1', startedHere: true }) expect(second).toEqual(first) }) it('hands the browser half\'s source only to the owning session, and only while it runs', async () => { const { runner } = await setup() const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'ui', purpose: 'p', client: CLIENT_CODE, }) expect(() => runner.getClientCode(AGENT_A, pluginId, 'run-0' as never)).toThrow('is not running') const started = await runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', null, false) if (!started.ok) throw new Error(started.message) expect(runner.getClientCode(AGENT_A, pluginId, started.pluginRunId)).toEqual({ code: CLIENT_CODE, name: 'ui', pluginId, packageId, pluginRunId: started.pluginRunId, }) expect(() => runner.getClientCode(AGENT_B, pluginId, started.pluginRunId)).toThrow('no dynamic plugin') }) it('accepts and ignores an answer to a request nobody is waiting for', async () => { const { runner } = await setup() await expect(runner.resolveRequestRun(ApprovalRequestId('approval-404'), { ok: true, pluginRunId: 'run-1' as never, })) .resolves.toEqual({ accepted: false }) }) it('refuses an answer after stop cancels the request and allows a fresh direct run', async () => { const { runner, gateway } = await setup() // No auto-answer: this suite drives the round trip by hand so the dispatch // can be replaced underneath the page that is still loading run 1. const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'ui', purpose: 'p', client: CLIENT_CODE, }) const pending = await runner.run(AGENT_A, pluginId, packageId, 'run') expect(pending).toMatchObject({ ok: true, status: 'awaiting-approval' }) await Promise.resolve() const asked = gateway.events.find(([name]) => name === 'cordis/request-run')?.[1] const requestId = (asked as { requestId: ApprovalRequestIdType }).requestId const first = await runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', requestId, false) if (!first.ok) throw new Error(first.message) expect(runner.getClientCode(AGENT_A, pluginId, first.pluginRunId).pluginRunId).toBe(first.pluginRunId) // The user stops it while that page is still loading, cancelling the request. await runner.stop(AGENT_A, pluginId) await expect(runner.resolveRequestRun(requestId, { ok: true, pluginRunId: first.pluginRunId })) .resolves.toEqual({ accepted: false }) expect(gateway.events).toContainEqual([ 'cordis/request-run-resolved', { requestId, outcome: 'cancelled' }, ]) await expect(runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', null, false)) .resolves.toMatchObject({ ok: true, pluginRunId: 'run-2', startedHere: true }) }) it('cancels a pending request after its provisional activation is stopped', async () => { const { runner, gateway } = await setup() const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'ui', purpose: 'p', client: CLIENT_CODE, }) const controller = new AbortController() const pending = await runner.run(AGENT_A, pluginId, packageId, 'run', controller.signal) expect(pending).toMatchObject({ ok: true, status: 'awaiting-approval' }) await Promise.resolve() const asked = gateway.events.find(([name]) => name === 'cordis/request-run')?.[1] const requestId = (asked as { requestId: ApprovalRequestIdType }).requestId const started = await runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', requestId, false) if (!started.ok) throw new Error(started.message) await runner.stop(AGENT_A, pluginId) await expect(runner.resolveRequestRun(requestId, { ok: true, pluginRunId: started.pluginRunId })) .resolves.toEqual({ accepted: false }) controller.abort() expect(gateway.events).toContainEqual([ 'cordis/request-run-resolved', { requestId, outcome: 'cancelled' }, ]) }) it('keeps a published request answerable after the creating Tool call ends', async () => { const { runner, gateway } = await setup() // No answer configured: the request stays pending until the signal fires. const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'ui', purpose: 'p', client: CLIENT_CODE, }) const controller = new AbortController() const pending = await runner.run(AGENT_A, pluginId, packageId, 'run', controller.signal) await Promise.resolve() controller.abort() expect(pending).toMatchObject({ ok: true, status: 'awaiting-approval' }) const asked = gateway.events.find(([name]) => name === 'cordis/request-run')?.[1] const requestId = (asked as { requestId: ApprovalRequestIdType }).requestId const started = await runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', requestId, false) if (!started.ok) throw new Error(started.message) await expect(runner.resolveRequestRun(requestId, { ok: true, pluginRunId: started.pluginRunId })) .resolves.toEqual({ accepted: true }) expect(running(runner, AGENT_A)).toEqual([{ id: pluginId, running: true }]) }) it('reports the sandbox failure and starts nothing when the host half throws', async () => { const { runner, gateway } = await setup() const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'broken', purpose: 'p', host: 'harness.handle(\'never\', async () => 1)\nthrow new Error(\'host half exploded\')', }) const receipt = await runner.run(AGENT_A, pluginId, packageId, 'run') expect(receipt).toMatchObject({ ok: false, reason: 'host-half-failed' }) expect(gateway.events).toEqual([]) expect(running(runner, AGENT_A)).toEqual([{ id: pluginId, running: false }]) await expect(runner.invoke(pluginId, 'run-1' as never, 'never', null)) .resolves.toMatchObject({ code: 'plugin-not-running' }) }) }) describe('dynamic runner teardown', () => { it('stops both halves while keeping the definition runnable', async () => { const { ctx, runner, gateway } = await setup() gateway.answer = 'approve' const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'doubler', purpose: 'p', host: HOST_CODE, client: CLIENT_CODE, }) const first = await runner.run(AGENT_A, pluginId, packageId, 'run') if (!first.ok) throw new Error(first.message) await gateway.answering await expect(runner.stop(AGENT_A, pluginId)).resolves.toEqual({ ok: true }) expect(ctx.get('dynDoubler')).toBeUndefined() await expect(runner.invoke(pluginId, first.pluginRunId, 'double', { value: 1 })) .resolves.toMatchObject({ code: 'plugin-not-running' }) expect(gateway.events.at(-1)).toEqual(['cordis/dynamic-retract', { pluginId, packageId, pluginRunId: first.pluginRunId, }]) expect(running(runner, AGENT_A)).toEqual([{ id: pluginId, running: false }]) // Runnable again, with a fresh activation identity. await expect(runner.run(AGENT_A, pluginId, packageId, 'run')) .resolves.toMatchObject({ ok: true, pluginRunId: 'run-2' }) }) it('announces the stop of a host-only package too, so a global surface drops its row', async () => { const { runner, gateway } = await setup() const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'doubler', purpose: 'p', host: HOST_CODE, }) await runner.run(AGENT_A, pluginId, packageId, 'run') await expect(runner.stop(AGENT_A, pluginId)).resolves.toEqual({ ok: true }) // The retract mirrors the start announcement: a run-control surface tracks // "is it running", which is independent of whether a browser half existed. expect(gateway.events).toEqual([ ['cordis/dynamic-package', { pluginId, packageId, pluginRunId: 'run-1', name: 'doubler' }], ['cordis/dynamic-retract', { pluginId, packageId, pluginRunId: 'run-1' }], ]) }) it('refuses to stop a definition that is not running', async () => { const { runner } = await setup() const { pluginId } = define(runner, { sessionId: AGENT_A.id, name: 'idle', purpose: 'p', host: HOST_CODE, }) await expect(runner.stop(AGENT_A, pluginId)).resolves.toMatchObject({ ok: false, reason: 'not-running' }) }) it('stops a running definition on undefine and forgets it', async () => { const { ctx, runner, gateway } = await setup() const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'doubler', purpose: 'p', host: HOST_CODE, }) await runner.run(AGENT_A, pluginId, packageId, 'run') await expect(runner.undefine(AGENT_A, pluginId)).resolves.toEqual({ ok: true, wasRunning: true }) expect(ctx.get('dynDoubler')).toBeUndefined() expect(running(runner, AGENT_A)).toEqual([]) expect(gateway.events.map(([name]) => name)) .toEqual(['cordis/dynamic-package', 'cordis/dynamic-retract']) await expect(runner.run(AGENT_A, pluginId, packageId, 'run')) .resolves.toMatchObject({ ok: false, reason: 'plugin-missing' }) }) it('answers a missing definition with the memory-only explanation', async () => { const { runner } = await setup() const receipt = await runner.undefine(AGENT_A, 'dyn-404' as CordisDynamicPluginId) expect(receipt).toMatchObject({ ok: false, reason: 'plugin-missing' }) expect((receipt as { message: string }).message).toContain('lost on DSH restart') }) it('unwinds every host half when the runner itself is disposed', async () => { const { ctx, runner } = await setup() const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'doubler', purpose: 'p', host: HOST_CODE, }) await runner.run(AGENT_A, pluginId, packageId, 'run') expect(ctx.get('dynDoubler')).toEqual({ ok: true }) await ctx.fiber.dispose() expect(ctx.get('dynDoubler')).toBeUndefined() }) }) describe('render failure reports', () => { it('keeps the last report per package and shows it to a snapshot reader', async () => { const { runner } = await setup() const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'ui', purpose: 'renders', client: CLIENT_CODE, }) const started = await runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', null, false) if (!started.ok) throw new Error(started.message) await runner.reportRenderFailure( AGENT_A, pluginId, started.pluginRunId, { slot: 'settings.section', message: 'boom', abdicated: true }, ) // Cross-page and last-writer-wins: a second page reporting overwrites, // because "did this package's UI fail anywhere" has one answer. await runner.reportRenderFailure( AGENT_A, pluginId, started.pluginRunId, { slot: 'shell.overlay', message: 'later', abdicated: false }, ) expect(runner.snapshot(AGENT_A)[0]?.activeRun?.renderFailure) .toEqual({ slot: 'shell.overlay', message: 'later', abdicated: false }) }) it('drops a report for a definition the reporting session does not own', async () => { const { runner } = await setup() const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'ui', purpose: 'renders', client: CLIENT_CODE, }) const started = await runner.runHostHalf(AGENT_A, pluginId, packageId, 'run', null, false) if (!started.ok) throw new Error(started.message) // The reporting path must never fail a render, so a report it cannot place // is dropped rather than thrown. await expect(runner.reportRenderFailure( AGENT_B, pluginId, started.pluginRunId, { slot: 's', message: 'm', abdicated: true }, )) .resolves.toBeNull() expect(runner.snapshot(AGENT_A)[0]?.activeRun?.renderFailure).toBeUndefined() }) it('clears the report when a fresh dispatch starts and when one stops', async () => { const { runner } = await setup() const { pluginId, packageId } = define(runner, { sessionId: AGENT_A.id, name: 'ui', purpose: 'renders', host: 'return () => {}', }) const first = await runner.run(AGENT_A, pluginId, packageId, 'run') if (!first.ok) throw new Error(first.message) await runner.reportRenderFailure( AGENT_A, pluginId, first.pluginRunId, { slot: 'settings.section', message: 'boom', abdicated: true }, ) expect(runner.snapshot(AGENT_A)[0]?.activeRun?.renderFailure).toBeDefined() // Stop clears it: nothing is mounted to have failed any more. await runner.stop(AGENT_A, pluginId) expect(runner.snapshot(AGENT_A)[0]?.activeRun?.renderFailure).toBeUndefined() await runner.reportRenderFailure( AGENT_A, pluginId, first.pluginRunId, { slot: 'settings.section', message: 'boom', abdicated: true }, ) // A fresh dispatch clears it too: a failure from the previous run would // describe something that is no longer there. await runner.run(AGENT_A, pluginId, packageId, 'run') expect(runner.snapshot(AGENT_A)[0]?.activeRun?.renderFailure).toBeUndefined() }) })