test(gui): cover edit tool row
This commit is contained in:
@@ -145,7 +145,7 @@ describe('web boot chain success pass (keyless, seven real bundles, ?fixture)',
|
||||
expect(owners).toContain('@deepseek-ai/dsh-client-ui-layout')
|
||||
})
|
||||
|
||||
it('expands fixture reasoning from either its summary or Think title', async () => {
|
||||
it('renders edit and expands fixture reasoning from either click target', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'smoke-think-disclosure'))
|
||||
await page.locator('[role="treeitem"]').first().click()
|
||||
await page.locator('[role="treeitem"][aria-selected]').first().click()
|
||||
@@ -161,6 +161,11 @@ describe('web boot chain success pass (keyless, seven real bundles, ?fixture)',
|
||||
|
||||
await think.getByText('Think', { exact: true }).click()
|
||||
expect(await think.getAttribute('aria-expanded')).toBe('false')
|
||||
|
||||
const editRoot = page.locator('[data-variant="edit"]').first()
|
||||
await editRoot.waitFor({ state: 'visible', timeout: 10_000 })
|
||||
expect(await editRoot.getByText('Edit', { exact: true }).count()).toBe(1)
|
||||
expect(await editRoot.getByText('notes/demo.txt', { exact: true }).count()).toBe(1)
|
||||
})
|
||||
|
||||
it('stayed clean: no page errors across the whole load chain', () => {
|
||||
|
||||
@@ -70,7 +70,8 @@ function buildAlphaLog(): SessionEvent[] {
|
||||
push({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } })
|
||||
}
|
||||
// Three view-sample turns (60-62) for the tool-card wire acceptance: one per built-in card
|
||||
// type. `echo` above stays presenter-less on purpose — it is the no-view fallback sample.
|
||||
// type. The generic sample uses the real `edit` name so the fallback row also exercises its
|
||||
// dedicated icon/title/path summary. `echo` above stays presenter-less as the unknown fallback.
|
||||
const toolTurn = (turn: number, name: string, args: string, resultText: string): void => {
|
||||
const callId = `fx-call-${turn}`
|
||||
push({ type: 'turn/start', data: { turn, trigger: { kind: 'message', source: { kind: 'user' } } } })
|
||||
@@ -87,7 +88,7 @@ function buildAlphaLog(): SessionEvent[] {
|
||||
}
|
||||
toolTurn(60, 'fx-bash', '{"command":"ls -la","cwd":"/tmp/fixture"}', 'total 2\ndrwxr-xr-x fixture\n-rw-r--r-- demo.txt')
|
||||
toolTurn(61, 'fx-write', '{"path":"notes/demo.txt","content":"hello fixture\\n"}', 'wrote notes/demo.txt')
|
||||
toolTurn(62, 'fx-note', '{"note":"三型卡验收样本"}', '已记录')
|
||||
toolTurn(62, 'edit', '{"file_path":"notes/demo.txt","old_string":"hello","new_string":"hello fixture"}', '已编辑')
|
||||
return events as unknown as SessionEvent[]
|
||||
}
|
||||
|
||||
@@ -112,8 +113,8 @@ function presentCall(name: string, argsRaw: string): ToolCallView | undefined {
|
||||
card: 'diff', title: `Write ${str(args.path)}`,
|
||||
diffs: [{ path: str(args.path), oldText: null, newText: str(args.content) }],
|
||||
}
|
||||
case 'fx-note':
|
||||
return { card: 'generic', title: '记录笔记', kind: 'edit', rawInput: args }
|
||||
case 'edit':
|
||||
return { card: 'generic', title: `Edit ${str(args.file_path)}`, kind: 'edit', rawInput: args }
|
||||
default:
|
||||
return undefined // echo et al: the documented no-view fallback path
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Conversation domain: skeleton (header/tabs/composer/empty state), chat view (grouped step-summary flow, streaming tail isolation), ctx.toolviews named registry with bash samples, minimal details panel, scope-addressed ConversationService. Contract: api-contracts v3 §7 plus the slot terminal design (store seat / props shares).
|
||||
|
||||
Generic tool rows classify the built-in bash, read, search, and edit names into dedicated visual variants. The edit variant renders the edit icon and `Edit · <path>` summary while retaining the shared row-to-details interaction.
|
||||
|
||||
Per-session UI state (selection, composer draft, active view) lives in the declared chat store (`stores.ts` `createChatStore`): apply constructs one handle and passes it to both the conversation and details registrations, so the two session slots share one instance per session (selection written by conversation, read by details) and the framework owns instance lifecycle and draft persistence. Components are pure — the framework standard kit (`useSession`/`sessionId`/`useSessions`) and the store faces (`useStore`/`actions`) arrive automatically from the registration declaration; the inject factories contribute plain data and callbacks only (send/stop choreography, view registry read face, startSession chain).
|
||||
|
||||
`src/client/` is organized for the future package split: `contract/` is the sole inter-domain shared face (`slots.ts` composed slot props, `views.ts` view ring, `toolview.ts` tool ring, `tool-call-model.ts`); the `skeleton/`, `chat/`, and `toolviews/` domain directories import contract files and never each other; `apply.ts` is the only assembly point allowed to import all three domains. The `/client` export surface is the contract only — `apply`/`inject`, the two service classes, and the `contract/` type families; implementation components (skeleton, chat rows) and the store factory stay internal and reach the page exclusively through apply's slot registrations (tests take them via the `./src/*` subpath).
|
||||
|
||||
@@ -29,6 +29,7 @@ describe('tool-call-model', () => {
|
||||
expect(classifyTool('web_fetch')).toBe('read')
|
||||
expect(classifyTool('web_search')).toBe('search')
|
||||
expect(classifyTool('grep')).toBe('search')
|
||||
expect(classifyTool('edit')).toBe('edit')
|
||||
expect(classifyTool('todo_write')).toBe('others')
|
||||
})
|
||||
|
||||
@@ -49,6 +50,7 @@ describe('tool-call-model', () => {
|
||||
it('keeps summaries single-line and falls back for opaque args', () => {
|
||||
expect(toolRowModel('bash', running({ argsRaw: '{"command":"a\\nb"}' })).summary).toBe('a')
|
||||
expect(toolRowModel('read', running({ name: 'read', argsRaw: '{"path":"/tmp/x.ts"}' })).summary).toBe('/tmp/x.ts')
|
||||
expect(toolRowModel('edit', running({ name: 'edit', argsRaw: '{"file_path":"src/x.ts"}' })).summary).toBe('src/x.ts')
|
||||
// Others rows prefix the real tool name into the summary slot (figma-flows
|
||||
// ruling: static "Tool call" title, name rides the mutable summary).
|
||||
expect(toolRowModel('x', running({ argsRaw: '{"n":1}' })).summary).toBe('x · {"n":1}')
|
||||
@@ -158,6 +160,19 @@ describe('GenericToolCard', () => {
|
||||
expect(view.container.querySelector('[data-state="running"]')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('renders edit with its dedicated title, icon variant, and path summary', () => {
|
||||
const view = render(
|
||||
<GenericToolCard {...props('edit', running({
|
||||
name: 'edit',
|
||||
argsRaw: '{"file_path":"src/x.ts","old_string":"before","new_string":"after"}',
|
||||
}))} />,
|
||||
)
|
||||
expect(view.getByText('Edit')).toBeTruthy()
|
||||
expect(view.getByText('src/x.ts')).toBeTruthy()
|
||||
expect(view.container.querySelector('[data-variant="edit"]')).not.toBeNull()
|
||||
expect(view.container.querySelector('svg')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('row click reaches actions.openDetails', () => {
|
||||
const p = props('bash', result())
|
||||
const view = render(<GenericToolCard {...p} />)
|
||||
|
||||
Reference in New Issue
Block a user