deepseek-harness/examples/acp-agent/tests/acp.e2e.ts

127 lines
5.8 KiB
TypeScript
Raw Normal View History

2026-07-14 14:11:33 +08:00
import { mkdtemp, readFile } from 'node:fs/promises'
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { afterEach, describe, expect, it } from 'vitest'
2026-07-14 00:18:00 +08:00
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
import {
2026-07-14 00:18:00 +08:00
launchAcpTestAgent,
type AgentUnderTest,
type LaunchedAcpTestAgent,
} from '@deepseek-ai/dsh-acp-snapshot'
2026-07-14 14:11:33 +08:00
import { cleanupAcpExampleTest } from './cleanup.ts'
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
/**
* End-to-end: boot examples/acp-agent as a real subprocess speaking ACP over
* its stdio, drive it with a real ClientSideConnection, send a real prompt, and
* verify the WORLD (a file the agent wrote), not the agent's self-report. Owns
* and disposes the subprocess in afterEach. Key-gated.
*
* Also asserts stdout purity (only framed JSON-RPC on stdout) — that one runs
* WITHOUT a key, since it only needs the server to boot and answer initialize.
*/
2026-07-14 00:18:00 +08:00
const AGENT: AgentUnderTest = {
binScript: fileURLToPath(new URL('../../../packages/examples/acp-demo/src/bin.ts', import.meta.url)),
2026-07-14 00:18:00 +08:00
configPath: fileURLToPath(new URL('../cordis.yml', import.meta.url)),
tsconfigPath: fileURLToPath(new URL('../../../tsconfig.json', import.meta.url)),
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
}
const DANGER_FULL_ACCESS_ENV = { DSH_PERMISSION_MODE: 'danger-full-access' }
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
2026-07-14 00:18:00 +08:00
let spawned: LaunchedAcpTestAgent | undefined
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
let workdir: string | undefined
afterEach(async () => {
2026-07-14 14:11:33 +08:00
const ownedSpawned = spawned
const ownedWorkdir = workdir
spawned = undefined
workdir = undefined
await cleanupAcpExampleTest(ownedSpawned, ownedWorkdir)
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
})
fix(acp): server crashed on connect — drop `export default`, read optional service cwd-independently Two independent bugs made the ACP server crash the moment an editor (Zed) connected, despite 178 green unit tests at 100% coverage: 1. `session/new` threw `cannot get property "agents" without inject`. Root cause: a stray `export default apply` made the cordis Loader's `unwrapExports` (`exports.default ?? exports`) collapse the module to the bare `apply` function, discarding the sibling `inject`/`name`/`Config` named exports. The plugin fiber was built with empty `inject`, so every `ctx.<service>` read in `apply` threw at load. Fix: remove the default export so the Loader uses the namespace. 2. `session/load` threw `cannot get property "sessionPersistence" without inject`. `AgentLoop.resume` read `this.ctx.sessionPersistence` (a service it deliberately does NOT inject); the property proxy's ancestor-only fiber walk fails through the bridge's traceable shadow. Fix: read it via `this.ctx.get('sessionPersistence', false)`, the topology-independent global-store lookup. Why the suite missed both: every test mounted the plugin by hand (`ctx.plugin({name,inject,apply})`), bypassing `unwrapExports` entirely, and the only test driving these RPCs was key-gated (skipped in CI). Added a no-key `session/new` e2e that boots the real example through the real Loader — it fails loudly on bug #1 without an API key. Set `TSX_TSCONFIG_PATH` in the e2e spawn so the subprocess resolves workspace `paths` from a temp cwd (it was silently falling back to a stale built `lib/`). Docs: post-mortem 0001; AGENTS.md "line coverage is not behavior coverage" + with-key/smoke-test philosophy; packages/AGENTS.md plugin-export-shape and ctx.get rules; dsh-code-review SKILL checks.
2026-06-18 03:12:37 +08:00
describe('acp-agent over real stdio (no key required)', () => {
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
it('emits only framed JSON-RPC on stdout', async () => {
workdir = await mkdtemp(join(tmpdir(), 'acp-e2e-'))
2026-07-14 00:18:00 +08:00
// Inspect the launcher's raw-byte tee in addition to driving its SDK client.
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
// A dummy key lets the deepseek adapter APPLY (it only checks the key is
// present at boot, not valid — the key is used only on a real model call,
// which this purity test never triggers). So this runs WITHOUT real creds.
2026-07-14 00:18:00 +08:00
spawned = launchAcpTestAgent({
agent: AGENT,
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
cwd: workdir,
2026-07-05 18:33:27 +08:00
env: {
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY ?? 'sk-dummy-for-boot',
...DANGER_FULL_ACCESS_ENV,
2026-07-05 18:33:27 +08:00
},
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
})
2026-07-14 00:18:00 +08:00
await spawned.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
2026-07-14 00:18:00 +08:00
const lines = spawned.rawStdout().split('\n').filter(line => line.trim().length > 0)
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
expect(lines.length).toBeGreaterThan(0)
for (const line of lines) {
// Every stdout line MUST parse as JSON (a JSON-RPC frame). A non-JSON
// line means a logger/print leaked onto the protocol channel.
expect(() => JSON.parse(line) as unknown).not.toThrow()
}
}, 30_000)
fix(acp): server crashed on connect — drop `export default`, read optional service cwd-independently Two independent bugs made the ACP server crash the moment an editor (Zed) connected, despite 178 green unit tests at 100% coverage: 1. `session/new` threw `cannot get property "agents" without inject`. Root cause: a stray `export default apply` made the cordis Loader's `unwrapExports` (`exports.default ?? exports`) collapse the module to the bare `apply` function, discarding the sibling `inject`/`name`/`Config` named exports. The plugin fiber was built with empty `inject`, so every `ctx.<service>` read in `apply` threw at load. Fix: remove the default export so the Loader uses the namespace. 2. `session/load` threw `cannot get property "sessionPersistence" without inject`. `AgentLoop.resume` read `this.ctx.sessionPersistence` (a service it deliberately does NOT inject); the property proxy's ancestor-only fiber walk fails through the bridge's traceable shadow. Fix: read it via `this.ctx.get('sessionPersistence', false)`, the topology-independent global-store lookup. Why the suite missed both: every test mounted the plugin by hand (`ctx.plugin({name,inject,apply})`), bypassing `unwrapExports` entirely, and the only test driving these RPCs was key-gated (skipped in CI). Added a no-key `session/new` e2e that boots the real example through the real Loader — it fails loudly on bug #1 without an API key. Set `TSX_TSCONFIG_PATH` in the e2e spawn so the subprocess resolves workspace `paths` from a temp cwd (it was silently falling back to a stale built `lib/`). Docs: post-mortem 0001; AGENTS.md "line coverage is not behavior coverage" + with-key/smoke-test philosophy; packages/AGENTS.md plugin-export-shape and ctx.get rules; dsh-code-review SKILL checks.
2026-06-18 03:12:37 +08:00
it('session/new succeeds over real stdio (no model call)', async () => {
// REGRESSION GUARD (this exact RPC exposed the missing-inject Loader bug):
// `session/new` drives the
fix(acp): server crashed on connect — drop `export default`, read optional service cwd-independently Two independent bugs made the ACP server crash the moment an editor (Zed) connected, despite 178 green unit tests at 100% coverage: 1. `session/new` threw `cannot get property "agents" without inject`. Root cause: a stray `export default apply` made the cordis Loader's `unwrapExports` (`exports.default ?? exports`) collapse the module to the bare `apply` function, discarding the sibling `inject`/`name`/`Config` named exports. The plugin fiber was built with empty `inject`, so every `ctx.<service>` read in `apply` threw at load. Fix: remove the default export so the Loader uses the namespace. 2. `session/load` threw `cannot get property "sessionPersistence" without inject`. `AgentLoop.resume` read `this.ctx.sessionPersistence` (a service it deliberately does NOT inject); the property proxy's ancestor-only fiber walk fails through the bridge's traceable shadow. Fix: read it via `this.ctx.get('sessionPersistence', false)`, the topology-independent global-store lookup. Why the suite missed both: every test mounted the plugin by hand (`ctx.plugin({name,inject,apply})`), bypassing `unwrapExports` entirely, and the only test driving these RPCs was key-gated (skipped in CI). Added a no-key `session/new` e2e that boots the real example through the real Loader — it fails loudly on bug #1 without an API key. Set `TSX_TSCONFIG_PATH` in the e2e spawn so the subprocess resolves workspace `paths` from a temp cwd (it was silently falling back to a stale built `lib/`). Docs: post-mortem 0001; AGENTS.md "line coverage is not behavior coverage" + with-key/smoke-test philosophy; packages/AGENTS.md plugin-export-shape and ctx.get rules; dsh-code-review SKILL checks.
2026-06-18 03:12:37 +08:00
// full bridge → `ctx.agents.create({sessionId, meta:{cwd}})` → AgentLoop →
// registry/persistence path, ALL of which run from the JSON-RPC read loop
// OUTSIDE the bridge plugin's injection scope. A lazy `ctx.<service>` read
// on that path throws and the RPC fails with an Internal error — yet the
// call never touches the model, so this reproduces WITHOUT a key. The
// key-gated prompt test below never caught it (it needs real creds); the
// initialize-only purity test never caught it (initialize does not reach
// the factory). This closes that gap: boot the real subprocess and create a
// session, asserting the RPC RESOLVES (not rejects with an inject error).
workdir = await mkdtemp(join(tmpdir(), 'acp-e2e-'))
// A dummy key lets the deepseek adapter boot (it only checks presence, not
// validity, at apply time); no model call is made, so the key is never used.
2026-07-14 00:18:00 +08:00
spawned = launchAcpTestAgent({
agent: AGENT,
cwd: workdir,
env: {
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY ?? 'sk-dummy-for-boot',
...DANGER_FULL_ACCESS_ENV,
},
2026-07-14 00:18:00 +08:00
})
fix(acp): server crashed on connect — drop `export default`, read optional service cwd-independently Two independent bugs made the ACP server crash the moment an editor (Zed) connected, despite 178 green unit tests at 100% coverage: 1. `session/new` threw `cannot get property "agents" without inject`. Root cause: a stray `export default apply` made the cordis Loader's `unwrapExports` (`exports.default ?? exports`) collapse the module to the bare `apply` function, discarding the sibling `inject`/`name`/`Config` named exports. The plugin fiber was built with empty `inject`, so every `ctx.<service>` read in `apply` threw at load. Fix: remove the default export so the Loader uses the namespace. 2. `session/load` threw `cannot get property "sessionPersistence" without inject`. `AgentLoop.resume` read `this.ctx.sessionPersistence` (a service it deliberately does NOT inject); the property proxy's ancestor-only fiber walk fails through the bridge's traceable shadow. Fix: read it via `this.ctx.get('sessionPersistence', false)`, the topology-independent global-store lookup. Why the suite missed both: every test mounted the plugin by hand (`ctx.plugin({name,inject,apply})`), bypassing `unwrapExports` entirely, and the only test driving these RPCs was key-gated (skipped in CI). Added a no-key `session/new` e2e that boots the real example through the real Loader — it fails loudly on bug #1 without an API key. Set `TSX_TSCONFIG_PATH` in the e2e spawn so the subprocess resolves workspace `paths` from a temp cwd (it was silently falling back to a stale built `lib/`). Docs: post-mortem 0001; AGENTS.md "line coverage is not behavior coverage" + with-key/smoke-test philosophy; packages/AGENTS.md plugin-export-shape and ctx.get rules; dsh-code-review SKILL checks.
2026-06-18 03:12:37 +08:00
const { client } = spawned
await client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await client.newSession({ cwd: workdir, mcpServers: [] })
expect(typeof sessionId).toBe('string')
expect(sessionId.length).toBeGreaterThan(0)
}, 60_000)
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
})
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('acp-agent e2e: real prompt over ACP', () => {
it('runs a real turn and the agent writes the requested file (verified on disk)', async () => {
workdir = await mkdtemp(join(tmpdir(), 'acp-e2e-'))
spawned = launchAcpTestAgent({ agent: AGENT, cwd: workdir, env: DANGER_FULL_ACCESS_ENV })
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
const { client, updates } = spawned
await client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
feat(acp): honor per-session cwd — run each ACP session in its own workspace Lifts the RFC 010 § Deferred restriction that the server had to launch in the workspace ("cwd must equal the launch directory"). An editor can now open any project folder, and N concurrent sessions over one connection can each target a different directory. - packages/acp: drop the `cwd === process.cwd()` guard in validateWorkspaceParams (keep "must be absolute" — the cwd becomes the session header / bash workdir), and drop the persisted-cwd-vs-launch-dir check in session/load (a resumed session keeps its original header.cwd, so its bash tools run in its workspace). - packages/tool-bash: the missing link — default the bash workdir to the calling agent's session cwd (`exec.agent.session.header.cwd`) via a new resolveWorkdir helper. An explicit model `workdir` still wins; a relative one resolves against the session cwd. This is the only correct spot for multi-session: N sessions share one ctx.bash executor, so the workdir must come per-call from exec.agent, not executor config. Falls back to the executor default when no session cwd is available (preserves non-ACP behavior). - Trust: the cwd originates from the ACP client (the user's editor) at session/new — same trust level as the old launch dir; no new untrusted-input path. `additionalDirectories` (scope widening / sandbox) stays rejected. - Tests: bridge accepts any absolute cwd + records it on the header; session/load honors the persisted cwd; bash defaults to / resolves relative against the session cwd; two sessions with different cwds each run bash in their own dir; non-absolute cwd still rejected. 100% per-file coverage maintained. - Docs: RFC 010 status + § Deferred cwd bullet marked RESOLVED; acp README adds a Per-session cwd section; tool-bash + example READMEs and e2e comments updated.
2026-06-17 10:01:18 +08:00
// Any absolute cwd is honored now; use the temp `workdir` as this session's
// workspace (the bash tool will run there) — it need not equal the launch dir.
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
const { sessionId } = await client.newSession({ cwd: workdir, mcpServers: [] })
const res = await client.prompt({
sessionId,
prompt: [{ type: 'text', text: 'Use the bash tool to write the exact text ACP_OK into a file named proof.txt in the current directory. Then stop.' }],
})
expect(['end_turn', 'max_tokens']).toContain(res.stopReason)
2026-07-25 13:02:37 +08:00
// Assert the filesystem effect independently of the model response.
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
const proof = await readFile(join(workdir, 'proof.txt'), 'utf8')
expect(proof).toContain('ACP_OK')
// The transport exposes only committed assistant text; tool execution is
// proved by the world effect above and remains session-log data.
expect(updates.length).toBeGreaterThan(0)
expect(updates.every(update => update.sessionUpdate === 'agent_message_chunk')).toBe(true)
feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited stdio), so Zed and other ACP editors can drive the coding agent — streaming render, tool-call display, and resumable sessions via `session/load`. - packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/ prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a fallback chain (agent/turn-end → logged turn/end → idle); single-session guard; cwd-must-equal-launch-dir validation; load replays from the persisted event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*). - agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent implements it (resolves on the first running→idle/disposed transition). The bridge awaits it on disposal so teardown reaches quiescence, not just abort. - examples: extract the shared provider/tool core into examples/base.yml; coding-agent nest-includes it; new examples/acp-agent serves the agent over ACP with JSONL persistence and no stdout logger (stdout is the protocol). - Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the executor's full authority; only the Agent→sessionId ownership seam is laid down. Cancel is best-effort for a not-yet-started queued turn (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`. - Docs: package README + Zed snippet; client-driver cookbook section; root and packages layout/commands; RFC 010 implementation-status note. 48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the example as a subprocess and verifies a written file on disk (key-gated, with a no-key stdout-purity check).
2026-06-16 11:10:25 +08:00
}, 180_000)
})