From 1a4af034c0edd60e4bd54de1f2a1371afbdef056 Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Sun, 5 Jul 2026 04:45:41 +0800 Subject: [PATCH] Accept native ACP path separators in tests --- packages/ui/acp/README.md | 4 +- packages/ui/acp/tests/stream-update.spec.ts | 53 ++++++++++++++------- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/packages/ui/acp/README.md b/packages/ui/acp/README.md index 300cd69023..4f810ba0b4 100644 --- a/packages/ui/acp/README.md +++ b/packages/ui/acp/README.md @@ -53,11 +53,11 @@ The shared [`ctx.tasks` runtime](../../tasks/tasks/) fences access to predictabl ## Tool-call presentation -Tools return provider-neutral `generic`, `terminal`, or `diff` render intents from `presentCall()` and `presentResult()`. The bridge maps the discriminator to ACP without special-casing tool names and falls back to a generic card. Per-session call-id state supplies result events with their omitted name and arguments during live streaming and replay. See [`dsh-tools`](../../core/tools/README.md#tool-owned-ui-presentation). +Tools return provider-neutral `generic`, `terminal`, or `diff` render intents from `presentCall()` and `presentResult()`. The bridge maps the discriminator to ACP without special-casing tool names and falls back to a generic card. Per-session call-id state supplies result events with their omitted name and arguments during live streaming and replay. File-card titles are relative to the session cwd and use the host separator, while location and diff paths remain raw so the editor opens the real file. See [`dsh-tools`](../../core/tools/README.md#tool-owned-ui-presentation). ## Terminal card (capability-gated) -When the client advertises `_meta.terminal_output`, terminal intents map to Zed's terminal info, output, and exit metadata. The bridge resolves relative cwd against the session, places the description before the terminal block, and omits result content because ACP updates replace call content. Other clients receive a generic card and bridge-derived fenced console fallback. Session creation snapshots the capability so call and result agree. The command still executes through the harness, not ACP terminal creation. See the [terminal-rendering RFC](../../../docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) and [render-intent RFC](../../../docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md). +When the client advertises `_meta.terminal_output`, terminal intents map to Zed's terminal info, output, and exit metadata. The bridge resolves relative cwd against the session and preserves the host filesystem separator, places the description before the terminal block, and omits result content because ACP updates replace call content. Other clients receive a generic card and bridge-derived fenced console fallback. Session creation snapshots the capability so call and result agree. The command still executes through the harness, not ACP terminal creation. See the [terminal-rendering RFC](../../../docs/rfc/implemented/feature/2026-06-18-acp-terminal-and-tool-rendering.md) and [render-intent RFC](../../../docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md). ## Settle-exactly-once diff --git a/packages/ui/acp/tests/stream-update.spec.ts b/packages/ui/acp/tests/stream-update.spec.ts index 415e9afb33..15a6d3c3fc 100644 --- a/packages/ui/acp/tests/stream-update.spec.ts +++ b/packages/ui/acp/tests/stream-update.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from 'vitest' +import { join as pathJoin, resolve as pathResolve } from 'node:path' import { Context } from 'cordis' import { CallId } from '@deepseek-ai/dsh-llm' import { SessionId, type SessionEvent } from '@deepseek-ai/dsh-session' @@ -49,6 +50,16 @@ function evt(type: T, data: Extract { it('maps assistant/chunk text-delta to agent_message_chunk', () => { expect(updatesFor(evt('assistant/chunk', { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'hi' } }))) @@ -480,10 +491,10 @@ describe('terminal-card mapping (capability-gated)', () => { it('capability ON: an ABSOLUTE tool cwd wins; a RELATIVE one resolves against the session cwd', () => { const [absCall] = termUpdates(termTool({ card: 'terminal', cwd: '/explicit/abs' }, { output: 'x' }), true, '/work/proj', callEvent) expect((absCall as unknown as { _meta: { terminal_info: { cwd: string } } })._meta.terminal_info.cwd).toBe('/explicit/abs') - const [relCall] = termUpdates(termTool({ card: 'terminal', cwd: 'sub/dir' }, { output: 'x' }), true, '/work/proj', callEvent) + const [relCall] = termUpdates(termTool({ card: 'terminal', cwd: nativePath('sub', 'dir') }, { output: 'x' }), true, nativeAbsolute('/work/proj'), callEvent) // Relative workdir resolved against the session cwd — the card header matches // where execution actually ran (tool-bash resolves the same way). - expect((relCall as unknown as { _meta: { terminal_info: { cwd: string } } })._meta.terminal_info.cwd).toBe('/work/proj/sub/dir') + expect((relCall as unknown as { _meta: { terminal_info: { cwd: string } } })._meta.terminal_info.cwd).toBe(nativeAbsolute('/work/proj', 'sub', 'dir')) // No session cwd to resolve against → the relative tool cwd is passed through as-is. const [noSessionCwd] = termUpdates(termTool({ card: 'terminal', cwd: 'rel/only' }, { output: 'x' }), true, undefined, callEvent) expect((noSessionCwd as unknown as { _meta: { terminal_info: { cwd: string } } })._meta.terminal_info.cwd).toBe('rel/only') @@ -675,10 +686,12 @@ describe('result-time diff card (REAL fs edit tool → tool_call_update diff blo // paths remain absolute so the editor can open the real file. const ctx = await fsCtx() const presenter = new ToolPresenter(ctx.tools) - const args = JSON.stringify({ file_path: '/work/proj/src/b.ts', old_string: 'OLD', new_string: 'NEW' }) - const meta = { diffs: [{ path: '/work/proj/src/b.ts', oldText: 'a\nOLD\nb', newText: 'a\nNEW\nb' }] } + const workspace = nativeAbsolute('/work/proj') + const file = nativeAbsolute('/work/proj', 'src', 'b.ts') + const args = JSON.stringify({ file_path: file, old_string: 'OLD', new_string: 'NEW' }) + const meta = { diffs: [{ path: file, oldText: 'a\nOLD\nb', newText: 'a\nNEW\nb' }] } const out: SessionNotification['update'][] = [] - const rendering = { enabled: false, cwd: '/work/proj' } + const rendering = { enabled: false, cwd: workspace } for (const event of [ evt('tool/call', { turn: 1, step: 1, callId: CallId('e1'), name: 'edit', arguments: args }), evt('tool/result', { turn: 1, step: 1, callId: CallId('e1'), content: [{ type: 'text', text: 'ok' }], isError: false, meta }), @@ -687,8 +700,8 @@ describe('result-time diff card (REAL fs edit tool → tool_call_update diff blo sessionUpdate: 'tool_call_update', toolCallId: 'e1', status: 'completed', - title: 'Edit src/b.ts', - content: [{ type: 'diff', path: '/work/proj/src/b.ts', oldText: 'a\nOLD\nb', newText: 'a\nNEW\nb' }], + title: `Edit ${nativePath('src', 'b.ts')}`, + content: [{ type: 'diff', path: file, oldText: 'a\nOLD\nb', newText: 'a\nNEW\nb' }], }) await ctx.fiber.dispose() }) @@ -739,21 +752,25 @@ describe('relative-path display titles (bridge relativizes the title against the it('read: an absolute path inside the workspace relativizes the TITLE; the location path stays absolute', async () => { const ctx = await fsCtx() - const update = callUpdate(ctx, '/work/proj', 'read', { file_path: '/work/proj/src/a.ts', offset: 5 }) + const workspace = nativeAbsolute('/work/proj') + const file = nativeAbsolute('/work/proj', 'src', 'a.ts') + const update = callUpdate(ctx, workspace, 'read', { file_path: file, offset: 5 }) expect(update).toMatchObject({ - title: 'Read src/a.ts (from line 5)', - locations: [{ path: '/work/proj/src/a.ts', line: 5 }], + title: `Read ${nativePath('src', 'a.ts')} (from line 5)`, + locations: [{ path: file, line: 5 }], }) await ctx.fiber.dispose() }) it('edit: the diff TITLE relativizes; the diff/location paths stay absolute (the editor opens the real path)', async () => { const ctx = await fsCtx() - const update = callUpdate(ctx, '/work/proj', 'edit', { file_path: '/work/proj/src/b.ts', old_string: 'x', new_string: 'y' }) + const workspace = nativeAbsolute('/work/proj') + const file = nativeAbsolute('/work/proj', 'src', 'b.ts') + const update = callUpdate(ctx, workspace, 'edit', { file_path: file, old_string: 'x', new_string: 'y' }) expect(update).toMatchObject({ - title: 'Edit src/b.ts', - locations: [{ path: '/work/proj/src/b.ts' }], - content: [{ type: 'diff', path: '/work/proj/src/b.ts', oldText: 'x', newText: 'y' }], + title: `Edit ${nativePath('src', 'b.ts')}`, + locations: [{ path: file }], + content: [{ type: 'diff', path: file, oldText: 'x', newText: 'y' }], }) await ctx.fiber.dispose() }) @@ -770,8 +787,8 @@ describe('relative-path display titles (bridge relativizes the title against the // with the chars `..` but is not a parent segment. Segment-aware guarding must relativize it, // matching targets under `cwd + sep` in the reference adapter. const ctx = await fsCtx() - const update = callUpdate(ctx, '/work/proj', 'read', { file_path: '/work/proj/..cache/x.ts' }) - expect((update as { title: string }).title).toBe('Read ..cache/x.ts') + const update = callUpdate(ctx, nativeAbsolute('/work/proj'), 'read', { file_path: nativeAbsolute('/work/proj', '..cache', 'x.ts') }) + expect((update as { title: string }).title).toBe(`Read ${nativePath('..cache', 'x.ts')}`) await ctx.fiber.dispose() }) @@ -784,8 +801,8 @@ describe('relative-path display titles (bridge relativizes the title against the it('a relative path is passed through unchanged (already display-friendly)', async () => { const ctx = await fsCtx() - const update = callUpdate(ctx, '/work/proj', 'read', { file_path: 'src/a.ts' }) - expect((update as { title: string }).title).toBe('Read src/a.ts') + const update = callUpdate(ctx, nativeAbsolute('/work/proj'), 'read', { file_path: nativePath('src', 'a.ts') }) + expect((update as { title: string }).title).toBe(`Read ${nativePath('src', 'a.ts')}`) await ctx.fiber.dispose() }) })