refactor(preset): bundle the shipped presets inside dsh-agent-presets
Review asked why the launcher special-cases one plugin's row. It no
longer does: the four shipped compositions move into the package
(presets/, in files), dsh-agent-presets resolves its own shipped root
and prepends it before configured roots (includeShippedRoot, default
true, opt-out for bare-machinery embedders), and the per-composition
derived patch, its spec, and the dump layer are deleted — profile-boot
and dump-config return to plain layer stacking. The always-load
guarantee now rides the schema default instead of patch ordering, so a
whole-config replacement keeps the shipped set and the squash, reload
freeze, and dump divergence stop being possible.
Gate globs, the web scaffold, and both preset browser lanes drop their
hand-fed shipped roots; the roster e2e keeps asserting configured roots
beside the shipped four against the built lib.
Fixes #2863.
2026-08-21 12:37:57 +08:00
|
|
|
// Web e2e scenario: agent-preset selection. Every lane mounts the plugin's
|
|
|
|
|
// own shipped presets; this is the lane that puts them in front of a browser.
|
feat(web): choose the agent preset on the new-session screen
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.
2026-08-05 18:51:11 +08:00
|
|
|
//
|
|
|
|
|
// 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'
|
2026-08-10 14:35:13 +08:00
|
|
|
import { mkdir, writeFile } from 'node:fs/promises'
|
feat(web): choose the agent preset on the new-session screen
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.
2026-08-05 18:51:11 +08:00
|
|
|
import { join } from 'node:path'
|
|
|
|
|
import type { Browser, Page } from 'playwright'
|
|
|
|
|
import { chromium } from 'playwright'
|
|
|
|
|
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
2026-08-10 19:53:58 +08:00
|
|
|
import {
|
|
|
|
|
SESSION_FORMAT_VERSION, SessionId as sessionId, type SessionEvent, type SessionId,
|
|
|
|
|
} from '@deepseek-ai/dsh-session'
|
|
|
|
|
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
|
feat(web): choose the agent preset on the new-session screen
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.
2026-08-05 18:51:11 +08:00
|
|
|
import {
|
|
|
|
|
captureStableAria, compareOrRefreshGolden, launchWebScaffold, seedSession, watchConsole,
|
|
|
|
|
webSnapshotMode, type WebScaffold,
|
|
|
|
|
} from './scaffold.ts'
|
|
|
|
|
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
|
|
|
|
|
2026-08-24 07:15:34 +08:00
|
|
|
const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/agent-preset-selection', import.meta.url))
|
feat(web): choose the agent preset on the new-session screen
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.
2026-08-05 18:51:11 +08:00
|
|
|
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')
|
|
|
|
|
const MODE = webSnapshotMode()
|
|
|
|
|
const SEED_ID = 'agent-preset-selection-web-e2e'
|
2026-08-13 00:36:22 +08:00
|
|
|
/** A project skill only a preset that mounts `skill-filesystem` can discover. */
|
2026-08-10 14:35:13 +08:00
|
|
|
const SKILL_NAME = 'preset-catalog-demo'
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Seed one project skill under the connected workspace.
|
|
|
|
|
*
|
|
|
|
|
* Local skill discovery is a PRESET row, so this file is visible through
|
|
|
|
|
* `standard` and invisible through `minimal` — which makes the '/' menu's
|
|
|
|
|
* skill group a statement about the session's composition.
|
|
|
|
|
* @param workspaceCwd - the scaffold's temp project parent.
|
|
|
|
|
*/
|
|
|
|
|
async function seedWorkspaceSkill(workspaceCwd: string): Promise<void> {
|
|
|
|
|
const directory = join(workspaceCwd, 'workspace', '.agents', 'skills', SKILL_NAME)
|
|
|
|
|
await mkdir(directory, { recursive: true })
|
|
|
|
|
await writeFile(join(directory, 'SKILL.md'), [
|
|
|
|
|
'---',
|
|
|
|
|
`name: ${SKILL_NAME}`,
|
|
|
|
|
'description: Prove the slash catalog follows the session composition',
|
|
|
|
|
'---',
|
|
|
|
|
'',
|
|
|
|
|
'Body.',
|
|
|
|
|
'',
|
|
|
|
|
].join('\n'))
|
|
|
|
|
}
|
feat(web): choose the agent preset on the new-session screen
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.
2026-08-05 18:51:11 +08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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')
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-10 19:53:58 +08:00
|
|
|
/**
|
|
|
|
|
* Persist one child so the assembled header snapshot exercises both action
|
|
|
|
|
* contributors whose relative order is the product contract under test.
|
|
|
|
|
* @param scaffold - the booted Web scaffold.
|
|
|
|
|
* @param parentId - the seeded session whose header the browser opens.
|
|
|
|
|
*/
|
|
|
|
|
async function seedSubagent(scaffold: WebScaffold, parentId: SessionId): Promise<void> {
|
|
|
|
|
const childId = sessionId('agent-preset-selection-child')
|
|
|
|
|
const createdAt = 1784974100100
|
|
|
|
|
await scaffold.ctx.sessionPersistence.create({
|
|
|
|
|
version: SESSION_FORMAT_VERSION,
|
|
|
|
|
id: childId,
|
|
|
|
|
createdAt,
|
|
|
|
|
cwd: scaffold.workspaceCwd,
|
|
|
|
|
parentSession: parentId,
|
|
|
|
|
origin: 'subagent',
|
|
|
|
|
delegationDepth: 1,
|
|
|
|
|
agentPreset: 'minimal',
|
|
|
|
|
})
|
|
|
|
|
await scaffold.ctx.sessionPersistence.append(childId, [
|
|
|
|
|
{
|
|
|
|
|
type: 'turn/start',
|
|
|
|
|
seq: 0,
|
|
|
|
|
time: createdAt,
|
|
|
|
|
data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'user/message',
|
|
|
|
|
seq: 1,
|
|
|
|
|
time: createdAt + 1,
|
|
|
|
|
data: {
|
|
|
|
|
content: [{ type: 'text', text: 'Check the session-header action order.' }],
|
|
|
|
|
source: { kind: 'user' },
|
|
|
|
|
},
|
|
|
|
|
surfaceOp: 'append',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'subagent/descriptor',
|
|
|
|
|
seq: 2,
|
|
|
|
|
time: createdAt + 2,
|
|
|
|
|
data: snapshotSubagentDescriptor({
|
|
|
|
|
mode: 'one-shot', provider: 'spawn', label: 'header order probe',
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'turn/end',
|
|
|
|
|
seq: 3,
|
|
|
|
|
time: createdAt + 3,
|
|
|
|
|
data: { turn: 1, reason: { kind: 'completed' } },
|
|
|
|
|
},
|
|
|
|
|
] as SessionEvent[])
|
|
|
|
|
await scaffold.ctx.sessionProjectionCache.coldSnapshot(childId)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-10 14:35:13 +08:00
|
|
|
/**
|
|
|
|
|
* The preset the host reports for the blank session the workspace connect
|
|
|
|
|
* produced. Addressed by id rather than by scanning the serialized list: the
|
|
|
|
|
* seeded session records `minimal` too, so a substring match over the whole
|
|
|
|
|
* list answers before the switch has landed.
|
|
|
|
|
* @param baseUrl - the scaffold's origin.
|
|
|
|
|
* @returns the live session's preset, or undefined before it is listed.
|
|
|
|
|
*/
|
|
|
|
|
async function livePreset(baseUrl: string): Promise<string | undefined> {
|
2026-08-22 21:11:51 +08:00
|
|
|
const response = await fetch(`${baseUrl}/api/session/list`, {
|
2026-08-10 14:35:13 +08:00
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'content-type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
2026-08-22 21:11:51 +08:00
|
|
|
type: 'client-request', rpcId: 'agent-preset-live', method: 'session/list',
|
|
|
|
|
payload: { args: { _request: {} } },
|
2026-08-10 14:35:13 +08:00
|
|
|
}),
|
|
|
|
|
})
|
|
|
|
|
const body = await response.json() as {
|
|
|
|
|
result: { value?: { items: { sessionId: string; agentPreset?: string }[] } }
|
|
|
|
|
}
|
|
|
|
|
return body.result.value?.items.find(item => item.sessionId !== SEED_ID)?.agentPreset
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Every option label the trigger menu currently lists. */
|
|
|
|
|
async function menuOptions(page: Page): Promise<string[]> {
|
|
|
|
|
const menu = page.getByRole('listbox', { name: 'Trigger suggestions' })
|
|
|
|
|
await menu.waitFor({ timeout: 10_000 })
|
|
|
|
|
return await menu.getByRole('option').allTextContents()
|
|
|
|
|
}
|
|
|
|
|
|
feat(web): choose the agent preset on the new-session screen
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.
2026-08-05 18:51:11 +08:00
|
|
|
describe('web e2e: agent-preset selection', () => {
|
|
|
|
|
let scaffold: WebScaffold
|
|
|
|
|
let browser: Browser
|
|
|
|
|
let page: Page
|
|
|
|
|
let tripwire: ReturnType<typeof watchConsole>
|
|
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
refactor(preset): bundle the shipped presets inside dsh-agent-presets
Review asked why the launcher special-cases one plugin's row. It no
longer does: the four shipped compositions move into the package
(presets/, in files), dsh-agent-presets resolves its own shipped root
and prepends it before configured roots (includeShippedRoot, default
true, opt-out for bare-machinery embedders), and the per-composition
derived patch, its spec, and the dump layer are deleted — profile-boot
and dump-config return to plain layer stacking. The always-load
guarantee now rides the schema default instead of patch ordering, so a
whole-config replacement keeps the shipped set and the squash, reload
freeze, and dump divergence stop being possible.
Gate globs, the web scaffold, and both preset browser lanes drop their
hand-fed shipped roots; the roster e2e keeps asserting configured roots
beside the shipped four against the built lib.
Fixes #2863.
2026-08-21 12:37:57 +08:00
|
|
|
// The scaffold's default roster pin is exactly this scenario's shape: the
|
|
|
|
|
// plugin's shipped presets, default `standard`.
|
|
|
|
|
scaffold = await launchWebScaffold({})
|
feat(web): choose the agent preset on the new-session screen
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.
2026-08-05 18:51:11 +08:00
|
|
|
// 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.
|
2026-08-10 19:53:58 +08:00
|
|
|
const seededId = await seedSession(scaffold, seedLog(), SEED_ID, 'minimal')
|
|
|
|
|
await seedSubagent(scaffold, seededId)
|
2026-08-10 14:35:13 +08:00
|
|
|
await seedWorkspaceSkill(scaffold.workspaceCwd)
|
feat(web): choose the agent preset on the new-session screen
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.
2026-08-05 18:51:11 +08:00
|
|
|
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.
|
2026-08-10 19:40:19 +08:00
|
|
|
expect(snapshot).toContain('Standard mode')
|
feat(web): choose the agent preset on the new-session screen
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.
2026-08-05 18:51:11 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('names every preset and what it is for', async () => {
|
|
|
|
|
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-menu'))
|
2026-08-10 19:40:19 +08:00
|
|
|
await page.getByRole('button', { name: 'Standard mode' }).click()
|
feat(web): choose the agent preset on the new-session screen
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.
2026-08-05 18:51:11 +08:00
|
|
|
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.
|
2026-08-10 19:40:19 +08:00
|
|
|
expect(snapshot).toContain('Minimal mode')
|
|
|
|
|
expect(snapshot).toContain('Creator mode')
|
feat(web): choose the agent preset on the new-session screen
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.
2026-08-05 18:51:11 +08:00
|
|
|
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'))
|
2026-08-10 19:40:19 +08:00
|
|
|
await page.getByRole('button', { name: 'Standard mode' }).click()
|
|
|
|
|
await page.getByRole('menuitem', { name: /Minimal mode/ }).click()
|
feat(web): choose the agent preset on the new-session screen
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.
2026-08-05 18:51:11 +08:00
|
|
|
|
|
|
|
|
// 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.
|
2026-08-10 14:35:13 +08:00
|
|
|
await expect.poll(() => livePreset(scaffold.baseUrl), { timeout: 15_000 }).toBe('minimal')
|
feat(web): choose the agent preset on the new-session screen
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.
2026-08-05 18:51:11 +08:00
|
|
|
})
|
|
|
|
|
|
2026-08-10 14:35:13 +08:00
|
|
|
it('re-reads the slash catalog through the composition the switch installed', async () => {
|
2026-08-10 15:12:07 +08:00
|
|
|
// Continues the previous case: the chip has already applied `minimal` to
|
|
|
|
|
// the blank session, and this one reads the menu that switch left behind.
|
2026-08-10 14:35:13 +08:00
|
|
|
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-slash-catalog'))
|
|
|
|
|
const composer = page.locator('textarea:enabled').last()
|
|
|
|
|
|
2026-08-10 15:12:07 +08:00
|
|
|
// `minimal` mounts neither the compaction group nor plan mode nor local
|
|
|
|
|
// skill discovery, so the catalog the composer warmed under the
|
|
|
|
|
// deployment default must not survive the switch.
|
2026-08-10 14:35:13 +08:00
|
|
|
await composer.fill('/')
|
|
|
|
|
await expect.poll(() => menuOptions(page), { timeout: 15_000 })
|
|
|
|
|
.not.toEqual(expect.arrayContaining([expect.stringContaining(SKILL_NAME)]))
|
|
|
|
|
const onMinimal = await menuOptions(page)
|
|
|
|
|
expect(onMinimal.some(option => option.startsWith('compact'))).toBe(false)
|
|
|
|
|
expect(onMinimal.some(option => option.startsWith('plan'))).toBe(false)
|
2026-08-24 09:18:48 +08:00
|
|
|
// Preset-scoped commands follow the switch; the client's own model command
|
|
|
|
|
// remains outside every preset.
|
|
|
|
|
expect(onMinimal.some(option => option.startsWith('goal'))).toBe(false)
|
2026-08-10 14:35:13 +08:00
|
|
|
expect(onMinimal.some(option => option.startsWith('model'))).toBe(true)
|
|
|
|
|
await composer.fill('')
|
|
|
|
|
|
|
|
|
|
// Switching back up reaches the host at all — the chip compares the pick
|
|
|
|
|
// against its list row, so a row that never reprojected the first switch
|
|
|
|
|
// answers "already standard" and sends nothing — and restores the catalog
|
|
|
|
|
// instead of leaving the session reading the narrower composition.
|
2026-08-10 19:40:19 +08:00
|
|
|
await page.getByRole('button', { name: 'Minimal mode' }).click()
|
|
|
|
|
await page.getByRole('menuitem', { name: /^Standard mode/ }).first().click()
|
2026-08-10 14:35:13 +08:00
|
|
|
await expect.poll(() => livePreset(scaffold.baseUrl), { timeout: 15_000 }).toBe('standard')
|
|
|
|
|
|
|
|
|
|
await composer.fill('/')
|
|
|
|
|
await expect.poll(() => menuOptions(page), { timeout: 15_000 })
|
|
|
|
|
.toEqual(expect.arrayContaining([expect.stringContaining(SKILL_NAME)]))
|
|
|
|
|
const onStandard = await menuOptions(page)
|
|
|
|
|
expect(onStandard.some(option => option.startsWith('compact'))).toBe(true)
|
2026-08-24 09:18:48 +08:00
|
|
|
expect(onStandard.some(option => option.startsWith('goal'))).toBe(true)
|
2026-08-10 14:35:13 +08:00
|
|
|
expect(onStandard.some(option => option.startsWith('plan'))).toBe(true)
|
|
|
|
|
await composer.fill('')
|
|
|
|
|
}, 90_000)
|
|
|
|
|
|
feat(web): choose the agent preset on the new-session screen
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.
2026-08-05 18:51:11 +08:00
|
|
|
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)
|
2026-08-10 19:40:19 +08:00
|
|
|
expect(snapshot).toContain('Minimal mode')
|
2026-08-10 19:53:58 +08:00
|
|
|
expect(snapshot).toContain('button "1 subagent"')
|
2026-08-20 16:01:57 +08:00
|
|
|
expect(snapshot.indexOf('button "1 subagent"')).toBeLessThan(snapshot.indexOf('Minimal mode'))
|
|
|
|
|
expect(snapshot.indexOf('Minimal mode')).toBeLessThan(snapshot.indexOf('button "Session log"'))
|
feat(web): choose the agent preset on the new-session screen
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.
2026-08-05 18:51:11 +08:00
|
|
|
// Static chrome, not a control: the header can only report a composition
|
|
|
|
|
// the host would refuse to change.
|
2026-08-10 19:40:19 +08:00
|
|
|
expect(snapshot).not.toContain('button "Minimal mode"')
|
feat(web): choose the agent preset on the new-session screen
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.
2026-08-05 18:51:11 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('drove every surface without a page error or a stream warning', () => {
|
|
|
|
|
expect(tripwire.pageErrors).toEqual([])
|
|
|
|
|
expect(tripwire.warnings).toEqual([])
|
|
|
|
|
})
|
|
|
|
|
})
|