Merge branch 'stack/agent-profiles-3-wire' into stack/agent-profiles-5-web-ui
recompose becomes a parent re-link: the new preset's standing mount is ensured BEFORE the link moves, so a failed switch leaves the agent exactly as it was — the unmount-then-restore dance (and unmountPresetFor with it) is gone, and the restore-failure test now asserts the agent KEEPS its tools with the source directory deleted, because the standing mount is not the file.
This commit is contained in:
@@ -38,7 +38,7 @@ async function harness(): Promise<Context> {
|
||||
}
|
||||
|
||||
describe('agent-presets invariants', () => {
|
||||
it('tracks a mounted composition and forgets it once the agent is gone', async () => {
|
||||
it('keeps the standing composition alive across the agents that joined it', async () => {
|
||||
const ctx = await harness()
|
||||
const handle = await ctx.agents.create({
|
||||
sessionId: SessionId('inv-live'),
|
||||
@@ -47,8 +47,20 @@ describe('agent-presets invariants', () => {
|
||||
|
||||
expect(livePresetMounts().map(mount => mount.presetId)).toContain('standard')
|
||||
|
||||
// A standing mount survives its agents: the composition a session joined
|
||||
// is shared, so one session ending must not strip it from the next.
|
||||
await handle.dispose()
|
||||
expect(livePresetMounts().map(mount => mount.presetId)).toContain('standard')
|
||||
|
||||
// A second agent reuses the same mount rather than adding one.
|
||||
await ctx.agents.create({
|
||||
sessionId: SessionId('inv-live-2'),
|
||||
setup: async (agentCtx: Context) => void await ctx.agentPresets.mount(agentCtx, 'standard'),
|
||||
})
|
||||
expect(livePresetMounts().filter(mount => mount.presetId === 'standard')).toHaveLength(1)
|
||||
|
||||
// Whole-tree teardown is the boundary that does reclaim it.
|
||||
await ctx.fiber.dispose()
|
||||
expect(livePresetMounts().map(mount => mount.presetId)).not.toContain('standard')
|
||||
})
|
||||
|
||||
|
||||
@@ -164,21 +164,21 @@ describe('rejecting a composition that cannot be used', () => {
|
||||
expect(rootResolves(ctx, 'fixtureIsolatedSvc')).toBe(false)
|
||||
})
|
||||
|
||||
it('addresses one agent\'s instance of a realm-private service', async () => {
|
||||
it('addresses the standing instance of a realm-private service through either agent', async () => {
|
||||
const first = await agentOn(ctx, 'sess-reach-a', 'isolated')
|
||||
const second = await agentOn(ctx, 'sess-reach-b', 'isolated')
|
||||
|
||||
// The realm keeps the service out of every host context — that is what
|
||||
// makes it per session — so a caller holding the agent is the only way a
|
||||
// request from OUTSIDE the session can read the instance it is about.
|
||||
// The realm keeps the service out of every host context, so a caller
|
||||
// holding the agent is how a request from OUTSIDE the session reads the
|
||||
// instance it is about.
|
||||
expect(rootResolves(ctx, 'fixtureIsolatedSvc')).toBe(false)
|
||||
const mine = ctx.agentPresets.serviceFor(first, 'fixtureIsolatedSvc')
|
||||
const theirs = ctx.agentPresets.serviceFor(second, 'fixtureIsolatedSvc')
|
||||
expect(mine).toBeDefined()
|
||||
expect(theirs).toBeDefined()
|
||||
// Each agent gets ITS own: the addressing is per subtree, not a lookup
|
||||
// that happens to find the first match.
|
||||
expect(mine).not.toBe(theirs)
|
||||
// ONE composition per preset: both agents joined the same standing mount,
|
||||
// so they address the same instance — sessions stay apart inside it by
|
||||
// the plugin's own Session/Agent keying, not by instance count.
|
||||
expect(theirs).toBe(mine)
|
||||
})
|
||||
|
||||
it('answers undefined for a service the agent\'s preset does not mount', async () => {
|
||||
@@ -342,7 +342,7 @@ describe('replacing a composition', () => {
|
||||
expect(toolNames(ctx, handle.agent)).toEqual([])
|
||||
})
|
||||
|
||||
it('reports the switch failure when the restore fails too', async () => {
|
||||
it('keeps the agent on its standing composition when a switch fails, even with the source deleted', async () => {
|
||||
// A preset root this test owns, so removing the composition mid-flight
|
||||
// cannot disturb the shipped fixtures.
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-preset-restore-'))
|
||||
@@ -368,14 +368,17 @@ describe('replacing a composition', () => {
|
||||
})
|
||||
|
||||
// The roster is a live directory: the composition the agent came from can
|
||||
// be gone by the time the restore reaches for it.
|
||||
// be gone from DISK by the time a switch fails. The standing mount is not
|
||||
// the file — it outlives deletion, so there is nothing to "restore".
|
||||
await rm(join(root, 'first'), { recursive: true })
|
||||
|
||||
await expect(scoped.agentPresets.recompose(handle.agent.ctx, 'broken'))
|
||||
.rejects.toThrow(/failed to mount/)
|
||||
|
||||
// The switch failure is the actionable one; the restore's is swallowed.
|
||||
expect(toolNames(scoped, handle.agent)).toEqual([])
|
||||
// The failed switch left the agent EXACTLY as it was: the new standing
|
||||
// mount is ensured before the parent link moves, so a rejection never
|
||||
// strips the old composition.
|
||||
expect(toolNames(scoped, handle.agent)).toEqual(['only'])
|
||||
})
|
||||
|
||||
it('refuses an unscoped context', async () => {
|
||||
|
||||
Reference in New Issue
Block a user