|
|
@@ -187,9 +187,11 @@ function stubAgent(cwd?: string, seed: SessionEvent[] = []): Agent {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function stubToolExecution(input: Omit<ToolExecution, 'token'>): ToolExecution {
|
|
|
+function stubToolExecution(
|
|
|
+ input: Omit<ToolExecution, 'token'> & { token?: ToolExecutionToken },
|
|
|
+): ToolExecution {
|
|
|
return {
|
|
|
- token: Symbol('workspace-context-test-execution') as ToolExecutionToken,
|
|
|
+ token: input.token ?? Symbol('workspace-context-test-execution') as ToolExecutionToken,
|
|
|
...input,
|
|
|
}
|
|
|
}
|
|
|
@@ -3948,6 +3950,106 @@ describe('dynamic nested workspace context injection', () => {
|
|
|
}
|
|
|
})
|
|
|
|
|
|
+ it('defers a nested file projection until the enclosing step commits', async () => {
|
|
|
+ const root = await tempRepo()
|
|
|
+ const home = await tempRepo()
|
|
|
+ const ctx = new Context()
|
|
|
+ try {
|
|
|
+ await ctx.plugin(RecordingFileSystem)
|
|
|
+ await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
|
|
|
+ const fs = ctx.fs as RecordingFileSystem
|
|
|
+ fs.entries.set(join(root, '.git'), { type: 'directory' })
|
|
|
+ fs.entries.set(join(root, 'pkg/AGENTS.md'), { type: 'file', content: 'nested package rule' })
|
|
|
+ const agent = stubAgent(root)
|
|
|
+ const turnStart = agent.session.append('turn/start', { turn: 1 })
|
|
|
+ ctx.emit('session/event', agent.session, turnStart)
|
|
|
+ const stepStart = agent.session.append('step/start', { turn: 1, step: 1 })
|
|
|
+ ctx.emit('session/event', agent.session, stepStart)
|
|
|
+ const outerToken = Symbol('outer-code-run') as ToolExecutionToken
|
|
|
+
|
|
|
+ ctx.emit('tools/result', stubToolExecution({
|
|
|
+ token: Symbol('nested-read') as ToolExecutionToken,
|
|
|
+ parent: outerToken,
|
|
|
+ signal: testToolSignal,
|
|
|
+ callId: CallId('nested-read'),
|
|
|
+ name: 'read',
|
|
|
+ arguments: { file_path: join('pkg', 'file.txt') },
|
|
|
+ agent,
|
|
|
+ }), { content: [], isError: false, value: null })
|
|
|
+ ctx.emit('tools/result', stubToolExecution({
|
|
|
+ token: Symbol('nested-non-file') as ToolExecutionToken,
|
|
|
+ parent: outerToken,
|
|
|
+ signal: testToolSignal,
|
|
|
+ callId: CallId('nested-non-file'),
|
|
|
+ name: 'search',
|
|
|
+ arguments: {},
|
|
|
+ agent,
|
|
|
+ }), { content: [], isError: false, value: null })
|
|
|
+ ctx.emit('tools/result', stubToolExecution({
|
|
|
+ token: Symbol('second-nested-read') as ToolExecutionToken,
|
|
|
+ parent: outerToken,
|
|
|
+ signal: testToolSignal,
|
|
|
+ callId: CallId('second-nested-read'),
|
|
|
+ name: 'read',
|
|
|
+ arguments: { file_path: join('pkg', 'second.txt') },
|
|
|
+ agent,
|
|
|
+ }), { content: [], isError: false, value: null })
|
|
|
+ ctx.emit('tools/result', stubToolExecution({
|
|
|
+ token: outerToken,
|
|
|
+ signal: testToolSignal,
|
|
|
+ callId: CallId('outer-code-run'),
|
|
|
+ name: 'run_code',
|
|
|
+ arguments: {},
|
|
|
+ agent,
|
|
|
+ }), { content: [], isError: false, value: null })
|
|
|
+
|
|
|
+ await syncWorkspaceContext(ctx, agent)
|
|
|
+ expect(agent.inbox.nextStep).toEqual([])
|
|
|
+
|
|
|
+ const stepEnd = agent.session.append('step/end', { turn: 1, step: 1 })
|
|
|
+ ctx.emit('session/event', agent.session, stepEnd)
|
|
|
+ expect(blocksText((await syncedWorkspaceContext(ctx, agent)).content))
|
|
|
+ .toContain('nested package rule')
|
|
|
+ } finally {
|
|
|
+ await ctx.fiber.dispose()
|
|
|
+ await rm(root, { recursive: true, force: true })
|
|
|
+ await rm(home, { recursive: true, force: true })
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ it('seeds closed step state from existing session history', async () => {
|
|
|
+ const root = await tempRepo()
|
|
|
+ const home = await tempRepo()
|
|
|
+ const ctx = new Context()
|
|
|
+ try {
|
|
|
+ await ctx.plugin(RecordingFileSystem)
|
|
|
+ const fs = ctx.fs as RecordingFileSystem
|
|
|
+ fs.entries.set(join(root, '.git'), { type: 'directory' })
|
|
|
+ fs.entries.set(join(root, 'pkg/AGENTS.md'), { type: 'file', content: 'nested package rule' })
|
|
|
+ const agent = stubAgent(root)
|
|
|
+ agent.session.append('turn/start', { turn: 1 })
|
|
|
+ agent.session.append('step/start', { turn: 1, step: 1 })
|
|
|
+ agent.session.append('step/end', { turn: 1, step: 1 })
|
|
|
+ agent.session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
|
|
+ await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
|
|
|
+
|
|
|
+ ctx.emit('tools/result', stubToolExecution({
|
|
|
+ signal: testToolSignal,
|
|
|
+ callId: CallId('read-after-closed-step'),
|
|
|
+ name: 'read',
|
|
|
+ arguments: { file_path: join('pkg', 'file.txt') },
|
|
|
+ agent,
|
|
|
+ }), { content: [], isError: false, value: null })
|
|
|
+
|
|
|
+ expect(blocksText((await syncedWorkspaceContext(ctx, agent)).content))
|
|
|
+ .toContain('nested package rule')
|
|
|
+ } finally {
|
|
|
+ await ctx.fiber.dispose()
|
|
|
+ await rm(root, { recursive: true, force: true })
|
|
|
+ await rm(home, { recursive: true, force: true })
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
it('ignores failed, aborted, agentless, and non-file final results', async () => {
|
|
|
const ctx = new Context()
|
|
|
try {
|