fix(web): grant turn-tail IconActions only after the turn ends
`assistantActionsSeqs` picked the last content-text assistant of each turn from the finalized transcript alone. That quantity is stable only once the turn closes: while a turn is still producing steps, the narration written before a tool call is the last content assistant so far, so copy, branch, and the clock appeared under an intermediate sentence for as long as the tool ran and then moved down to the next step's text. Pass `ConversationSnapshot.turnEnds` into the derivation and grant the row only inside a turn that has a durable `turn/end`. This is the same completion fact the branch control and the `Ran for` label already read, so the three parts of one row now agree; mid-turn narration owns nothing, and the seat appears once under the settled answer. `hasContentText` moves to chat-flow.ts so the ownership gate and AssistantMarkdown's mount gate cannot drift apart. apps/web/tests/turn-tail-actions.e2e.ts pins both states through the assembled application: a hang sidecar on the second model call parks a turn whose first step narrated before calling bash, and the two goldens hold the parked flow and the flow after stopping.
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Begin your reply with the" [disabled]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- text: Begin your reply with the plain sentence "Reading the workspace now." as text, and in that same message call the bash tool with the command "echo alpha". After the tool result, reply with the single word DONE and stop. {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- tooltip "Copy"
|
||||
- button "Branch into a new conversation" [disabled]:
|
||||
- img
|
||||
- text: Available only on the last message of a completed turn
|
||||
- button "Context injection @deepseek-ai/dsh-system-prompt":
|
||||
- img
|
||||
- img
|
||||
- text: Context injection @deepseek-ai/dsh-system-prompt
|
||||
- button "Think The user wants me to begin with \"Reading the workspace now.\" and call bash with \"echo alpha\" in the same message. Then after the tool result, reply with the single word DONE and stop.":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user wants me to begin with "Reading the workspace now." and call bash with "echo alpha" in the same message. Then after the tool result, reply with the single word DONE and stop.
|
||||
- paragraph: Reading the workspace now.
|
||||
- button "Bash Print alpha to stdout":
|
||||
- img
|
||||
- img
|
||||
- text: Bash Print alpha to stdout
|
||||
- paragraph: partial
|
||||
- status: Deep diving...
|
||||
- textbox "Message the agent"
|
||||
- button "Commands":
|
||||
- img
|
||||
- 'button "Access mode, current: Workspace Write"': Workspace Write
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "6% of context used"
|
||||
- button "Stop generating"
|
||||
- text: 1 turns · 1 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 7.8K tok · Output 109 tok
|
||||
36
apps/web/tests/snapshots/turn-tail-actions/session.jsonl
Normal file
36
apps/web/tests/snapshots/turn-tail-actions/session.jsonl
Normal file
@@ -0,0 +1,36 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785918212891,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"permission/preset","seq":0,"time":1785918212892,"data":{"preset":"workspace-write"}}
|
||||
{"type":"sandbox/mode","seq":1,"time":1785918212893,"data":{"mode":"workspace-write"}}
|
||||
{"type":"approval/policy","seq":2,"time":1785918212893,"data":{"policy":"ask"}}
|
||||
{"type":"turn/start","seq":3,"time":1785918212945,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":4,"time":1785918212945,"data":{"content":[{"type":"text","text":"Begin your reply with the plain sentence \"Reading the workspace now.\" as text, and in that same message call the bash tool with the command \"echo alpha\". After the tool result, reply with the single word DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"},"role":"user","id":"4dcaa766-7ea2-4c6a-84cb-0d6ab53b5fb4"},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":5,"time":1785918212946,"data":{"title":"Begin your reply with the","messageSeqs":[4],"source":{"kind":"fallback"}}}
|
||||
{"type":"user/message","seq":6,"time":1785918212956,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}/workspace\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"plugin","plugin":"@deepseek-ai/dsh-system-prompt"},"role":"user","id":"60b8851a-888c-4d7e-9513-7d845f8d769b"},"surfaceOp":"append"}
|
||||
{"type":"step/start","seq":7,"time":1785918212956,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":8,"time":1785918212957,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","seq":9,"time":1785918212958,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}}
|
||||
{"type":"assistant/chunk","seq":10,"time":1785918214389,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":11,"time0":1785918214390,"data":{"turn":1,"step":1,"index":0,"dt":[101,1,0,0,0,56,1,0,0,0,0,0,0,72,1,0,0,0,0,29,0,0,0,0,35,1,0,17,39,0,0,0,0,0,31,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," begin"," with"," \"","Reading"," the"," workspace"," now",".\""," and"," call"," bash"," with"," \"","echo"," alpha","\""," in"," the"," same"," message","."," Then"," after"," the"," tool"," result",","," reply"," with"," the"," single"," word"," D","ONE"," and"," stop","."]}}
|
||||
{"type":"assistant/chunk","seq":53,"time":1785918214774,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
{"type":"text-chunks","seq0":54,"time0":1785918214774,"data":{"turn":1,"step":1,"index":1,"dt":[0,0,0,48],"texts":["Reading"," the"," workspace"," now","."]}}
|
||||
{"type":"assistant/chunk","seq":59,"time":1785918214841,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":2,"blockType":"tool-call"}}}
|
||||
{"type":"tool-call-chunks","seq0":60,"time0":1785918214842,"data":{"turn":1,"step":1,"index":2,"dt":[28,0,0,0,0,25,0,0,0,52,1,0,0,0,25,0,0,1,15,0,25],"id":"call_00_1yZGg4XTqe0N5r1rnDLx5082","name":"bash","args":["","{","\"","command","\"",": ","\"","echo"," alpha","\"",", ","\"","description","\"",": ","\"","Print"," alpha"," to"," stdout","\"","}"]}}
|
||||
{"type":"assistant/chunk","seq":82,"time":1785918215056,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to begin with \"Reading the workspace now.\" and call bash with \"echo alpha\" in the same message. Then after the tool result, reply with the single word DONE and stop."}}}}
|
||||
{"type":"assistant/chunk","seq":83,"time":1785918215057,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"Reading the workspace now."}}}}
|
||||
{"type":"assistant/chunk","seq":84,"time":1785918215057,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":2,"block":{"type":"tool-call","id":"call_00_1yZGg4XTqe0N5r1rnDLx5082","name":"bash","arguments":"{\"command\": \"echo alpha\", \"description\": \"Print alpha to stdout\"}"}}}}
|
||||
{"type":"assistant/chunk","seq":85,"time":1785918215057,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":7788,"outputTokens":109,"cacheReadTokens":0,"reasoningTokens":42}}}}
|
||||
{"type":"assistant/chunk","seq":86,"time":1785918215057,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":87,"time":1785918215061,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to begin with \"Reading the workspace now.\" and call bash with \"echo alpha\" in the same message. Then after the tool result, reply with the single word DONE and stop."},{"type":"text","text":"Reading the workspace now."},{"type":"tool-call","id":"call_00_1yZGg4XTqe0N5r1rnDLx5082","name":"bash","arguments":"{\"command\": \"echo alpha\", \"description\": \"Print alpha to stdout\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"73708391-4b5f-429e-b71c-ef2114244a95"},"usage":{"inputTokens":7788,"outputTokens":109,"cacheReadTokens":0,"reasoningTokens":42}},"sourceEventSeqs":[10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":88,"time":1785918215062,"data":{"turn":1,"step":1,"callId":"call_00_1yZGg4XTqe0N5r1rnDLx5082","name":"bash","arguments":"{\"command\": \"echo alpha\", \"description\": \"Print alpha to stdout\"}"}}
|
||||
{"type":"tool/result","seq":89,"time":1785918215096,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_1yZGg4XTqe0N5r1rnDLx5082"},"content":[{"type":"tool-result","toolCallId":"call_00_1yZGg4XTqe0N5r1rnDLx5082","content":[{"type":"text","text":"alpha\n"}],"isError":false}],"role":"user","id":"8b7ad694-b19e-4728-a804-eef9f53820b9"}},"sourceEventSeqs":[88],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":90,"time":1785918215097,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":91,"time":1785918215106,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":92,"time":1785918216259,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":93,"time":1785918216259,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"D"}}}
|
||||
{"type":"assistant/chunk","seq":94,"time":1785918216288,"data":{"turn":1,"step":2,"chunk":{"type":"text-delta","index":0,"text":"ONE"}}}
|
||||
{"type":"assistant/chunk","seq":95,"time":1785918216289,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}}}
|
||||
{"type":"assistant/chunk","seq":96,"time":1785918216289,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":103,"outputTokens":3,"cacheReadTokens":7808,"reasoningTokens":0}}}}
|
||||
{"type":"assistant/chunk","seq":97,"time":1785918216289,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":98,"time":1785918216289,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"fbb5a6d0-9945-4b52-ad15-978173d450a7"},"usage":{"inputTokens":103,"outputTokens":3,"cacheReadTokens":7808,"reasoningTokens":0}},"sourceEventSeqs":[92,93,94,95,96,97],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":99,"time":1785918216289,"data":{"turn":1,"step":2}}
|
||||
{"type":"turn/end","seq":100,"time":1785918216289,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -0,0 +1,43 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Begin your reply with the" [disabled]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- text: Begin your reply with the plain sentence "Reading the workspace now." as text, and in that same message call the bash tool with the command "echo alpha". After the tool result, reply with the single word DONE and stop. {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Branch into a new conversation" [disabled]:
|
||||
- img
|
||||
- text: Available only on the last message of a completed turn
|
||||
- button "Context injection @deepseek-ai/dsh-system-prompt":
|
||||
- img
|
||||
- img
|
||||
- text: Context injection @deepseek-ai/dsh-system-prompt
|
||||
- button "Think The user wants me to begin with \"Reading the workspace now.\" and call bash with \"echo alpha\" in the same message. Then after the tool result, reply with the single word DONE and stop.":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user wants me to begin with "Reading the workspace now." and call bash with "echo alpha" in the same message. Then after the tool result, reply with the single word DONE and stop.
|
||||
- paragraph: Reading the workspace now.
|
||||
- button "Bash Print alpha to stdout":
|
||||
- img
|
||||
- img
|
||||
- text: Bash Print alpha to stdout
|
||||
- paragraph: partial
|
||||
- text: Stopped
|
||||
- button "Copy":
|
||||
- img
|
||||
- tooltip "Copy"
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
|
||||
- textbox "Message the agent"
|
||||
- button "Commands":
|
||||
- img
|
||||
- 'button "Access mode, current: Workspace Write"': Workspace Write
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "6% of context used"
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 2 steps LLM {{duration}} · Tool call {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 0% Input 7.8K tok · Output 109 tok
|
||||
147
apps/web/tests/turn-tail-actions.e2e.ts
Normal file
147
apps/web/tests/turn-tail-actions.e2e.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
// Web e2e scenario: assistant IconActions belong to the settled answer, so
|
||||
// they arrive with `turn/end` and not before. The recorded turn narrates in
|
||||
// plain text before its tool call, which is the shape that used to hand the
|
||||
// footer to mid-turn narration for the seconds a tool runs and then move it
|
||||
// down. A `hang` sidecar on the SECOND model call parks the turn after the
|
||||
// narration and the tool result are durable, so the running state is stable by
|
||||
// construction rather than by timing; stopping from that park writes the
|
||||
// `turn/end` that hands the footer to the turn's transcript tail.
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterEach, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import type { ReplayOverrideDoc } from '@deepseek-ai/dsh-llm-replay'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/turn-tail-actions', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
// Two goldens for the same message: parked mid-turn, then settled.
|
||||
const RUNNING_EXPECTED = join(SNAPSHOT_DIR, 'running.expected.md')
|
||||
const SETTLED_EXPECTED = join(SNAPSHOT_DIR, 'settled.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
// The recording must carry text in the SAME assistant message as the tool
|
||||
// call; a Think-only step would leave nothing for the footer to attach to and
|
||||
// the scenario would pass against either implementation.
|
||||
const NARRATION = 'Reading the workspace now.'
|
||||
const PROMPT = `Begin your reply with the plain sentence "${NARRATION}" as text, and in that same message call the bash tool with the command "echo alpha". After the tool result, reply with the single word DONE and stop.`
|
||||
|
||||
describe('web e2e: assistant IconActions wait for the turn to end', () => {
|
||||
let scaffold: WebScaffold | undefined
|
||||
let browser: Browser | undefined
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let sessionEvents: SessionEvent[]
|
||||
let sidecarDir: string | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
// close() carries the fixture-consumption tripwire, so its failure is the
|
||||
// scenario's failure; run every teardown step, then rethrow what failed.
|
||||
const failures: unknown[] = []
|
||||
await browser?.close().catch((error: unknown) => failures.push(error))
|
||||
browser = undefined
|
||||
const closing = scaffold
|
||||
scaffold = undefined
|
||||
await closing?.close().catch((error: unknown) => failures.push(error))
|
||||
if (sidecarDir !== undefined) await rm(sidecarDir, { recursive: true, force: true }).catch((error: unknown) => failures.push(error))
|
||||
sidecarDir = undefined
|
||||
if (failures.length === 1) throw failures[0]
|
||||
if (failures.length > 1) throw new AggregateError(failures, 'turn-tail-actions teardown failed')
|
||||
})
|
||||
|
||||
/** Boot scaffold + page, materializing the sidecar before the replay row installs. */
|
||||
async function launch(buildOverride?: (sidecarHome: string) => ReplayOverrideDoc): Promise<void> {
|
||||
sessionEvents = []
|
||||
let overridePath: string | undefined
|
||||
if (buildOverride !== undefined) {
|
||||
sidecarDir = await mkdtemp(join(tmpdir(), 'dsh-web-e2e-sidecar-'))
|
||||
overridePath = join(sidecarDir, 'replay.override.json')
|
||||
await writeFile(overridePath, JSON.stringify(buildOverride(sidecarDir)))
|
||||
}
|
||||
scaffold = await launchWebScaffold(
|
||||
MODE === 'record'
|
||||
? {}
|
||||
: { replayFixture: FIXTURE, ...(overridePath === undefined ? {} : { replayOverride: overridePath }) },
|
||||
)
|
||||
scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { sessionEvents.push(event) })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await connectFreshWorkspace(page, scaffold.workspaceCwd)
|
||||
}
|
||||
|
||||
/** Send the recorded prompt with the settled barrier pre-armed (returned wrapped so the caller can act mid-turn). */
|
||||
async function sendPrompt(timeoutMs?: number): Promise<{ settled: ReturnType<WebScaffold['whenTurnSettled']> }> {
|
||||
const input = page.locator('textarea').first()
|
||||
await input.waitFor({ timeout: 10_000 })
|
||||
const settled = scaffold!.whenTurnSettled(timeoutMs)
|
||||
await input.fill(PROMPT)
|
||||
await input.press('Enter')
|
||||
return { settled }
|
||||
}
|
||||
|
||||
it.skipIf(MODE !== 'record')('records the narrate-then-call turn live through the composer', async () => {
|
||||
await launch()
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-turn-tail-actions-record'))
|
||||
const { settled } = await sendPrompt(180_000)
|
||||
const sessionId = await settled
|
||||
await recordFixture(scaffold!, sessionId, FIXTURE)
|
||||
}, 200_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('withholds the footer while the turn runs and grants it at turn/end', async () => {
|
||||
expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([PROMPT])
|
||||
let marker = ''
|
||||
// Patch the SECOND call: the first one delivers the narration and the tool
|
||||
// call as recorded, so the park happens with a durable mid-turn message.
|
||||
await launch((sidecarHome) => {
|
||||
marker = join(sidecarHome, '.hang-ready')
|
||||
return { patches: [{ at: 1, entry: { kind: 'hang', readyFile: marker } }] }
|
||||
})
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-turn-tail-actions'))
|
||||
const { settled } = await sendPrompt()
|
||||
// The marker IS the synchronization: the second call is provably parked,
|
||||
// so the first step's message and tool result are already durable.
|
||||
await expect.poll(() => existsSync(marker), { timeout: 20_000 }).toBe(true)
|
||||
await expect.poll(() => page.getByText(NARRATION, { exact: true }).count(), { timeout: 10_000 }).toBe(1)
|
||||
await expect.poll(
|
||||
() => page.getByRole('status').filter({ hasText: 'Deep diving...' }).isVisible(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(true)
|
||||
// Only the user bubble owns a footer: the narration is not the answer yet.
|
||||
const copyButtons = page.getByRole('button', { name: 'Copy' })
|
||||
await expect.poll(() => copyButtons.count(), { timeout: 10_000 }).toBe(1)
|
||||
expect(await page.getByRole('button', { name: 'Branch into a new conversation' }).count()).toBe(1)
|
||||
await copyButtons.first().focus()
|
||||
const running = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd)
|
||||
await compareOrRefreshGolden(RUNNING_EXPECTED, running, MODE)
|
||||
|
||||
// Closing the turn from the park is the state change under test: an
|
||||
// aborted turn is durably closed, so its transcript tail (the frozen
|
||||
// partial) takes the seat while the mid-turn narration keeps none.
|
||||
await page.getByRole('button', { name: 'Stop generating' }).click()
|
||||
await settled
|
||||
expect(sessionEvents.filter(e => e.type === 'turn/end').map(e => e.data.reason.kind)).toEqual(['aborted'])
|
||||
await expect.poll(() => copyButtons.count(), { timeout: 10_000 }).toBe(2)
|
||||
await expect.poll(() => page.locator('[data-streaming="true"]').count(), { timeout: 10_000 }).toBe(0)
|
||||
await copyButtons.last().focus()
|
||||
const settledAria = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd)
|
||||
await compareOrRefreshGolden(SETTLED_EXPECTED, settledAria, MODE)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 120_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps a closed fixture inventory', async () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['running.expected.md', 'session.jsonl', 'settled.expected.md'])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user