Accept native ACP path separators in tests

This commit is contained in:
Huanqi Cao
2026-07-18 12:40:12 +08:00
committed by imccyu
parent 5ed34b66ce
commit 1a4af034c0
2 files changed
+37 -20

No files matched your search

+2 -2
View File
@@ -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
+35 -18
View File
@@ -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<T extends SessionEvent['type']>(type: T, data: Extract<SessionEvent
return { type, seq: 0, time: 0, data } as SessionEvent
}
/** ACP path fields are filesystem paths; expectations use the host separator. */
function nativePath(...segments: string[]): string {
return pathJoin(...segments)
}
/** Resolve root-relative fixtures the same way the bridge does on this host. */
function nativeAbsolute(...segments: string[]): string {
return pathResolve(...segments)
}
describe('streamSessionEventUpdate', () => {
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()
})
})