fix(ui-trajectory): timing provenance on sub-span lanes; assembled snapshot for both views

Responding to ds-review-bot on #664:

- SubSpanLane gains a 'timing' discriminant (measured | running | unknown).
  A settle-only replay entry (callTime null, start outside the window) was
  previously indistinguishable from a measured 0 ms span; it now renders
  hollow with a 'duration unknown' hover title, and durationMs stays null
  for anything unmeasured. Pairs with the client-runtime fix that stopped
  fabricating callTime = settle time (826c3696a on the live-parallel PR).

- The built-client Code Mode fixture snapshot now switches to the
  Trajectory and Waterfall tabs and pins the assembled rendering: three
  Sub cells with real +0.8s durations and three measured lanes with their
  hover titles — product-visible coverage through the real bundle graph,
  not just package-level jsdom fixtures.

Agent Note (both languages) updated for the timing contract; pairing
re-recorded.
This commit is contained in:
Tianyi Cui
2026-07-26 14:20:59 +08:00
parent c5e1f53d7e
commit 835156b038
8 changed files with 133 additions and 14 deletions

View File

@@ -5,7 +5,8 @@
// the code-variant parent row titled by the model-authored description, its
// three always-visible nested sub-rows (bash through the sample registration,
// read through GenericToolCard, the failing read wearing the error state),
// the expanded program body, and details-panel resolution of a sub-callId.
// the expanded program body, details-panel resolution of a sub-callId, and
// the trajectory/waterfall tabs' sub-call cells and timing lanes.
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
@@ -177,3 +178,64 @@ it('expands the code row into the program body and resolves a sub-row through th
}
`)
})
it('trajectory and waterfall surface the run_code sub-calls with real timing', async () => {
boot()
await openFixtureSession()
// Switch to the trajectory tab (same slot ring the chat view registers in).
fireEvent.click(await screen.findByRole('tab', { name: 'Trajectory' }))
await waitFor(() => {
expect(document.querySelector('[data-kind="subtool"]')).not.toBeNull()
}, { timeout: 10_000 })
const subCells = [...document.querySelectorAll('[data-kind="subtool"]')]
expect({
// Three Sub cells nested under the run_code Tool cell, in dispatch order,
// each with a real +N.Ns own-duration off the start/settle pair (the
// fixture spaces every event 800ms apart — never the em dash).
subCells: subCells.map(cell => visibleText(cell)),
}).toMatchInlineSnapshot(`
{
"subCells": [
"#53Subbash · {"command":"ls notes","description":"List notes"}+0.8s",
"#54Subread · {"path":"notes/demo.txt"}+0.8s",
"#55Subread · {"path":"notes/missing.txt"}+0.8s",
],
}
`)
// Waterfall: each sub-call draws a measured lane scaled into the parent
// turn's dispatch window.
fireEvent.click(screen.getByRole('tab', { name: 'Waterfall' }))
await waitFor(() => {
expect(document.querySelector('[data-subspan]')).not.toBeNull()
}, { timeout: 10_000 })
const lanes = [...document.querySelectorAll('[data-subspan]')]
expect({
lanes: lanes.map(lane => ({
label: visibleText(lane.querySelector('[class*="subTag"]') ?? lane),
title: lane.querySelector('[data-timing]')?.getAttribute('title'),
timing: lane.querySelector('[data-timing]')?.getAttribute('data-timing'),
})),
}).toMatchInlineSnapshot(`
{
"lanes": [
{
"label": "bash",
"timing": "measured",
"title": "bash · 0.80s",
},
{
"label": "read",
"timing": "measured",
"title": "read · 0.80s",
},
{
"label": "read",
"timing": "measured",
"title": "read · 0.80s",
},
],
}
`)
})