mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
# Conflicts: # .agents/notes/implemented/feature/2026-07-07-plan-mode.md # .agents/notes/implemented/simplification/2026-07-04-fold-stdio-ui-helper.md # docs/capability-seams.md # docs/config-catalog.md # docs/event-producer-consumer.md # examples/acp-agent/tests/plan-mode.e2e.ts # examples/acp-agent/tests/snapshots/plan-mode/tool-schemas.expected.json # examples/plan-acp-agent/README.md # examples/plan-acp-agent/cordis.yml # examples/plan-acp-agent/tests/acp.snapshot.ts # scripts/gen-doc-graphs.ts
85 lines
3.8 KiB
TypeScript
85 lines
3.8 KiB
TypeScript
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import {
|
|
PROTOCOL_VERSION,
|
|
type CreateElicitationRequest,
|
|
type CreateElicitationResponse,
|
|
} from '@agentclientprotocol/sdk'
|
|
import {
|
|
launchAcpTestAgent,
|
|
type AgentUnderTest,
|
|
type LaunchedAcpTestAgent,
|
|
} from '@deepseek-ai/dsh-acp-snapshot'
|
|
|
|
/** The shipped ACP leaf's plan mode exercised through its real subprocess entry. */
|
|
const AGENT: AgentUnderTest = {
|
|
binScript: fileURLToPath(new URL('../../../packages/examples/acp-demo/src/bin.ts', import.meta.url)),
|
|
configPath: fileURLToPath(new URL('../cordis.yml', import.meta.url)),
|
|
tsconfigPath: fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)),
|
|
}
|
|
|
|
let spawned: LaunchedAcpTestAgent | undefined
|
|
let workdir: string | undefined
|
|
|
|
afterEach(async () => {
|
|
const ownedSpawned = spawned
|
|
const ownedWorkdir = workdir
|
|
spawned = undefined
|
|
workdir = undefined
|
|
try {
|
|
if (ownedSpawned !== undefined) {
|
|
await ownedSpawned.close('SIGKILL').catch((error: unknown) => {
|
|
throw new Error(`plan ACP cleanup failed; child stderr:\n${ownedSpawned.stderr()}`, { cause: error })
|
|
})
|
|
}
|
|
} finally {
|
|
if (ownedWorkdir !== undefined) await rm(ownedWorkdir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('acp-agent plan mode e2e: approval gates implementation (real model)', () => {
|
|
it('keeps the file unchanged through review, then applies the approved plan', async () => {
|
|
workdir = await mkdtemp(join(tmpdir(), 'acp-plan-e2e-'))
|
|
const proofPath = join(workdir, 'proof.txt')
|
|
await writeFile(proofPath, 'BEFORE\n')
|
|
|
|
const reviews: CreateElicitationRequest[] = []
|
|
let contentAtReview: string | undefined
|
|
const createElicitation = async (request: CreateElicitationRequest): Promise<CreateElicitationResponse> => {
|
|
if (request.mode !== 'form' || request.requestedSchema.title !== 'Plan review') return { action: 'cancel' }
|
|
reviews.push(request)
|
|
contentAtReview = await readFile(proofPath, 'utf8')
|
|
return { action: 'accept', content: { choice: 'Approve' } }
|
|
}
|
|
|
|
spawned = launchAcpTestAgent({ agent: AGENT, cwd: workdir, createElicitation })
|
|
const { client, updates } = spawned
|
|
const rpc = async <T>(stage: string, operation: Promise<T>): Promise<T> => operation.catch((error: unknown) => {
|
|
throw new Error(`plan ACP ${stage} failed; child stderr:\n${spawned?.stderr() ?? '<unavailable>'}`, { cause: error })
|
|
})
|
|
await rpc('initialize', client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }))
|
|
const created = await rpc('session/new', client.newSession({ cwd: workdir, mcpServers: [] }))
|
|
expect(created.modes?.availableModes.map(mode => mode.id)).toEqual(['default', 'plan'])
|
|
await rpc('session/set_mode', client.setSessionMode({ sessionId: created.sessionId, modeId: 'plan' }))
|
|
|
|
const result = await rpc('prompt', client.prompt({
|
|
sessionId: created.sessionId,
|
|
prompt: [{
|
|
type: 'text',
|
|
text: 'Inspect proof.txt and plan the smallest change that replaces its contents with exactly AFTER followed by one newline. Present the complete plan through exit_plan_mode. After I approve it, implement the change with the filesystem tools, verify the exact file contents, and stop. Do not ask questions.',
|
|
}],
|
|
}))
|
|
|
|
expect(['end_turn', 'max_tokens']).toContain(result.stopReason)
|
|
expect(reviews).toHaveLength(1)
|
|
expect(contentAtReview).toBe('BEFORE\n')
|
|
expect(await readFile(proofPath, 'utf8')).toBe('AFTER\n')
|
|
expect(updates
|
|
.filter(update => update.sessionUpdate === 'current_mode_update')
|
|
.map(update => update.currentModeId)).toEqual(['plan', 'default'])
|
|
}, 240_000)
|
|
})
|