Fix Code Mode workspace context propagation

This commit is contained in:
Yichen Jiang
2026-07-13 13:56:45 +08:00
parent c748f30055
commit 768c79fd45
51 changed files with 1040 additions and 265 deletions

View File

@@ -6,7 +6,7 @@ Per-session workspace instruction loading for `AGENTS.md`-compatible files. The
The baseline is composed once per agent-loop instance on `agent/session-prefix`. It reads `$DSH_HOME/AGENTS.md` followed by one configured instruction candidate in each directory from the project root to `agent.session.header.cwd`. The prefix is placed before all derived history, recorded in `EpochHeader.messagePrefix`, and reused verbatim for that loop instance. Because the plugin prepends its contribution before delegating, a later-registered skills catalog appears after workspace instructions.
The plugin also listens on `tools/post-execute` for successful first-party `read`, `write`, and `edit` calls. Each touch checks newly reached descendant scopes and every previously loaded scope. A new file is attached as `additionalContext`; a changed file or candidate switch appends a replacement; a missing final candidate appends a removal notice. This follows structured filesystem activity rather than shell `cd`, because each local bash call starts a fresh shell and parsing arbitrary shell syntax would be unreliable.
The plugin also listens on `tools/post-execute` for successful first-party `read`, `write`, and `edit` calls. Each touch checks newly reached descendant scopes and every previously loaded scope. A new file is attached through the result's `additionalContexts`; a changed file or candidate switch appends a replacement; a missing final candidate appends a removal notice. Native calls and Code Mode sub-dispatches share this path: `run_code` defers each nested context until its outer result, so the loop still appends updates after tool-call/result adjacency is complete. This follows structured filesystem activity rather than shell `cd`, because each local bash call starts a fresh shell and parsing arbitrary shell syntax would be unreliable.
Instruction reads use the optional `ctx.fs` provider. The plugin does not statically inject `fs`, so providerless product trees still boot and instruction loading becomes a no-op until a provider is present. It calls `ctx.fs.lstat` before resolving a candidate, rejecting a final-component symlink instead of following repository-owned links across the trust boundary. A provider failure after a file was loaded is treated as temporarily unavailable, not as proof that the file was deleted.

View File

@@ -20,7 +20,6 @@ import {
} from './files.ts'
import {
baselineInstructionChanges,
concatContext,
dynamicInstructionContext,
name,
reconcileInstructionContext,
@@ -114,7 +113,7 @@ export function apply(ctx: Context, config: Config): void {
return {
kind: 'accept',
...downstream.content !== undefined ? { content: downstream.content } : {},
additionalContext: concatContext(context, downstream.additionalContext),
additionalContexts: [context, ...downstream.additionalContexts ?? []],
}
})
}

View File

