fix(scope): close remaining ownership boundaries
This commit is contained in:
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import LlmService from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
|
||||
@@ -99,6 +99,22 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('createAgent sends raw metadata to the session validator before cloning can sanitize it', async () => {
|
||||
class ExoticMeta {
|
||||
readonly cwd = '/accepted'
|
||||
}
|
||||
const { ctx } = await persistentHarness(new MockAdapter([textResponse('hi')]))
|
||||
|
||||
await expect(ctx.agents.create({
|
||||
agentId: AgentId('exotic-meta-agent'),
|
||||
sessionId: SessionId('exotic-meta-session'),
|
||||
meta: new ExoticMeta(),
|
||||
})).rejects.toThrow(/session metadata is not a plain JSON record/)
|
||||
expect(ctx.agents.get(AgentId('exotic-meta-agent'))).toBeUndefined()
|
||||
expect(ctx.sessions.get(SessionId('exotic-meta-session'))).toBeUndefined()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('resume of a session with no cwd carries an undefined cwd header', async () => {
|
||||
// Lifecycle 1: create a no-cwd session and run a turn.
|
||||
const adapter1 = new MockAdapter([textResponse('a')])
|
||||
@@ -411,6 +427,54 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
|
||||
await ctx2.fiber.dispose()
|
||||
})
|
||||
|
||||
it('reads each loaded metadata field once before reconstructing a resumed session', async () => {
|
||||
const sessionId = SessionId('resume-loaded-meta-once')
|
||||
const root = await persistSession(sessionId)
|
||||
const ctx = await mountPersistentHarness(root, new MockAdapter([textResponse('next')]))
|
||||
const loaded = await ctx.sessionPersistence.load(sessionId)
|
||||
const reads = { createdAt: 0, cwd: 0, parentSession: 0, seedLength: 0 }
|
||||
const meta = Object.defineProperties({
|
||||
version: loaded.meta.version,
|
||||
id: loaded.meta.id,
|
||||
}, {
|
||||
createdAt: {
|
||||
enumerable: true,
|
||||
get: () => { reads.createdAt += 1; return reads.createdAt === 1 ? loaded.meta.createdAt : 1n },
|
||||
},
|
||||
cwd: {
|
||||
enumerable: true,
|
||||
get: () => { reads.cwd += 1; return reads.cwd === 1 ? '/loaded' : 'relative' },
|
||||
},
|
||||
parentSession: {
|
||||
enumerable: true,
|
||||
get: () => { reads.parentSession += 1; return reads.parentSession === 1 ? SessionId('parent') : 1n },
|
||||
},
|
||||
seedLength: {
|
||||
enumerable: true,
|
||||
get: () => { reads.seedLength += 1; return reads.seedLength === 1 ? 0 : 1n },
|
||||
},
|
||||
}) as unknown as SessionHeader
|
||||
ctx.sessionPersistence.load = () => Promise.resolve({ meta, events: loaded.events })
|
||||
|
||||
const resumed = await ctx.agents.resume({
|
||||
agentId: AgentId('resume-loaded-meta-once'),
|
||||
resumeSessionId: sessionId,
|
||||
agentOptions: { model: 'mock' },
|
||||
})
|
||||
|
||||
expect(reads).toEqual({ createdAt: 1, cwd: 1, parentSession: 1, seedLength: 1 })
|
||||
expect(resumed.agent.session.header).toEqual({
|
||||
version: loaded.meta.version,
|
||||
id: sessionId,
|
||||
createdAt: loaded.meta.createdAt,
|
||||
cwd: '/loaded',
|
||||
parentSession: 'parent',
|
||||
seedLength: 0,
|
||||
})
|
||||
await resumed.dispose()
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('an idle inject() is flushed durably on its own (survives without explicit flush/dispose)', async () => {
|
||||
// Lifecycle 1: run a turn, then inject context while idle. The idle inject
|
||||
// wraps its context/message in a one-shot turn AND checkpoints it (the turn-enclosure RFC)
|
||||
|
||||
@@ -605,7 +605,7 @@ describe('P1-5: a started turn (and any open step) is always closed on a boundar
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(Invariants, { freeze: false })
|
||||
await ctx.plugin(Invariants)
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
return ctx
|
||||
}
|
||||
@@ -1014,7 +1014,7 @@ describe('disposal/cancel honored during pre-step assembly (P1-1)', () => {
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(Invariants, { freeze: false })
|
||||
await ctx.plugin(Invariants)
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
|
||||
// Blocking listener on the parent context (survives fiber disposal).
|
||||
@@ -1071,7 +1071,7 @@ describe('disposal/cancel honored during pre-step assembly (P1-1)', () => {
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(Invariants, { freeze: false })
|
||||
await ctx.plugin(Invariants)
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
|
||||
const unlisten = ctx.on('system-prompt/assemble', async function (_assembly, _context, next) {
|
||||
@@ -1127,7 +1127,7 @@ describe('disposal/cancel honored during pre-step assembly (P1-1)', () => {
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(Invariants, { freeze: false })
|
||||
await ctx.plugin(Invariants)
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
|
||||
ctx.on('agent/pre-step', async () => {
|
||||
@@ -1179,7 +1179,7 @@ describe('disposal/cancel honored during pre-step assembly (P1-1)', () => {
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(Invariants, { freeze: false })
|
||||
await ctx.plugin(Invariants)
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
|
||||
ctx.on('agent/pre-step', async () => {
|
||||
@@ -1228,7 +1228,7 @@ describe('disposal/cancel honored during pre-step assembly (P1-1)', () => {
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(AgentRegistry)
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(Invariants, { freeze: false })
|
||||
await ctx.plugin(Invariants)
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
|
||||
ctx.on('system-prompt/assemble', async function (_assembly, _context, next) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import LlmService from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SessionStore, { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
import AgentRegistry, { AgentId, agentEvents, assembleContextFor } from '@deepseek-ai/dsh-agent'
|
||||
@@ -307,6 +307,36 @@ describe('agent scope lifecycle', () => {
|
||||
await retry.dispose()
|
||||
})
|
||||
|
||||
it('rejects an exotic seed before publishing either reserved identity', async () => {
|
||||
const ctx = await harness()
|
||||
const published: string[] = []
|
||||
ctx.on('session/created', () => { published.push('session') })
|
||||
ctx.on('agent/created', () => { published.push('agent') })
|
||||
class ExoticData { readonly value = 'not durable JSON' }
|
||||
const seed = [{
|
||||
seq: 0,
|
||||
type: 'test/exotic-seed',
|
||||
data: new ExoticData(),
|
||||
}] as unknown as SessionEvent[]
|
||||
|
||||
await expect(ctx.agents.create({
|
||||
agentId: AgentId('exotic-seed'),
|
||||
sessionId: SessionId('exotic-seed-session'),
|
||||
agentOptions: { model: 'mock' },
|
||||
seed,
|
||||
})).rejects.toThrow(/seed event at index 0 is not losslessly JSON-serializable/)
|
||||
|
||||
expect(published).toEqual([])
|
||||
expect(ctx.agents.get(AgentId('exotic-seed'))).toBeUndefined()
|
||||
expect(ctx.sessions.get(SessionId('exotic-seed-session'))).toBeUndefined()
|
||||
const retry = await ctx.agents.create({
|
||||
agentId: AgentId('exotic-seed'),
|
||||
sessionId: SessionId('exotic-seed-session'),
|
||||
agentOptions: { model: 'mock' },
|
||||
})
|
||||
await retry.dispose()
|
||||
})
|
||||
|
||||
it('a throwing session/created listener disposes the scope (pre-nesting rollback window)', async () => {
|
||||
const ctx = await harness()
|
||||
let boom = true
|
||||
|
||||
Reference in New Issue
Block a user