Merge remote-tracking branch 'origin/master' into worktree/custom-deepseek-models
# Conflicts: # packages/host/apiproxy/README.i18n.yaml
This commit is contained in:
@@ -27,11 +27,12 @@ import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './suppor
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/live-interactions', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
// One golden per interactive end-state: what the user is left looking at
|
||||
// after cancel, after a non-retryable failure (pins the FIXME(web-error-surface)
|
||||
// gap as a reviewable artifact: NO error copy in the tree), and after retry
|
||||
// recovery — three genuinely different terminal surfaces of one fixture.
|
||||
// One golden pins the stable mid-turn loading state; the other three capture
|
||||
// what the user is left looking at after cancel, after a non-retryable failure
|
||||
// (pins the FIXME(web-error-surface) gap as a reviewable artifact: NO error
|
||||
// copy in the tree), and after retry recovery.
|
||||
const CANCEL_EXPECTED = join(SNAPSHOT_DIR, 'cancel.expected.md')
|
||||
const LOADING_EXPECTED = join(SNAPSHOT_DIR, 'loading.expected.md')
|
||||
const ERROR_EXPECTED = join(SNAPSHOT_DIR, 'error-auth.expected.md')
|
||||
const RETRY_EXPECTED = join(SNAPSHOT_DIR, 'retry.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
@@ -133,6 +134,12 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
|
||||
// The marker IS the synchronization: the stream is provably parked in the
|
||||
// hang (prefix chunks delivered to the loop) before the stop click.
|
||||
await expect.poll(() => existsSync(marker), { timeout: 15_000 }).toBe(true)
|
||||
await expect.poll(
|
||||
() => page.getByRole('status').filter({ hasText: 'Deep diving...' }).isVisible(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(true)
|
||||
const loadingSnapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd)
|
||||
await compareOrRefreshGolden(LOADING_EXPECTED, loadingSnapshot, MODE)
|
||||
await page.getByRole('button', { name: 'Stop generating' }).click()
|
||||
await settled
|
||||
expect(turnEndReasons(sessionEvents).at(-1)).toBe('aborted')
|
||||
@@ -231,7 +238,7 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, [
|
||||
'session.jsonl', 'cancel.expected.md', 'error-auth.expected.md', 'retry.expected.md',
|
||||
'session.jsonl', 'cancel.expected.md', 'loading.expected.md', 'error-auth.expected.md', 'retry.expected.md',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
- treeitem "workspace 1 session" [expanded]:
|
||||
- img
|
||||
- text: workspace 1 session
|
||||
- treeitem "New Session now" [selected]
|
||||
- treeitem "New Session" [selected]
|
||||
- button "Settings":
|
||||
- img
|
||||
- text: Settings
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
- treeitem "workspace 1 session" [expanded]:
|
||||
- img
|
||||
- text: workspace 1 session
|
||||
- treeitem "New Session now" [selected]
|
||||
- treeitem "New Session" [selected]
|
||||
- button "Settings":
|
||||
- img
|
||||
- text: Settings
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- 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 "Edit":
|
||||
- img
|
||||
- paragraph: partial
|
||||
- status: Deep diving...
|
||||
- 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"
|
||||
@@ -12,6 +12,7 @@
|
||||
- button "Edit":
|
||||
- img
|
||||
- paragraph: partial
|
||||
- status: Deep diving...
|
||||
- button "2 queued messages"
|
||||
- textbox "Message the agent"
|
||||
- button "Commands":
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
- button "Edit":
|
||||
- img
|
||||
- paragraph: partial
|
||||
- status: Deep diving...
|
||||
- button "2 queued messages" [disabled] [expanded]
|
||||
- list:
|
||||
- listitem:
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
- button "Edit":
|
||||
- img
|
||||
- paragraph: partial
|
||||
- status: Deep diving...
|
||||
- list:
|
||||
- listitem:
|
||||
- text: Edited queue item
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
- img
|
||||
- img
|
||||
- text: Ask question waiting
|
||||
- status: Deep diving...
|
||||
- region "Ready to continue?":
|
||||
- text: Checkpoint
|
||||
- heading "Ready to continue?" [level=2]
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// Web e2e scenarios: workspace management — the create-by-name dialog, the
|
||||
// rename round trip over the real wire (workspace.rename RPC + durable
|
||||
// registry), duplicate-name pre-check, the flat "In one list" view with its
|
||||
// persisted group-by preference, and the session hover card. Zero model
|
||||
// calls: workspace.create/rename are host RPCs with no model involvement,
|
||||
// and the one session row the flat/hover scenarios need comes from a seeded
|
||||
// fixture (the seeded-history seed reused verbatim — no new recording).
|
||||
// persisted group-by preference, the session hover card, and the session
|
||||
// archive round trip (row menu → workspace.archiveSession RPC → durable
|
||||
// global set → row hidden across reload). Zero model calls:
|
||||
// workspace.create/rename/archiveSession are host RPCs with no model
|
||||
// involvement, and the one session row the flat/hover/archive scenarios need
|
||||
// comes from a seeded fixture (the seeded-history seed reused verbatim — no
|
||||
// new recording).
|
||||
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
@@ -413,6 +416,55 @@ describe('web e2e: workspace management (create / rename / flat view / hover car
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('archives the seeded session from its row menu, hiding it durably across reload', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-ws-archive'))
|
||||
// The seeded session lives under Ungrouped (expanded by the hover-card
|
||||
// test's gesture; converge again for order independence).
|
||||
const ungroupedRow = page.getByText('Ungrouped', { exact: true }).locator('..').locator('..')
|
||||
const ungroupedSection = ungroupedRow.locator('..')
|
||||
await expect.poll(async () => {
|
||||
if (await ungroupedRow.getAttribute('aria-expanded') !== 'true') {
|
||||
await page.getByText('Ungrouped', { exact: true }).click()
|
||||
await page.waitForTimeout(50)
|
||||
}
|
||||
return await ungroupedRow.getAttribute('aria-expanded')
|
||||
}, { timeout: 5_000 }).toBe('true')
|
||||
// Anchor on session rows (the rows carrying a session actions button),
|
||||
// not a positional index, and assert the single-stray assumption loudly
|
||||
// so a fixture gaining a second stray fails here instead of archiving
|
||||
// the wrong row. CSS attribute match, not getByRole: the button is
|
||||
// display:none until its row hovers, and role queries skip hidden nodes.
|
||||
const sessionRows = ungroupedSection.locator('[role="treeitem"]')
|
||||
.filter({ has: page.locator('button[aria-label^="Session actions for "]') })
|
||||
await expect.poll(() => sessionRows.count(), { timeout: 10_000 }).toBe(1)
|
||||
const sessionRow = sessionRows.first()
|
||||
const rowTitle = await sessionRow.locator('[class*="title"]').innerText()
|
||||
// Row menu: hover reveals the actions button; Archive session commits
|
||||
// without a confirmation dialog (non-destructive: log + accounting stay).
|
||||
await sessionRow.hover()
|
||||
await sessionRow.getByRole('button', { name: `Session actions for ${rowTitle}` }).click()
|
||||
await page.getByRole('menuitem', { name: 'Archive session' }).click()
|
||||
// The row disappears on the archive-set echo; with no other visible
|
||||
// stray, the whole Ungrouped bucket withdraws.
|
||||
await expect.poll(() => page.getByText(rowTitle, { exact: true }).count(), { timeout: 10_000 }).toBe(0)
|
||||
await expect.poll(() => page.getByText('Ungrouped', { exact: true }).count(), { timeout: 10_000 }).toBe(0)
|
||||
// Durable on the host: the registry-global set carries the id while the
|
||||
// session log itself stays in persistence untouched.
|
||||
expect([...scaffold.ctx.workspace.archivedSessionIds]).toEqual([SessionId(SEED_ID)])
|
||||
expect((await scaffold.ctx.sessionPersistence.list()).map(header => header.id)).toContain(SessionId(SEED_ID))
|
||||
// Reload: the hidden state is rebuilt from the workspace.list baseline.
|
||||
const warningStart = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
await expect.poll(() => page.getByText('Workspaces', { exact: true }).count(), { timeout: 15_000 }).toBe(1)
|
||||
// The archived row must not resurface (the Ungrouped bucket itself may
|
||||
// reappear if selection restore lands on another stray — not this test's
|
||||
// concern).
|
||||
expect(await page.getByText(rowTitle, { exact: true }).count()).toBe(0)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 90_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('issued zero model calls and stayed clean', async () => {
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
// The directory-browser aria golden is this spec's one owned artifact;
|
||||
|
||||
Reference in New Issue
Block a user