Merge remote-tracking branch 'origin/master' into worktree/default-workspace-write-ui-20260731
# Conflicts: # apps/cli/README.i18n.yaml
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
// Keyless browser coverage for pending queue actions through the shipped Web
|
||||
// composition and real HTTP/SSE wire. A replay override parks the active turn
|
||||
// so two ordinary follow-ups remain addressable while the page edits one and
|
||||
// removes one. The queue uses an existing recorded model
|
||||
// call; this scenario owns only the user-visible mid-turn golden.
|
||||
// composition and real HTTP/SSE wire. Replay overrides park consecutive turns
|
||||
// so the page can edit and remove exact occurrences, then stop the active turn
|
||||
// while proving the preserved Queue advances in FIFO order.
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
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 { deriveReplayScript, parseSessionLog, type ReplayEntry } from '@deepseek-ai/dsh-llm-replay'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
@@ -22,6 +22,7 @@ const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/queue-actions', import.m
|
||||
const FIXTURE = fileURLToPath(new URL('./snapshots/live-interactions/session.jsonl', import.meta.url))
|
||||
const COLLAPSED_EXPECTED = join(SNAPSHOT_DIR, 'collapsed.expected.md')
|
||||
const EDITING_EXPECTED = join(SNAPSHOT_DIR, 'editing.expected.md')
|
||||
const PRESERVED_EXPECTED = join(SNAPSHOT_DIR, 'preserved.expected.md')
|
||||
const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
@@ -29,6 +30,12 @@ const ACTIVE_PROMPT = 'Reply with a one-sentence description of event sourcing,
|
||||
const REMOVE = 'Queue item to remove'
|
||||
const EDIT = 'Queue item to edit'
|
||||
const EDITED = 'Edited queue item'
|
||||
const TAIL = 'Queue item preserved after stop'
|
||||
|
||||
/** Durable turn-end classifications observed by the scenario. */
|
||||
function turnEndReasons(events: readonly SessionEvent[]): string[] {
|
||||
return events.flatMap(event => event.type === 'turn/end' ? [event.data.reason.kind] : [])
|
||||
}
|
||||
|
||||
describe('web e2e: queue row actions', () => {
|
||||
let scaffold: WebScaffold | undefined
|
||||
@@ -52,13 +59,19 @@ describe('web e2e: queue row actions', () => {
|
||||
if (failures.length > 1) throw new AggregateError(failures, 'queue-actions teardown failed')
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('edits and removes exact pending occurrences', async () => {
|
||||
it.skipIf(MODE === 'record')('edits and removes exact occurrences and preserves Queue across stop', async () => {
|
||||
overrideDir = await mkdtemp(join(tmpdir(), 'dsh-web-queue-actions-'))
|
||||
const readyFile = join(overrideDir, '.hang-ready')
|
||||
const nextReadyFile = join(overrideDir, '.next-hang-ready')
|
||||
const overridePath = join(overrideDir, 'replay.override.json')
|
||||
await writeFile(overridePath, JSON.stringify({
|
||||
patches: [{ at: 0, entry: { kind: 'hang', readyFile } }],
|
||||
}))
|
||||
const recorded = deriveReplayScript(parseSessionLog(await readFile(FIXTURE, 'utf8')))
|
||||
expect(recorded).toHaveLength(1)
|
||||
const replay: ReplayEntry[] = [
|
||||
{ kind: 'hang', readyFile },
|
||||
{ kind: 'hang', readyFile: nextReadyFile },
|
||||
recorded[0]!,
|
||||
]
|
||||
await writeFile(overridePath, JSON.stringify(replay))
|
||||
|
||||
const sessionEvents: SessionEvent[] = []
|
||||
scaffold = await launchWebScaffold({ replayFixture: FIXTURE, replayOverride: overridePath })
|
||||
@@ -135,17 +148,34 @@ describe('web e2e: queue row actions', () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
|
||||
const editedRow = page.getByText(EDITED, { exact: true }).locator('..')
|
||||
await editedRow.getByRole('button', { name: 'Remove queued message' }).click()
|
||||
await expect.poll(() => page.getByText(EDITED, { exact: true }).count()).toBe(0)
|
||||
await input.fill(TAIL)
|
||||
await input.press('Enter')
|
||||
await expect.poll(
|
||||
() => page.getByRole('button', { name: 'Remove queued message' }).count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(2)
|
||||
|
||||
await page.getByRole('button', { name: 'Stop generating' }).click()
|
||||
await expect.poll(() => existsSync(nextReadyFile), { timeout: 15_000 }).toBe(true)
|
||||
await page.getByText(TAIL, { exact: true }).waitFor()
|
||||
await expect.poll(() => page.getByRole('button', { name: 'Remove queued message' }).count())
|
||||
.toBe(1)
|
||||
|
||||
const preservedSnapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(PRESERVED_EXPECTED, preservedSnapshot, MODE)
|
||||
|
||||
await page.getByRole('button', { name: 'Stop generating' }).click()
|
||||
await settled
|
||||
expect(turnEndReasons(sessionEvents)).toEqual(['aborted', 'aborted', 'completed'])
|
||||
expect(sessionEvents.filter(event => event.type === 'user/message' && event.data.source.kind === 'user'))
|
||||
.toHaveLength(3)
|
||||
await expect.poll(() => page.locator('[data-queue-dock]').count()).toBe(0)
|
||||
}, 120_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps its snapshot inventory closed', async () => {
|
||||
await assertFixtureInventory(
|
||||
SNAPSHOT_DIR,
|
||||
['collapsed.expected.md', 'editing.expected.md', 'ui.expected.md'],
|
||||
['collapsed.expected.md', 'editing.expected.md', 'preserved.expected.md', 'ui.expected.md'],
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -496,7 +496,7 @@ export async function seedSession(scaffold: WebScaffold, fixtureText: string, id
|
||||
* volatility collapse to stable tokens.
|
||||
*/
|
||||
function normalizeAria(snapshot: string, workspaceCwd: string): string {
|
||||
// The header breadcrumb renders the workspace's basename, not the full
|
||||
// The session heading renders the workspace's basename, not the full
|
||||
// path, so both spellings must collapse to the token.
|
||||
const base = workspaceCwd.split('/').pop()!
|
||||
return snapshot
|
||||
|
||||
@@ -357,6 +357,27 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
await compareOrRefreshGolden(COMMAND_ROW_EXPECTED, snapshot, MODE)
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('fits short injected context without a scrollport', async () => {
|
||||
const agent = scaffold.ctx.agents.get(SessionId(SEED_ID))
|
||||
if (agent === undefined) throw new Error('seeded session did not attach an agent')
|
||||
agent.inject(createUserMessage({
|
||||
content: [{ type: 'text', text: 'Short injected context.' }],
|
||||
source: { kind: 'plugin', plugin: 'fixture' },
|
||||
}))
|
||||
|
||||
const disclosures = page.getByRole('button', { name: 'Context injection' })
|
||||
await expect.poll(() => disclosures.count(), { timeout: 10_000 }).toBe(2)
|
||||
const disclosure = disclosures.nth(1)
|
||||
await disclosure.click()
|
||||
await expect.poll(() => disclosure.getAttribute('aria-expanded')).toBe('true')
|
||||
|
||||
const body = page.locator('[data-context-injection-body]')
|
||||
const bodyBox = await body.boundingBox()
|
||||
if (bodyBox === null) throw new Error('short context disclosure geometry is not measurable')
|
||||
expect(bodyBox.height).toBeLessThan(141)
|
||||
expect(await body.evaluate(element => element.scrollHeight > element.clientHeight)).toBe(false)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('issued zero model calls and stayed clean', async () => {
|
||||
// No replay fixture was installed and the llm seam is open — any stray
|
||||
// stream would have failed the turn loudly. Cleanliness pins the wire.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- 'button "Using ONE run_code program: run" [disabled]'
|
||||
- 'heading "Using ONE run_code program: run" [level=1]'
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use only Cordis tools. First" [disabled]
|
||||
- heading "Use only Cordis tools. First" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the bash tool to" [disabled]
|
||||
- heading "Use the bash tool to" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with the single word" [disabled]
|
||||
- heading "Reply with the single word" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- heading "Reply with a one-sentence description" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- heading "Reply with a one-sentence description" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- heading "Reply with a one-sentence description" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- heading "Reply with a one-sentence description" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the read tool twice" [disabled]
|
||||
- heading "Use the read tool twice" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- 'button "Plan a small change: add" [disabled]'
|
||||
- 'heading "Plan a small change: add" [level=1]'
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the ask_user_question tool to" [disabled]
|
||||
- heading "Use the ask_user_question tool to" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- heading "Reply with a one-sentence description" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- heading "Reply with a one-sentence description" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
43
apps/web/tests/snapshots/queue-actions/preserved.expected.md
Normal file
43
apps/web/tests/snapshots/queue-actions/preserved.expected.md
Normal file
@@ -0,0 +1,43 @@
|
||||
- banner:
|
||||
- heading "Reply with a one-sentence description" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- button "Context injection":
|
||||
- img
|
||||
- img
|
||||
- text: Context injection
|
||||
- paragraph: partial
|
||||
- text: Stopped
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Edited queue item {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- paragraph: partial
|
||||
- status: Deep diving...
|
||||
- list:
|
||||
- listitem:
|
||||
- text: Queue item preserved after stop
|
||||
- button "Edit queued message":
|
||||
- img
|
||||
- button "Remove queued message":
|
||||
- img
|
||||
- textbox "Message the agent"
|
||||
- button "Commands":
|
||||
- img
|
||||
- 'button "Access mode, current: Full access"': Full access
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Stop generating"
|
||||
- text: 1 turns · 1 steps Input 0 tok · Output 0 tok
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- heading "Reply with a one-sentence description" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the read tool twice" [disabled]
|
||||
- heading "Use the read tool twice" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the read tool twice" [disabled]
|
||||
- heading "Use the read tool twice" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the ask_user_question tool to" [disabled]
|
||||
- heading "Use the ask_user_question tool to" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the ask_user_question tool to" [disabled]
|
||||
- heading "Use the ask_user_question tool to" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use web_search to search exactly" [disabled]
|
||||
- heading "Use web_search to search exactly" [level=1]
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
Reference in New Issue
Block a user