Merge branch 'master' into feat/plan-mode

This commit is contained in:
Tianyi Cui
2026-07-22 11:29:58 +08:00
committed by GitHub
71 changed files with 2447 additions and 312 deletions

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'
@@ -50,6 +51,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 a title event to session_info_update with the event timestamp', () => {
expect(updatesFor({
@@ -576,10 +587,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')
@@ -792,10 +803,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 }),
@@ -804,8 +817,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()
})
@@ -856,21 +869,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()
})
@@ -887,8 +904,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()
})
@@ -901,8 +918,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()
})
})