@@ -6,7 +6,7 @@
import type { Agent, HookContext } from '@deepseek-ai/dsh-agent'
import type { Message } from '@deepseek-ai/dsh-llm'
import { renderContextContent, type JsonValue } from '@deepseek-ai/dsh-session'
import type { JsonValue } from '@deepseek-ai/dsh-session'
import type { FileSystem } from '@deepseek-ai/dsh-fs'
import type { ToolExecution, ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import type { ResolvedConfig } from './config.ts'
@@ -65,23 +65,6 @@ export function workspaceContextMessage(text: string): Message {
return { role: 'user', content: [{ type: 'text', text }] }
}
/**
* Preserve workspace state ownership while folding a downstream context contribution.
* @param ours - workspace raw context and structured metadata.
* @param theirs - optional downstream context with its own envelope semantics.
* @returns one workspace-owned context containing both model-visible contributions.
*/
export function concatContext(ours: WorkspaceHookContext, theirs: HookContext | undefined): WorkspaceHookContext {
if (theirs === undefined) return ours
return {
...ours,
content: [
...ours.content,
...renderContextContent(theirs.content, theirs.source, theirs.envelope),
],
}
}
function filePathFromExecution(exec: ToolExecution): string | undefined {
if (!FILE_TOUCH_TOOL_NAMES.has(exec.name)) return undefined
if (typeof exec.arguments !== 'object' || exec.arguments === null) return undefined

View File

@@ -139,15 +139,22 @@ function blocksText(blocks: { type: string; text?: string }[] | undefined): stri
return blocks?.map(block => block.type === 'text' ? block.text ?? '' : '').join('\n') ?? ''
}
function appendAdditionalContext(agent: Agent, result: { additionalContext?: HookContext }): number | undefined {
const context = result.additionalContext
if (context === undefined) return undefined
return agent.session.append('context/message', {
content: context.content,
source: context.source,
...context.envelope !== undefined ? { envelope: context.envelope } : {},
...context.meta !== undefined ? { meta: context.meta } : {},
}, { surfaceOp: 'append' }).seq
function workspaceContextOf(result: { additionalContexts?: HookContext[] }): HookContext | undefined {
return result.additionalContexts?.find(context =>
context.source.kind === 'plugin' && context.source.plugin === 'workspace-context')
}
function appendAdditionalContexts(agent: Agent, result: { additionalContexts?: HookContext[] }): number | undefined {
let lastSeq: number | undefined
for (const context of result.additionalContexts ?? []) {
lastSeq = agent.session.append('context/message', {
content: context.content,
source: context.source,
...context.envelope !== undefined ? { envelope: context.envelope } : {},
...context.meta !== undefined ? { meta: context.meta } : {},
}, { surfaceOp: 'append' }).seq
}
return lastSeq
}
const composedPrefixes = new WeakMap<object, Message[]>()
@@ -763,7 +770,7 @@ describe('workspace context request injection', () => {
kind: 'block',
feedback: [{ type: 'text', text: 'blocked by policy' }],
})
expect(blocked.additionalContext).toBeUndefined()
expect(blocked.additionalContexts).toBeUndefined()
// The same read, when the downstream accepts, DOES surface the nested
// instructions — proving the block branch above is what suppressed them,
@@ -772,8 +779,8 @@ describe('workspace context request injection', () => {
kind: 'accept' as const,
}))
expect(accepted.kind).toBe('accept')
expect(accepted.additionalContext?.source).toEqual({ kind: 'plugin', plugin: 'workspace-context' })
expect(blocksText(accepted.additionalContext?.content)).toContain('nested package rule')
expect(workspaceContextOf(accepted)?.source).toEqual({ kind: 'plugin', plugin: 'workspace-context' })
expect(blocksText(workspaceContextOf(accepted)?.content)).toContain('nested package rule')
} finally {
await ctx.fiber.dispose()
await rm(root, { recursive: true, force: true })
@@ -891,11 +898,11 @@ describe('workspace context request injection', () => {
callId: CallId('read-after-baseline-change'), name: 'read', arguments: { file_path: 'file.txt' }, agent,
})
expect(result.additionalContext?.meta).toMatchObject({
expect(workspaceContextOf(result)?.meta).toMatchObject({
changes: [{ action: 'replace', scope: '.', path: 'AGENTS.md' }],
})
expect(blocksText(result.additionalContext?.content)).toContain('Updated instructions from: AGENTS.md')
expect(blocksText(result.additionalContext?.content)).toContain('new root rule with more detail')
expect(blocksText(workspaceContextOf(result)?.content)).toContain('Updated instructions from: AGENTS.md')
expect(blocksText(workspaceContextOf(result)?.content)).toContain('new root rule with more detail')
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -919,10 +926,10 @@ describe('workspace context request injection', () => {
callId: CallId('read-after-baseline-remove'), name: 'read', arguments: { file_path: 'file.txt' }, agent,
})
expect(result.additionalContext?.meta).toMatchObject({
expect(workspaceContextOf(result)?.meta).toMatchObject({
changes: [{ action: 'remove', scope: '.', path: 'AGENTS.md' }],
})
expect(blocksText(result.additionalContext?.content)).toContain('Instructions removed: AGENTS.md')
expect(blocksText(workspaceContextOf(result)?.content)).toContain('Instructions removed: AGENTS.md')
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -945,7 +952,7 @@ describe('workspace context request injection', () => {
})
expect(derivedText(agent).match(/shared root and global rule/g)).toHaveLength(1)
expect(result.additionalContext).toBeUndefined()
expect(result.additionalContexts).toBeUndefined()
} finally {
await rm(root, { recursive: true, force: true })
}
@@ -1361,9 +1368,9 @@ describe('dynamic nested workspace context injection', () => {
})
expect(result.isError).toBe(false)
expect(result.additionalContext?.source).toEqual({ kind: 'plugin', plugin: 'workspace-context' })
expect(result.additionalContext?.envelope).toBe('raw')
expect(result.additionalContext?.meta).toMatchObject({
expect(workspaceContextOf(result)?.source).toEqual({ kind: 'plugin', plugin: 'workspace-context' })
expect(workspaceContextOf(result)?.envelope).toBe('raw')
expect(workspaceContextOf(result)?.meta).toMatchObject({
kind: 'workspace-instructions',
version: 1,
changes: [{
@@ -1372,7 +1379,7 @@ describe('dynamic nested workspace context injection', () => {
path: 'pkg/AGENTS.md',
}],
})
const meta = result.additionalContext?.meta
const meta = workspaceContextOf(result)?.meta
const firstChange = typeof meta === 'object' && meta !== null && !Array.isArray(meta) && Array.isArray(meta.changes)
? meta.changes[0]
: undefined
@@ -1380,7 +1387,7 @@ describe('dynamic nested workspace context injection', () => {
? firstChange.digest
: undefined
expect(changeDigest).toMatch(/^[a-f0-9]{40}$/)
const text = blocksText(result.additionalContext?.content)
const text = blocksText(workspaceContextOf(result)?.content)
expect(text).toBe([
'<system-reminder>',
'Additional instructions from: pkg/AGENTS.md',
@@ -1420,7 +1427,7 @@ describe('dynamic nested workspace context injection', () => {
agent: stubAgent(root),
})
const text = blocksText(result.additionalContext?.content)
const text = blocksText(workspaceContextOf(result)?.content)
expect(text).toContain('Additional instructions from: pkg/CLAUDE.local.md')
expect(text).toContain('local package rule')
expect(text).not.toContain('native package rule')
@@ -1454,8 +1461,8 @@ describe('dynamic nested workspace context injection', () => {
agent,
})
expect(first.additionalContext).toBeDefined()
expect(second.additionalContext).toBeUndefined()
expect(first.additionalContexts).toBeDefined()
expect(second.additionalContexts).toBeUndefined()
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -1476,17 +1483,17 @@ describe('dynamic nested workspace context injection', () => {
const first = await ctx.tools.execute({
callId: CallId('read-before-change'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
appendAdditionalContext(agent, first)
appendAdditionalContexts(agent, first)
await write(join(root, 'pkg/AGENTS.md'), 'new package rule with more detail')
const changed = await ctx.tools.execute({
callId: CallId('read-after-change'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
expect(changed.additionalContext?.meta).toMatchObject({
expect(workspaceContextOf(changed)?.meta).toMatchObject({
kind: 'workspace-instructions',
changes: [{ action: 'replace', scope: 'pkg', path: 'pkg/AGENTS.md' }],
})
expect(blocksText(changed.additionalContext?.content)).toBe([
expect(blocksText(workspaceContextOf(changed)?.content)).toBe([
'<system-reminder>',
'Updated instructions from: pkg/AGENTS.md',
'',
@@ -1516,25 +1523,25 @@ describe('dynamic nested workspace context injection', () => {
const first = await ctx.tools.execute({
callId: CallId('read-before-fallback'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
appendAdditionalContext(agent, first)
appendAdditionalContexts(agent, first)
await rm(join(root, 'pkg/AGENTS.md'))
const changed = await ctx.tools.execute({
callId: CallId('read-after-fallback'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
appendAdditionalContext(agent, changed)
appendAdditionalContexts(agent, changed)
const unchanged = await ctx.tools.execute({
callId: CallId('read-after-logged-fallback'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
expect(changed.additionalContext?.meta).toMatchObject({
expect(workspaceContextOf(changed)?.meta).toMatchObject({
changes: [{
action: 'replace', scope: 'pkg', path: 'pkg/CLAUDE.md', previousPath: 'pkg/AGENTS.md',
}],
})
expect(blocksText(changed.additionalContext?.content)).toContain('Updated instructions from: pkg/CLAUDE.md')
expect(blocksText(changed.additionalContext?.content)).toContain('The instructions previously loaded from `pkg/AGENTS.md` no longer apply. Use the following content for `pkg` instead.')
expect(blocksText(changed.additionalContext?.content)).toContain('fallback package rule')
expect(unchanged.additionalContext).toBeUndefined()
expect(blocksText(workspaceContextOf(changed)?.content)).toContain('Updated instructions from: pkg/CLAUDE.md')
expect(blocksText(workspaceContextOf(changed)?.content)).toContain('The instructions previously loaded from `pkg/AGENTS.md` no longer apply. Use the following content for `pkg` instead.')
expect(blocksText(workspaceContextOf(changed)?.content)).toContain('fallback package rule')
expect(unchanged.additionalContexts).toBeUndefined()
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -1555,18 +1562,18 @@ describe('dynamic nested workspace context injection', () => {
const first = await ctx.tools.execute({
callId: CallId('read-before-remove'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
appendAdditionalContext(agent, first)
appendAdditionalContexts(agent, first)
await rm(join(root, 'pkg/AGENTS.md'))
const removed = await ctx.tools.execute({
callId: CallId('read-after-remove'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
expect(removed.additionalContext?.meta).toEqual({
expect(workspaceContextOf(removed)?.meta).toEqual({
kind: 'workspace-instructions',
version: 1,
changes: [{ action: 'remove', scope: 'pkg', path: 'pkg/AGENTS.md' }],
})
expect(blocksText(removed.additionalContext?.content)).toBe([
expect(blocksText(workspaceContextOf(removed)?.content)).toBe([
'<system-reminder>',
'Instructions removed: pkg/AGENTS.md',
'',
@@ -1593,23 +1600,23 @@ describe('dynamic nested workspace context injection', () => {
const first = await ctx.tools.execute({
callId: CallId('read-before-tombstone'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
appendAdditionalContext(agent, first)
appendAdditionalContexts(agent, first)
await rm(join(root, 'pkg/AGENTS.md'))
const removed = await ctx.tools.execute({
callId: CallId('read-to-create-tombstone'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
appendAdditionalContext(agent, removed)
appendAdditionalContexts(agent, removed)
await write(join(root, 'pkg/AGENTS.md'), 'restored package rule')
const restored = await ctx.tools.execute({
callId: CallId('read-after-tombstone'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
expect(restored.additionalContext?.meta).toMatchObject({
expect(workspaceContextOf(restored)?.meta).toMatchObject({
changes: [{ action: 'set', scope: 'pkg', path: 'pkg/AGENTS.md' }],
})
expect(blocksText(restored.additionalContext?.content)).toContain('Additional instructions from: pkg/AGENTS.md')
expect(blocksText(restored.additionalContext?.content)).toContain('restored package rule')
expect(blocksText(workspaceContextOf(restored)?.content)).toContain('Additional instructions from: pkg/AGENTS.md')
expect(blocksText(workspaceContextOf(restored)?.content)).toContain('restored package rule')
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -1635,14 +1642,14 @@ describe('dynamic nested workspace context injection', () => {
const first = await ctx.tools.execute({
callId: CallId('read-before-provider-failure'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
appendAdditionalContext(agent, first)
appendAdditionalContexts(agent, first)
fs.throwOnStat.add(join(root, 'pkg/AGENTS.md'))
const duringFailure = await ctx.tools.execute({
callId: CallId('read-during-provider-failure'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
expect(first.additionalContext).toBeDefined()
expect(duringFailure.additionalContext).toBeUndefined()
expect(first.additionalContexts).toBeDefined()
expect(duringFailure.additionalContexts).toBeUndefined()
} finally {
await ctx.fiber.dispose()
await rm(root, { recursive: true, force: true })
@@ -1666,7 +1673,7 @@ describe('dynamic nested workspace context injection', () => {
arguments: { file_path: 'pkg/deep/file.txt' },
agent,
})
appendAdditionalContext(agent, first)
appendAdditionalContexts(agent, first)
const resumed = {
...agent,
session: new Session(agent.session.id, [...agent.session.events], agent.session.header),
@@ -1679,8 +1686,8 @@ describe('dynamic nested workspace context injection', () => {
agent: resumed,
})
expect(first.additionalContext).toBeDefined()
expect(afterResume.additionalContext).toBeUndefined()
expect(first.additionalContexts).toBeDefined()
expect(afterResume.additionalContexts).toBeUndefined()
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -1700,7 +1707,7 @@ describe('dynamic nested workspace context injection', () => {
const first = await ctx.tools.execute({
callId: CallId('read-before-offline-change'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent: original,
})
appendAdditionalContext(original, first)
appendAdditionalContexts(original, first)
await write(join(root, 'pkg/AGENTS.md'), 'new nested rule after resume')
const resumed = stubAgent(root, [...original.session.events])
@@ -1733,7 +1740,7 @@ describe('dynamic nested workspace context injection', () => {
arguments: { file_path: 'pkg/deep/file.txt' },
agent,
})
const contextSeq = appendAdditionalContext(agent, first)!
const contextSeq = appendAdditionalContexts(agent, first)!
const visibleBeforeCompact = await ctx.tools.execute({
callId: CallId('read-while-visible'),
name: 'read',
@@ -1753,10 +1760,10 @@ describe('dynamic nested workspace context injection', () => {
agent,
})
expect(first.additionalContext).toBeDefined()
expect(visibleBeforeCompact.additionalContext).toBeUndefined()
expect(afterCompact.additionalContext).toBeDefined()
expect(blocksText(afterCompact.additionalContext?.content)).toContain('nested package rule')
expect(first.additionalContexts).toBeDefined()
expect(visibleBeforeCompact.additionalContexts).toBeUndefined()
expect(afterCompact.additionalContexts).toBeDefined()
expect(blocksText(workspaceContextOf(afterCompact)?.content)).toContain('nested package rule')
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -1781,7 +1788,7 @@ describe('dynamic nested workspace context injection', () => {
arguments: { file_path: 'pkg/file.txt' },
agent,
})
appendAdditionalContext(agent, first)
appendAdditionalContexts(agent, first)
const second = await ctx.tools.execute({
callId: CallId('read-subtree'),
@@ -1790,8 +1797,8 @@ describe('dynamic nested workspace context injection', () => {
agent,
})
expect(blocksText(first.additionalContext?.content)).toContain('package note')
expect(blocksText(second.additionalContext?.content)).toContain('subtree rule')
expect(blocksText(workspaceContextOf(first)?.content)).toContain('package note')
expect(blocksText(workspaceContextOf(second)?.content)).toContain('subtree rule')
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -1816,7 +1823,7 @@ describe('dynamic nested workspace context injection', () => {
arguments: { file_path: 'pkg/sub/file.txt' },
agent,
})
appendAdditionalContext(agent, first)
appendAdditionalContexts(agent, first)
const second = await ctx.tools.execute({
callId: CallId('read-parent-after-omit'),
@@ -1825,11 +1832,11 @@ describe('dynamic nested workspace context injection', () => {
agent,
})
const firstText = blocksText(first.additionalContext?.content)
const firstText = blocksText(workspaceContextOf(first)?.content)
expect(firstText).toContain('omitted pkg/AGENTS.md')
expect(firstText).not.toContain('## pkg/AGENTS.md')
expect(firstText).toContain('subtree rule')
expect(blocksText(second.additionalContext?.content)).toContain('parent rule')
expect(blocksText(workspaceContextOf(second)?.content)).toContain('parent rule')
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -1886,7 +1893,7 @@ describe('dynamic nested workspace context injection', () => {
agent,
})
expect(blocksText(result.additionalContext?.content)).toContain('nested package rule')
expect(blocksText(workspaceContextOf(result)?.content)).toContain('nested package rule')
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -1918,8 +1925,8 @@ describe('dynamic nested workspace context injection', () => {
agent,
})
expect(rootResult.additionalContext).toBeUndefined()
expect(blocksText(absoluteResult.additionalContext?.content)).toContain('nested package rule')
expect(rootResult.additionalContexts).toBeUndefined()
expect(blocksText(workspaceContextOf(absoluteResult)?.content)).toContain('nested package rule')
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -1982,7 +1989,7 @@ describe('dynamic nested workspace context injection', () => {
})
expect(result.isError).toBe(false)
expect(result.additionalContext).toBeUndefined()
expect(result.additionalContexts).toBeUndefined()
await chmod(nested, 0o600)
} finally {
await rm(root, { recursive: true, force: true })
@@ -1990,7 +1997,7 @@ describe('dynamic nested workspace context injection', () => {
}
})
it('folds nested instruction context with downstream post-execute content and context', async () => {
it('preserves nested and downstream post-execute contexts as separate entries', async () => {
const root = await tempRepo()
const home = await tempRepo()
try {
@@ -2002,10 +2009,10 @@ describe('dynamic nested workspace context injection', () => {
ctx.on('tools/post-execute', async () => ({
kind: 'accept' as const,
content: [{ type: 'text' as const, text: 'downstream replacement' }],
additionalContext: {
additionalContexts: [{
content: [{ type: 'text' as const, text: 'downstream context' }],
source: { kind: 'plugin' as const, plugin: 'downstream' },
},
}],
}))
const result = await ctx.tools.execute({
@@ -2016,17 +2023,22 @@ describe('dynamic nested workspace context injection', () => {
})
expect(blocksText(result.content)).toBe('downstream replacement')
expect(result.additionalContext?.source).toEqual({ kind: 'plugin', plugin: 'workspace-context' })
expect(result.additionalContext?.envelope).toBe('raw')
expect(result.additionalContext?.meta).toMatchObject({
expect(result.additionalContexts).toHaveLength(2)
expect(workspaceContextOf(result)?.source).toEqual({ kind: 'plugin', plugin: 'workspace-context' })
expect(workspaceContextOf(result)?.envelope).toBe('raw')
expect(workspaceContextOf(result)?.meta).toMatchObject({
kind: 'workspace-instructions',
changes: [{ action: 'set', scope: 'pkg', path: 'pkg/AGENTS.md' }],
})
expect(blocksText(result.additionalContext?.content)).toContain('nested package rule')
expect(blocksText(result.additionalContext?.content)).toContain('downstream context')
expect(blocksText(workspaceContextOf(result)?.content)).toContain('nested package rule')
expect(blocksText(workspaceContextOf(result)?.content)).not.toContain('downstream context')
expect(result.additionalContexts?.[1]).toEqual({
content: [{ type: 'text', text: 'downstream context' }],
source: { kind: 'plugin', plugin: 'downstream' },
})
const agent = stubAgent(root)
appendAdditionalContext(agent, result)
expect(blocksText(agent.session.deriveMessages()[0]?.content)).toContain('<context source="plugin">\ndownstream context\n</context>')
appendAdditionalContexts(agent, result)
expect(blocksText(agent.session.deriveMessages()[1]?.content)).toContain('<context source="plugin">\ndownstream context\n</context>')
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -2058,7 +2070,7 @@ describe('dynamic nested workspace context injection', () => {
// should reach the model, and the block feedback must survive unchanged.
expect(result.isError).toBe(true)
expect(blocksText(result.content)).toBe('blocked downstream')
expect(result.additionalContext).toBeUndefined()
expect(result.additionalContexts).toBeUndefined()
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -2122,7 +2134,7 @@ describe('dynamic nested workspace context injection', () => {
})
expect(result.isError).toBe(false)
expect(result.additionalContext).toBeUndefined()
expect(result.additionalContexts).toBeUndefined()
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -2146,7 +2158,7 @@ describe('dynamic nested workspace context injection', () => {
})
expect(result.isError).toBe(true)
expect(result.additionalContext).toBeUndefined()
expect(result.additionalContexts).toBeUndefined()
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
@@ -2172,7 +2184,7 @@ describe('dynamic nested workspace context injection', () => {
})
expect(result.isError).toBe(false)
expect(result.additionalContext).toBeUndefined()
expect(result.additionalContexts).toBeUndefined()
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })