fix(web): hide verified cold blank sessions

This commit is contained in:
_Kerman
2026-08-13 14:04:16 +08:00
parent 2c456a00ea
commit 3c06c66ac4
26 changed files with 548 additions and 125 deletions
+60
View File
@@ -0,0 +1,60 @@
/** Cold Session list visibility through the shipped compressed JSONL backend. */
import { mkdir, stat } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import { join } from 'node:path'
import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import {
captureStableAria, compareOrRefreshGolden, launchWebScaffold, seedBlankSession,
watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { newEnglishPage, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/cold-blank-session', import.meta.url))
const SIDEBAR_EXPECTED = join(SNAPSHOT_DIR, 'sidebar.expected.md')
const MODE = webSnapshotMode()
const SESSION_ID = 'cold-blank-session-web-e2e'
const WORKSPACE_NAME = 'cold-blank-workspace'
describe('web e2e: cold blank Session visibility', () => {
let scaffold: WebScaffold
let browser: Browser
let page: Page
let tripwire: ReturnType<typeof watchConsole>
beforeAll(async () => {
scaffold = await launchWebScaffold({})
const cwd = join(scaffold.workspaceCwd, WORKSPACE_NAME)
await mkdir(cwd, { recursive: true })
await seedBlankSession(scaffold, SESSION_ID, cwd)
const header = (await scaffold.ctx.sessionPersistence.list())
.find(candidate => candidate.id === SESSION_ID)
if (header === undefined) throw new Error('blank Session fixture did not materialize')
const location = scaffold.ctx.sessionPersistence.locate(header)
if (location === undefined) throw new Error('JSONL fixture has no physical artifact')
expect((await stat(location.path)).size).toBeLessThanOrEqual(1024)
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 })
}, 120_000)
afterAll(async () => {
await browser?.close()
await scaffold?.close()
})
it('keeps the verified cold blank Session out of the sidebar', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-cold-blank-session'))
const tree = page.getByRole('tree', { name: 'Sessions' })
await tree.waitFor({ timeout: 30_000 })
expect(await tree.getByText(WORKSPACE_NAME, { exact: true }).count()).toBe(0)
const sidebar = await captureStableAria(page, '[role="tree"][aria-label="Sessions"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(SIDEBAR_EXPECTED, sidebar, MODE)
expect(tripwire.pageErrors).toEqual([])
})
})
+33 -8
View File
@@ -23,7 +23,7 @@
// (the plugin-row path discards the ReplayHandle; the direct install keeps
// assertConsumed for the teardown fixture-consumption check).
import { existsSync } from 'node:fs'
import { mkdir, mkdtemp, readFile, readdir, realpath, rm, utimes, writeFile } from 'node:fs/promises'
import { mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
@@ -724,6 +724,38 @@ export async function seedSession(
delegationDepth: 0,
...agentPreset === undefined ? {} : { agentPreset },
}
await persistSeedSession(scaffold, meta, events)
return meta.id
}
/** Seed one materialized cold Session whose log has no turn/start event. */
export async function seedBlankSession(
scaffold: WebScaffold,
id: string,
cwd: string,
): Promise<SessionId> {
const meta: SessionHeader = {
version: SESSION_FORMAT_VERSION,
id: SessionId(id),
createdAt: Date.now() - 60_000,
cwd,
delegationDepth: 0,
}
await persistSeedSession(scaffold, meta, [{
type: 'session/end-seed',
seq: 0,
time: meta.createdAt,
data: {},
}])
return meta.id
}
/** Materialize one detached Session fixture through the shipped JSONL provider. */
async function persistSeedSession(
scaffold: WebScaffold,
meta: SessionHeader,
events: readonly SessionEvent[],
): Promise<void> {
const seeder = new Context()
try {
await seeder.plugin(SessionStore)
@@ -732,16 +764,9 @@ export async function seedSession(
await seeder.plugin(JsonlSessionPersistence, { root: scaffold.persistenceRoot })
await seeder.sessionPersistence.create(meta)
await seeder.sessionPersistence.append(meta.id, events)
// Deterministic sidebar order: cold summaries take updatedAt from mtime.
const located = seeder.sessionPersistence.locate(meta)
if (located !== undefined) {
const backdated = new Date(meta.createdAt)
await utimes(located.path, backdated, backdated)
}
} finally {
await seeder.fiber.dispose()
}
return meta.id
}
/**
@@ -0,0 +1 @@
- tree "Sessions": No sessions yet
+1
View File
@@ -48,6 +48,7 @@
"tests/replay-round-trip.e2e.ts",
"tests/hmr-live.e2e.ts",
"tests/seeded-history.e2e.ts",
"tests/cold-blank-session.e2e.ts",
"tests/stats-paged-history.e2e.ts",
"tests/sidebar-scrollbar.e2e.ts",
"tests/conversation-column-overflow.e2e.ts",