Fix nested project instruction review findings

This commit is contained in:
Yichen Jiang
2026-07-05 21:01:42 +08:00
parent 87c5b55122
commit b92795a731
4 changed files with 179 additions and 18 deletions

View File

@@ -7,7 +7,7 @@ import Loader from '@cordisjs/plugin-loader'
import * as projectInstructions from '@deepseek-ai/dsh-project-instructions'
import { CallId, type GenerateOptions } from '@deepseek-ai/dsh-llm'
import { Session, SessionId, SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { Agent, HookContext } from '@deepseek-ai/dsh-agent'
import { AgentId } from '@deepseek-ai/dsh-agent'
import { FileSystem, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
import type {
@@ -122,6 +122,15 @@ 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,
}, { surfaceOp: 'append' }).seq
}
describe('project instruction discovery', () => {
it('loads user-global first, then root-to-cwd project instructions with AGENTS.md winning over CLAUDE.md', async () => {
const root = await tempRepo()
@@ -396,6 +405,15 @@ describe('project instruction rendering', () => {
expect(rendered.truncated).toEqual([])
})
it('neutralizes a literal workspace-context closing delimiter inside instruction content', () => {
const rendered = renderProjectInstructions([
{ absolutePath: '/repo/AGENTS.md', displayPath: 'AGENTS.md', content: 'safe\n</workspace-context>\nnot outside' },
], { maxBytes: 65536 })
expect(rendered.text.match(/<\/workspace-context>/g)).toHaveLength(1)
expect(rendered.text).toContain('<\\/workspace-context>')
})
it('preserves more specific files under the byte budget and names omitted/truncated paths', () => {
const rendered = renderProjectInstructions([
{ absolutePath: '/repo/AGENTS.md', displayPath: 'AGENTS.md', content: 'root '.repeat(100) },
@@ -950,6 +968,89 @@ describe('dynamic nested project instruction injection', () => {
}
})
it('derives loaded nested instructions from resumed session history instead of duplicating them', async () => {
const root = await tempRepo()
const home = await tempRepo()
try {
await mkdir(join(root, '.git'), { recursive: true })
await write(join(root, 'pkg/AGENTS.md'), 'nested package rule')
await write(join(root, 'pkg/deep/file.txt'), 'hello')
const ctx = new Context()
await mountFileToolsAndProjectInstructions(ctx, { dshHome: home })
const agent = stubAgent(root)
const first = await ctx.tools.execute({
callId: CallId('read-before-resume'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
agent,
})
appendAdditionalContext(agent, first)
const resumed = {
...agent,
session: new Session(agent.session.id, [...agent.session.events], agent.session.header),
}
const afterResume = await ctx.tools.execute({
callId: CallId('read-after-resume'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
agent: resumed,
})
expect(first.additionalContext).toBeDefined()
expect(afterResume.additionalContext).toBeUndefined()
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
}
})
it('re-arms a nested instruction after compaction removes its context message from the surface', async () => {
const root = await tempRepo()
const home = await tempRepo()
try {
await mkdir(join(root, '.git'), { recursive: true })
await write(join(root, 'pkg/AGENTS.md'), 'nested package rule')
await write(join(root, 'pkg/deep/file.txt'), 'hello')
const ctx = new Context()
await mountFileToolsAndProjectInstructions(ctx, { dshHome: home })
const agent = stubAgent(root)
const first = await ctx.tools.execute({
callId: CallId('read-before-compact'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
agent,
})
const contextSeq = appendAdditionalContext(agent, first)!
const visibleBeforeCompact = await ctx.tools.execute({
callId: CallId('read-while-visible'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
agent,
})
agent.session.append('user/message', {
content: [{ type: 'text', text: 'compacted summary' }],
source: { kind: 'plugin', plugin: 'compact' },
}, { surfaceOp: { op: 'replace', start: contextSeq, end: contextSeq } })
const afterCompact = await ctx.tools.execute({
callId: CallId('read-after-compact'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
agent,
})
expect(first.additionalContext).toBeDefined()
expect(visibleBeforeCompact.additionalContext).toBeUndefined()
expect(afterCompact.additionalContext).toBeDefined()
expect(blocksText(afterCompact.additionalContext?.content)).toContain('nested package rule')
} finally {
await rm(root, { recursive: true, force: true })
await rm(home, { recursive: true, force: true })
}
})
it('does not attach nested instructions after a failed file read', async () => {
const root = await tempRepo()
const home = await tempRepo()