소스 검색

test: cover project instruction configuration branches

Yichen Jiang 2 달 전
부모
커밋
2b99d8f5c2

+ 10 - 0
packages/core/agent-core/tests/agent-core.spec.ts

@@ -128,6 +128,16 @@ describe('dsh-agent-core bundle', () => {
     }
   })
 
+  it('supports direct apply with project instructions disabled and no forwarded agents', async () => {
+    const ctx = new Context()
+    agentCore.apply(ctx, { projectInstructions: false })
+    await new Promise(resolve => setTimeout(resolve, 50))
+
+    expect(ctx.get('agents')?.list()).toEqual([])
+    expect(ctx.get('systemPrompt')).toBeDefined()
+    await ctx.fiber.dispose()
+  })
+
   it('re-exports the loop config schema as its own', () => {
     expect(agentCore.Config).toBeDefined()
     expect(agentCore.name).toBe('agent-core')

+ 1 - 0
packages/core/project-instructions/src/index.ts

@@ -150,6 +150,7 @@ function ancestorChain(root: string, cwd: string): string[] {
   while (current !== resolvedRoot) {
     chain.push(current)
     const parent = dirname(current)
+    /* v8 ignore next -- defensive guard for direct helper misuse; discovery always passes cwd or an ancestor root. */
     if (parent === current) break
     current = parent
   }

+ 50 - 0
packages/core/project-instructions/tests/project-instructions.spec.ts

@@ -195,6 +195,24 @@ describe('project instruction discovery', () => {
     }
   })
 
+  it('labels the default DSH home as ~/.dsh when HOME points at the configured default', async () => {
+    const root = await tempRepo()
+    const home = await tempRepo()
+    const previousHome = process.env.HOME
+    try {
+      process.env.HOME = home
+      await write(join(home, '.dsh/AGENTS.md'), 'global default rule')
+
+      const files = await discoverBaselineInstructionFiles({ cwd: root })
+
+      expect(files.map(file => file.displayPath)).toEqual(['~/.dsh/AGENTS.md'])
+    } finally {
+      process.env.HOME = previousHome
+      await rm(root, { recursive: true, force: true })
+      await rm(home, { recursive: true, force: true })
+    }
+  })
+
   it('ignores instruction candidates that are directories', async () => {
     const root = await tempRepo()
     const home = await tempRepo()
@@ -280,6 +298,38 @@ describe('project instruction rendering', () => {
     expect(rendered.truncated[0]!.includedBytes).toBeGreaterThan(0)
     expect(Buffer.byteLength(rendered.text, 'utf8')).toBeLessThanOrEqual(700)
   })
+
+  it('omits all text when the render budget is disabled', () => {
+    const rendered = renderProjectInstructions([
+      { absolutePath: '/repo/AGENTS.md', displayPath: 'AGENTS.md', content: 'root rules' },
+    ], { maxBytes: 0 })
+
+    expect(rendered).toEqual({
+      text: '',
+      omitted: [{ absolutePath: '/repo/AGENTS.md', displayPath: 'AGENTS.md', content: 'root rules' }],
+      truncated: [],
+    })
+  })
+
+  it('falls back to a compact truncation notice when even the empty heading cannot fit', () => {
+    const rendered = renderProjectInstructions([
+      { absolutePath: '/repo/pkg/AGENTS.md', displayPath: 'pkg/AGENTS.md', content: 'x'.repeat(1000) },
+    ], { maxBytes: 100 })
+
+    expect(rendered.text).toBe('<!-- Project instruction budget 100 bytes: truncated pkg/AGENTS.md from 1000 to 0 bytes -->')
+    expect(rendered.truncated).toEqual([{ displayPath: 'pkg/AGENTS.md', originalBytes: 1000, includedBytes: 0 }])
+    expect(Buffer.byteLength(rendered.text, 'utf8')).toBeLessThanOrEqual(100)
+  })
+
+  it('truncates the compact notice itself when the render budget is smaller than the notice', () => {
+    const rendered = renderProjectInstructions([
+      { absolutePath: '/repo/pkg/AGENTS.md', displayPath: 'pkg/AGENTS.md', content: 'x'.repeat(1000) },
+    ], { maxBytes: 20 })
+
+    expect(rendered.text).toBe('<!-- Project instruc')
+    expect(rendered.truncated).toEqual([{ displayPath: 'pkg/AGENTS.md', originalBytes: 1000, includedBytes: 0 }])
+    expect(Buffer.byteLength(rendered.text, 'utf8')).toBe(20)
+  })
 })
 
 describe('project instruction request injection', () => {

+ 12 - 0
packages/ui/acp-agent/tests/acp-agent.spec.ts

@@ -46,6 +46,18 @@ describe('dsh-acp-agent composition', () => {
     await ctx.fiber.dispose()
   })
 
+  it('forwards explicit project-instruction controls to the bundled spine', async () => {
+    const ctx = await mount({
+      model: 'mock',
+      systemPrompt: 'hi',
+      persistenceRoot: '/tmp/dsh-acp-agent-project-instructions',
+      projectInstructions: false,
+    })
+    expect(ctx.get('agents')).toBeDefined()
+    expect(ctx.get('agentLoop')).toBeDefined()
+    await ctx.fiber.dispose()
+  })
+
   it('exposes its plugin shape', () => {
     expect(acpAgent.name).toBe('acp-agent')
     expect(acpAgent.Config).toBeDefined()

+ 11 - 0
packages/ui/stdio-agent/tests/stdio-agent.spec.ts

@@ -51,6 +51,17 @@ describe('dsh-stdio-agent app', () => {
     await ctx.fiber.dispose()
   })
 
+  it('forwards explicit project-instruction controls to the bundled spine', async () => {
+    const ctx = await mount({
+      model: 'mock',
+      systemPrompt: 'hi',
+      persistenceRoot: '/tmp/dsh-stdio-agent-spec-project-instructions',
+      projectInstructions: false,
+    })
+    expect(ctx.get('agents')?.get(AgentId('main'))).toBeDefined()
+    await ctx.fiber.dispose()
+  })
+
   it('forwards resumeSessionId onto the pre-created agent when set', async () => {
     // A resume id defers agent creation until persistence loads; with no backing
     // session the resume is contained + logged, so no `main` agent registers —