2a40cbf8ef
git mv timeout-policy beside repeat-tool-guard (both are loop-hygiene policies on the tool-execution pipeline, and the timeout/ group name collided with util/timeout) and tool-cordis into self-modification/ (naming the role the framework name obscured). Merged/renamed group README triplets, tsconfig globs, generator sources, hierarchy tables, catalogs, and the timeout-policy design note's group references follow. Adds the fifth FIXME marker (dsh-timeout-guard, recorded as a suggestion to settle at resolution time). guard + self-modification suites: 197 passed.
85 lines
3.5 KiB
TypeScript
85 lines
3.5 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { Context } from 'cordis'
|
|
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
|
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
|
import * as tool from '../src/index.ts'
|
|
import { call, dummyTool, LISTENER_CODE, REVERSE_TOOL_CODE, setup, text } from './helpers.ts'
|
|
|
|
/**
|
|
* Disposal semantics: `cordis_unmount` reaches quiescence before returning,
|
|
* and disposing the tool-cordis fiber itself (the HMR path) cascades over the
|
|
* whole dynamic subtree through the ordinary parent→child fiber lifecycle.
|
|
*/
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
describe('cordis_unmount', () => {
|
|
it('disposes the mount and its registrations have stopped by the time it returns (quiescence)', async () => {
|
|
const ctx = await setup()
|
|
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
await call(ctx, 'cordis_mount', { code: LISTENER_CODE })
|
|
|
|
ctx.tools.register(dummyTool('trigger_before'))
|
|
expect(log).toHaveBeenCalledTimes(1)
|
|
|
|
const result = await call(ctx, 'cordis_unmount', { id: 'dyn-1' })
|
|
expect(result.isError).toBe(false)
|
|
if (result.isError) throw new Error('expected cordis_unmount success')
|
|
expect(result.value).toEqual({ id: 'dyn-1', pluginName: 'change-logger' })
|
|
expect(text(result)).toBe('Temporary Plugin dyn-1 was unmounted and removed.')
|
|
|
|
// Immediately after the awaited unmount, the listener must be gone — no
|
|
// grace period, no eventual consistency.
|
|
ctx.tools.register(dummyTool('trigger_after'))
|
|
expect(log).toHaveBeenCalledTimes(1)
|
|
expect(text(await call(ctx, 'cordis_inspect', { what: 'temporary' }))).toContain('No temporary Plugins are running.')
|
|
})
|
|
|
|
it('unregisters a self-made tool on unmount', async () => {
|
|
const ctx = await setup()
|
|
await call(ctx, 'cordis_mount', { code: REVERSE_TOOL_CODE })
|
|
expect(ctx.tools.get('reverse_text')).toBeDefined()
|
|
|
|
await call(ctx, 'cordis_unmount', { id: 'dyn-1' })
|
|
expect(ctx.tools.get('reverse_text')).toBeUndefined()
|
|
})
|
|
|
|
it('rejects an unknown id, and a second unmount of the same id', async () => {
|
|
const ctx = await setup()
|
|
const unknown = await call(ctx, 'cordis_unmount', { id: 'dyn-99' })
|
|
expect(unknown.isError).toBe(true)
|
|
expect(text(unknown)).toContain('no temporary Plugin with id "dyn-99"')
|
|
|
|
await call(ctx, 'cordis_mount', { code: LISTENER_CODE })
|
|
await call(ctx, 'cordis_unmount', { id: 'dyn-1' })
|
|
const again = await call(ctx, 'cordis_unmount', { id: 'dyn-1' })
|
|
expect(again.isError).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('HMR safety', () => {
|
|
it('disposing the tool-cordis fiber cascades over the dynamic subtree and its registrations', async () => {
|
|
const ctx = new Context()
|
|
await ctx.plugin(SystemPrompt)
|
|
await ctx.plugin(ToolRegistry)
|
|
const fiber = await ctx.plugin(tool)
|
|
|
|
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
await call(ctx, 'cordis_mount', { code: LISTENER_CODE })
|
|
await call(ctx, 'cordis_mount', { code: REVERSE_TOOL_CODE })
|
|
expect(ctx.tools.get('reverse_text')).toBeDefined()
|
|
|
|
await fiber.dispose()
|
|
|
|
// The whole subtree is gone: the self-made tool, the cordis tools, and the
|
|
// mounted listener (no log on a fresh tools/change).
|
|
expect(ctx.tools.get('reverse_text')).toBeUndefined()
|
|
expect(ctx.tools.get('cordis_mount')).toBeUndefined()
|
|
const calls = log.mock.calls.length
|
|
ctx.tools.register(dummyTool('trigger_post_dispose'))
|
|
expect(log).toHaveBeenCalledTimes(calls)
|
|
})
|
|
})
|