Merge latest Web transcript parent into manual compaction

# Conflicts:
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	packages/client/runtime/README.i18n.yaml
This commit is contained in:
Tianyi Cui
2026-07-31 14:39:20 +08:00
101 changed files with 1519 additions and 210 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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;