fix(tasks-local): return the layer disposer directly and cover scoped teardown
`ScopedLayers.effect` already returns an exact `() => void`, so the inherited `() => void dispose()` wrapper voided a void — two lint rules, four errors. The scoped layer's own teardown had no test, which is the registry-contribution disposal contract the testing policy requires and the only path that calls `TaskLayer.isEmpty()`: `ScopedLayers` prunes a scope's layer when its last contribution disposes. The new case mounts one plugin contributing both a surface and a listener into one scope, then unloads it and observes that the agents which joined that scope are refused again. Refs #2141
This commit is contained in:
@@ -238,23 +238,21 @@ export class LocalTaskService extends TaskService {
|
||||
}
|
||||
|
||||
onTaskDone(listener: TaskDoneListener): () => void {
|
||||
const dispose = this.layers.effect(
|
||||
return this.layers.effect(
|
||||
this.ctx,
|
||||
layer => layer.listeners.append(listener),
|
||||
{ label: 'tasks.onTaskDone()' },
|
||||
)
|
||||
return () => void dispose()
|
||||
}
|
||||
|
||||
attachSurface(name: string): () => void {
|
||||
// One token per call keeps duplicate labels independently disposable.
|
||||
const token = Symbol(name)
|
||||
const dispose = this.layers.effect(
|
||||
return this.layers.effect(
|
||||
this.ctx,
|
||||
layer => layer.surfaces.append(token),
|
||||
{ label: 'tasks.attachSurface()' },
|
||||
)
|
||||
return () => void dispose()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -805,6 +805,30 @@ describe('LocalTaskService disposal', () => {
|
||||
expect(ownerEffects()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('drops a scoped layer when its registrations dispose', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(LocalTaskService)
|
||||
const standing = createScope(ctx, {})
|
||||
// One mount contributes both kinds into the same layer, as `tool-tasks`
|
||||
// does; unloading it must leave nothing serving the agents that joined it.
|
||||
const mount = await standing.ctx.plugin({
|
||||
inject: ['tasks'],
|
||||
apply(pluginCtx: Context) {
|
||||
pluginCtx.tasks.attachSurface('tool-tasks')
|
||||
pluginCtx.tasks.onTaskDone(() => {})
|
||||
},
|
||||
})
|
||||
const owner = stubAgent(ctx, 'joined', scopeOf(standing.ctx))
|
||||
ctx.agents.register(owner)
|
||||
expect(() => ctx.tasks.start(producer({ owner }).spec)).not.toThrow()
|
||||
|
||||
await mount.dispose()
|
||||
|
||||
expect(() => ctx.tasks.start(producer({ owner }).spec))
|
||||
.toThrow('no control surface serves this agent')
|
||||
})
|
||||
|
||||
it('detaching the last surface re-arms the register fence', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LocalTaskService)
|
||||
|
||||
Reference in New Issue
Block a user