mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
The composer seat spent nearly all its life disabled: a session's composition is fixed once a turn has run. Move the choice to the new-session screen beside the workspace picker, where it still works, and let the session header report what a running session runs. The hero pick is staged rather than applied — that screen precedes the session it belongs to. It lands when a session becomes current and is still blank, which covers both the session a workspace connect creates and the blank one it reuses; riding `sessions.create` would miss the second. It is spent on first use, matching the workspace picker. Fix the durability the header field claimed but never had: `agentPreset` was declared on `SessionHeader` and dropped by the JSONL header line, the SQLite sessions row, the derived query index, and the cold list projection, so every resumed session came back composed from nothing. Add the web e2e lane that would have caught it — the one lane that mounts the shipped roster, which needed `cordis:group` in the scaffold's Loader builtins, as `mountRootInclude` already registers.
154 lines
7.1 KiB
TypeScript
154 lines
7.1 KiB
TypeScript
// Web e2e scenario: agent-preset selection. The roster's `roots` is an
|
|
// assembly fact the CLI entry resolves and patches in, so every other lane
|
|
// boots with an empty roster and no preset surface at all; this is the one
|
|
// lane that mounts the SHIPPED presets and puts them in front of a browser.
|
|
//
|
|
// Two surfaces, one host rule: a session's composition is fixed when the
|
|
// session starts. Before that, the new-session chip stages the choice beside
|
|
// the workspace picker — the only screen where it still works. After it, the
|
|
// session header names what the session runs and offers no control at all,
|
|
// because the host answers `agent-preset-locked` to anything else.
|
|
//
|
|
// Zero model calls: no replay fixture mounts, so a stray stream fails loud.
|
|
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, seedSession, watchConsole,
|
|
webSnapshotMode, type WebScaffold,
|
|
} from './scaffold.ts'
|
|
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
|
|
|
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/agent-preset-selection', import.meta.url))
|
|
const HERO_EXPECTED = join(SNAPSHOT_DIR, 'hero.expected.md')
|
|
const MENU_EXPECTED = join(SNAPSHOT_DIR, 'menu.expected.md')
|
|
const HEADER_EXPECTED = join(SNAPSHOT_DIR, 'header.expected.md')
|
|
/** The shipped roster, beside the composition that names it. */
|
|
const SHIPPED_PRESETS = fileURLToPath(new URL('../../cli/config/agent-presets', import.meta.url))
|
|
const MODE = webSnapshotMode()
|
|
const SEED_ID = 'agent-preset-selection-web-e2e'
|
|
|
|
/**
|
|
* A settled one-turn session with no model content: this lane asserts chrome
|
|
* around a conversation, not a conversation, and a recorded turn would tie
|
|
* the golden to a provider's wording for no gain.
|
|
* @returns a tokenized session log ending on a closed turn.
|
|
*/
|
|
function seedLog(): string {
|
|
const time = 1784974100000
|
|
const at = (index: number, event: Record<string, unknown>): string =>
|
|
JSON.stringify({ ...event, seq: index, time: time + index })
|
|
return [
|
|
JSON.stringify({ type: 'session', version: 0, id: '{{sessionId}}', createdAt: time, cwd: '{{cwd}}/workspace' }),
|
|
at(0, { type: 'turn/start', data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user', rpcId: 'seed' } } } }),
|
|
at(1, {
|
|
type: 'user/message',
|
|
data: { content: [{ type: 'text', text: 'Seeded turn.' }], source: { kind: 'user', rpcId: 'seed' } },
|
|
surfaceOp: 'append',
|
|
}),
|
|
at(2, { type: 'session/title', data: { title: 'Seeded turn', messageSeqs: [1], source: { kind: 'fallback' } } }),
|
|
at(3, { type: 'turn/end', data: { turn: 1, reason: { kind: 'completed' } } }),
|
|
].join('\n')
|
|
}
|
|
|
|
describe('web e2e: agent-preset selection', () => {
|
|
let scaffold: WebScaffold
|
|
let browser: Browser
|
|
let page: Page
|
|
let tripwire: ReturnType<typeof watchConsole>
|
|
|
|
beforeAll(async () => {
|
|
scaffold = await launchWebScaffold({
|
|
agentPresets: { roots: [{ path: SHIPPED_PRESETS, trust: 'system' }], default: 'standard' },
|
|
})
|
|
// A resumed session runs what it was created with; seeding one that
|
|
// records `minimal` is what makes the header label a claim about the
|
|
// session rather than an echo of the current default.
|
|
await seedSession(scaffold, seedLog(), SEED_ID, 'minimal')
|
|
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('offers the chip on the new-session screen, beside the workspace picker', async () => {
|
|
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-hero'))
|
|
await connectFreshWorkspace(page, scaffold.workspaceCwd)
|
|
|
|
const snapshot = await captureStableAria(page, '[class*="heroWorkspaceRow"]', scaffold.workspaceCwd)
|
|
|
|
await compareOrRefreshGolden(HERO_EXPECTED, snapshot, MODE)
|
|
// The chip opens on the deployment default, by the name that preset
|
|
// publishes rather than its directory name.
|
|
expect(snapshot).toContain('标准模式')
|
|
})
|
|
|
|
it('names every preset and what it is for', async () => {
|
|
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-menu'))
|
|
await page.getByRole('button', { name: '标准模式' }).click()
|
|
const menu = page.getByRole('menu')
|
|
await menu.waitFor({ timeout: 10_000 })
|
|
|
|
const snapshot = await captureStableAria(page, '[role="menu"]', scaffold.workspaceCwd)
|
|
|
|
await compareOrRefreshGolden(MENU_EXPECTED, snapshot, MODE)
|
|
// Every shipped preset, each with the sentence saying what it composes —
|
|
// the id alone never said what a preset does.
|
|
expect(snapshot).toContain('极简模式')
|
|
expect(snapshot).toContain('创造模式')
|
|
await page.keyboard.press('Escape')
|
|
})
|
|
|
|
it('applies the staged pick to the blank session, and the host honors it', async () => {
|
|
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-stage'))
|
|
await page.getByRole('button', { name: '标准模式' }).click()
|
|
await page.getByRole('menuitem', { name: /极简模式/ }).click()
|
|
|
|
// The chip stages; the blank session the workspace connect produced is
|
|
// what the stage lands on. The host's own answer is what comes back.
|
|
await expect.poll(async () => {
|
|
const response = await fetch(`${scaffold.baseUrl}/api/session.list`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({
|
|
type: 'client-request', rpcId: 'agent-preset-stage', method: 'session.list', payload: {},
|
|
}),
|
|
})
|
|
const body = await response.json() as {
|
|
result: { value?: { sessions: { blank: boolean; agentPreset?: string }[] } }
|
|
}
|
|
return JSON.stringify(body.result.value?.sessions ?? body.result)
|
|
}, { timeout: 15_000 }).toContain('minimal')
|
|
})
|
|
|
|
it('labels a resumed session with the preset it was created under', async () => {
|
|
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-header'))
|
|
// The seeded session's cwd is the scaffold root rather than the connected
|
|
// workspace, so it lists under Ungrouped; the group collapses by default.
|
|
await page.getByRole('treeitem', { name: /^Ungrouped/ }).click()
|
|
await page.locator('[role="treeitem"]').last().click()
|
|
await page.getByText('Seeded turn.').waitFor({ timeout: 15_000 })
|
|
|
|
const snapshot = await captureStableAria(page, '[class*="titleRow"]', scaffold.workspaceCwd)
|
|
|
|
await compareOrRefreshGolden(HEADER_EXPECTED, snapshot, MODE)
|
|
expect(snapshot).toContain('极简模式')
|
|
// Static chrome, not a control: the header can only report a composition
|
|
// the host would refuse to change.
|
|
expect(snapshot).not.toContain('button "极简模式"')
|
|
})
|
|
|
|
it('drove every surface without a page error or a stream warning', () => {
|
|
expect(tripwire.pageErrors).toEqual([])
|
|
expect(tripwire.warnings).toEqual([])
|
|
})
|
|
})
|