Merge remote-tracking branch 'origin/master' into worktree/web-theme-settings-integration-fde706

This commit is contained in:
Yichen Jiang
2026-08-10 17:05:18 +08:00
488 changed files with 1747 additions and 1433 deletions

View File

@@ -35,9 +35,9 @@ const UI_EXPECTED = join(SNAPSHOT_DIR, 'ui.expected.md')
const MODE = webSnapshotMode()
// Irreducible payload: the command has to be long enough to pass the card's
// height cap, which is the only shape that reproduces an action row pushed off
// screen. Unrelated tokens, not a repeated word — the model compresses a
// repeated word into `printf 'alpha %.0s' {1..400}` when recording, and a
// height cap, which is the only command length that reproduces an action row pushed off
// screen. Unrelated tokens, not a repeated word — a repeated word is what the
// model compressed into `printf 'alpha %.0s' {1..400}` while recording, and a
// short command proves nothing here. The formula keeps the source small; the
// model receives the expanded literal it has to put in the command.
const TOKENS = Array.from({ length: 220 }, (_, index) => `tok${((index + 1) * 7919 % 99991).toString(36)}`).join(' ')

View File

@@ -186,7 +186,7 @@ describe('web e2e: long Chat interaction contract', () => {
const boundary = source.session.events.find((event): event is SessionEvent<'turn/end'> => (
event.type === 'turn/end' && event.data.turn === BRANCH_TURN
))
if (boundary === undefined) throw new Error(`turn ${String(BRANCH_TURN)} has no completed boundary`)
if (boundary === undefined) throw new Error(`turn ${String(BRANCH_TURN)} has no turn/end event`)
const expectedUserText = textContent(branchUserEvent.data.content)
await wheelUntilMounted(page, `[data-chat-call-id="${TARGET_CALL_2}"]`, -1_100)

View File

@@ -1,7 +1,7 @@
// Opt-in browser benchmark for high-cardinality workspace and history
// rendering. It reports measurements without timing assertions because host
// speed is not a correctness contract; structural assertions keep the load
// shape from silently shrinking.
// speed is not a correctness contract; structural assertions keep the number
// of workspaces and history entries from silently shrinking.
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'

View File

@@ -64,12 +64,12 @@ const DRAFT = Array.from({ length: DRAFT_LINES }, (_unused, index) => {
}).join('\n')
/**
* A draft ending in a newline: the shape where the two layers reserve their
* A draft ending in a newline, where the two layers reserve their
* final line box on different terms. A textarea keeps one for the caret after a
* final newline; `white-space: pre-wrap` collapses a text node's trailing
* newline and generates none. The hidden auto-grow mirror carries the newline
* and so decides the height for both, which is why the backdrop needs no
* padding of its own — but only a draft of this shape can show it.
* padding of its own — but only a draft with a trailing newline can show it.
*/
const DRAFT_TRAILING_NEWLINE = `${DRAFT}\n`
@@ -381,7 +381,7 @@ describe('web e2e: composer draft scrolling', () => {
const data = new DataTransfer()
data.setData('text/plain', text)
el.dispatchEvent(new ClipboardEvent('paste', { clipboardData: data, bubbles: true, cancelable: true }))
// Ending in a newline is the shape the engines disagree on: the caret
// The engines disagree when the draft ends in a newline: the caret
// lands on a line with nothing on it, where chromium reports no client
// rects at all for the collapsed position.
}, `\n${DRAFT}\n`)
@@ -401,7 +401,7 @@ describe('web e2e: composer draft scrolling', () => {
it('a draft ending in a newline scrolls to its true end, not a line above it', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-composer-draft-scroll-trailing-newline'))
// The layers reserve a final line box on different terms, so this shape is
// The layers reserve a final line box on different terms, so the trailing-newline case is
// the one that separates a height every layer agrees on from a box measured
// one line short of the caret's own last position.
const input = page.locator('textarea:enabled').first()
@@ -453,7 +453,7 @@ describe('web e2e: composer draft scrolling', () => {
const data = new DataTransfer()
data.setData('text/plain', text)
el.dispatchEvent(new ClipboardEvent('paste', { clipboardData: data, bubbles: true, cancelable: true }))
// The ordinary shape — not ending in a newline so the collapsed branch
// The ordinary case, without a trailing newline, so the collapsed branch
// of the reveal keeps a real engine under it; the case above owns the
// after-newline branch.
}, `\n${DRAFT}`)

View File

@@ -42,7 +42,7 @@ function appFrame(page: Page) {
return page.locator('[style*="grid-template-columns"]').first()
}
/** Render the two boundary affordances without platform-dependent coordinates. */
/** Render the two column-resize handles without platform-dependent coordinates. */
async function handleSnapshot(page: Page): Promise<string> {
const handles = await page.locator('[class*="handle"]').evaluateAll(elements =>
elements.map(element => ({

View File

@@ -164,8 +164,8 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
sessionId = await settled
}
await recordFixture(scaffold, sessionId!, SEED)
// Fixture honesty: the recording must carry the shape the replay
// scenarios assert on three calls in turn 1 and two closed turns.
// Fixture honesty: the recording must contain the events the replay
// scenarios assert on: three calls in turn 1 and two closed turns.
const recorded = parseSessionLog(await readFile(SEED, 'utf8'))
expect(recorded.filter(e => e.type === 'turn/end')).toHaveLength(2)
const calls = recorded.filter((e): e is SessionEvent & { data: { name: string } } => e.type === 'tool/call')

View File

@@ -1,7 +1,7 @@
// Keyless browser regression for pwsh UI parity with bash: a seeded session
// whose pwsh call/result is presented by the REAL tool-pwsh on replay (the
// api-proxy recomputes presentation views from logged args/result content)
// must render as a bash-shaped terminal card with the parsed exit-status
// must render with the same terminal card layout as bash and show the parsed exit-status
// pill — not a generic console-fenced card. The seed is authored, not
// recorded: its header line carries no `cwd`
// field (seedSession writes the session cwd itself, and a Windows temp path
@@ -43,7 +43,7 @@ const HAS_PWSH = MODE === 'record' ? false : spawnSync(
{ encoding: 'utf8' },
).status === 0
describe.skipIf(MODE === 'record' || !HAS_PWSH)('web e2e: pwsh calls render as bash-shaped terminal cards', () => {
describe.skipIf(MODE === 'record' || !HAS_PWSH)('web e2e: pwsh calls use the bash terminal-card layout', () => {
let scaffold: WebScaffold
let browser: Browser
let page: Page
@@ -75,7 +75,7 @@ describe.skipIf(MODE === 'record' || !HAS_PWSH)('web e2e: pwsh calls render as b
await expect.poll(() => result.count(), { timeout: 15_000 }).toBe(1)
await result.click()
await page.getByRole('tab', { name: 'Chat', exact: true }).waitFor({ timeout: 15_000 })
// The tool row is expand-gated: the settled bash-shaped row carries the
// The tool row is expand-gated: the settled row uses the bash layout and carries the
// shell-family variant, and the terminal card lives in the expanded body.
const row = page.locator('[data-tool="pwsh"]').first()
await row.waitFor({ timeout: 15_000 })

View File

@@ -31,7 +31,7 @@ const ANSWERED_EXPECTED = join(SNAPSHOT_DIR, 'answered.expected.md')
const MODE = webSnapshotMode()
// The options carry long descriptions on purpose: the squeeze assertion below
// needs option copy that WRAPS, which is the only shape that reproduces a
// needs option copy that WRAPS, which is the only text layout that reproduces a
// collapsed row painting its copy outside its own box.
const PROMPT = 'Use the ask_user_question tool to ask me exactly one multi-select question with id "color", question "Which color do you prefer?", header "Pick one", and two options: label "Blue" with description "A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.", and label "Green" with description "A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions." Set multi_select to true. After I answer, reply with the single word DONE and stop.'
@@ -116,7 +116,7 @@ describe('web e2e: resident question composer round trip', () => {
return {
rows: rows.length,
spill: Math.max(...spill),
// Wrapped copy is the shape that overflows a collapsed row, and a
// Wrapped option text is what overflows a collapsed row, and a
// scrolling list proves the seat is genuinely capped. Without both,
// the spill assertion would hold vacuously.
wrappedRows: rows.filter(row => row.getBoundingClientRect().height > 42).length,

View File

@@ -448,7 +448,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
let replayHandle: ReplayHandle | undefined
try {
process.chdir(workspaceCwd)
// The production resolution shape: an empty profile root inside the temp
// The production module-resolution setup: an empty profile root inside the temp
// harness home, with bare plugin names resolving through the flat module
// fallback the launcher heals under <home>/profiles.
healProfilesModuleFallback(INSTALL_ANCHOR, harnessHome)
@@ -626,7 +626,7 @@ export function fixtureUserPrompts(fixtureText: string): string[] {
* through the REAL backend API (throwaway Context + SessionStore + JSONL
* plugin — the semantic-checkpoint precedent), never raw file writes: no
* knowledge of bucket hashing, filename encoding, or compression, and
* malformed shapes fail loud at seed time. The fixture's tokenized identity
* malformed session events fail loud at seed time. The fixture's tokenized identity
* ({{sessionId}}/{{cwd}}) is realized for this world before parsing.
* @param scaffold - the target scaffold.
* @param fixtureText - raw recorded session.jsonl contents.
@@ -730,7 +730,7 @@ function normalizeAria(snapshot: string, workspaceCwd: string): string {
// between local worktrees and CI scratch directories.
.replace(/(Compacted \d+ history items \(~)\d+( tokens\))/g, '$1{{tokens}}$2')
// Message IconActions clocks widen by calendar day/year; collapse every
// shape so goldens stay stable across midnight and year boundaries.
// format so goldens stay stable across midnight and year changes.
.replace(/\d{4}年\d{1,2}月\d{1,2}日 \d{2}:\d{2}/g, '{{clock}}')
.replace(/\d{1,2}月\d{1,2}日 \d{2}:\d{2}/g, '{{clock}}')
.replace(/(?<!\d)\d{1,2}:\d{2}:\d{2}(?:\.\d+)?(?:\s*[AP]M)?(?!\d)/gi, '{{clock}}')

View File

@@ -4,7 +4,7 @@
// FixtureApiClient transport (no API key, no model round), opens the fixture
// session, and pins the search card the `grep` turn (fixture turn 67) renders in
// the assembled application. The built-boot smoke proves the graph boots but
// carries no behavior assertions by contract; this is the assembled-output check
// intentionally carries no behavior assertions; this is the assembled-output check
// that a broken SearchRow registration or a dropped card would fail — the
// per-package suites bench over src and cannot see the bundled wiring.
//
@@ -13,7 +13,7 @@
// fixture, not harvested from a live model. The recovery-footer arm is a pure
// derivation over the result view, pinned at every render site by the
// ui-conversation suite; here the fixture turn exercises the assembled card
// shape and its cap.
// fields and its cap.
import { mkdirSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { act, fireEvent, screen, waitFor, within } from '@testing-library/react'
@@ -24,7 +24,7 @@ const EXPECTED = join(process.cwd(), 'apps/web/tests/snapshots/search-card/grep-
installAssembledBootEnv()
/** Normalize a rendered search card to a stable text shape: the kind, the banner
/** Normalize a rendered search card to stable text fields: the kind, the banner
* summary, each file header (path + count), each visible match line, the expand
* control label, and the recovery footer. */
function cardShape(root: Element): string {
@@ -63,7 +63,7 @@ describe('assembled search card', () => {
}, { timeout: 10_000 })
// `data-tool` sits on the ToolRow root; the collapsed row is the expand
// toggle. Click it so the card and its recovery footer mount, then shape the
// toggle. Click it so the card and its recovery footer mount, then serialize the
// whole row (the card lives inside ToolRow's body wrapper).
const grepRow = document.querySelector('[data-tool="grep"]')!
act(() => { fireEvent.click(grepRow.querySelector('[data-expandable]') ?? grepRow) })

View File

@@ -102,7 +102,7 @@ function withCompaction(raw: string, meter: TokenMeterService): string {
})
// Load-bearing exactness: the projections subtract this count verbatim, so
// it must equal what the host's fold prices for these nodes. The estimator
// prices message CONTENT only, so a minimal wrapper per storage shape is
// prices message CONTENT only, so a minimal wrapper for each stored event format is
// exact — pre-identity rows carry bare `content` (the persistence read path
// upgrades them), a current row carries the full `message` envelope.
const priceRow = (row: (typeof events)[number]): number => {
@@ -169,7 +169,7 @@ function withCompaction(raw: string, meter: TokenMeterService): string {
},
})
// The persistence seed helper requires a terminal turn/end. Keep the manual
// command standalone, then add a closed zero-step fixture boundary after it.
// command standalone, then add a closed zero-step turn after it.
const closureTurn = lastTurn + 1
at({ type: 'turn/start', data: { turn: closureTurn } })
at({ type: 'turn/end', data: { turn: closureTurn, reason: { kind: 'completed' } } })

View File

@@ -5,6 +5,7 @@
// surface itself.
import { tmpdir } from 'node:os'
import { afterEach, expect, it } from 'vitest'
import { CallId } from '@deepseek-ai/dsh-llm'
import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox'
import { SessionId } from '@deepseek-ai/dsh-session'
// Empty type imports carry the tools/sandboxPolicy/approval Context merges.
@@ -90,7 +91,7 @@ it('assembles the shipped Web catalog with the confined access default', async (
// `workspace-write` is not "the workspace and nothing else": the shared roots
// helper always admits the temp directories too. Pinning it against an
// explicit mode keeps the claim independent of this surface's default, and
// keeps a future boundary test from being run inside /tmp — where an
// keeps a future sandbox-confinement test from being run inside /tmp — where an
// "escape" write succeeds by design and reads as a sandbox failure.
expect(writableRoots(scaffold.ctx.sandboxPolicy.resolve({ mode: 'workspace-write' }))).toEqual(
expect.arrayContaining([canonicalPath('/tmp'), canonicalPath(tmpdir())]),
@@ -114,3 +115,64 @@ it('assembles the shipped Web catalog with the confined access default', async (
await commandHandle.dispose()
}
}, 120_000)
it('lets a preset producer reach the background-task registry', async () => {
scaffold = await launchWebScaffold()
const ctx = scaffold.ctx
const handle = await ctx.agents.create({
sessionId: SessionId('shipped-background-task'),
meta: { cwd: scaffold.workspaceCwd },
setup: agentCtx => ctx.agentPresets.mount(agentCtx).then(() => undefined),
})
try {
const signal = new AbortController().signal
// `tool-bash` is a preset row and `tasks` is a host registry; the producer
// resolves it with `ctx.get`, so a registry hidden behind a preset realm
// fails here — with every task control still listed in the catalog above.
const started = await ctx.tools.execute({
signal,
callId: CallId('shipped-bash-background'),
name: 'bash',
arguments: {
command: 'printf SHIPPED_BACKGROUND_OK',
description: 'shipped background probe',
run_in_background: true,
},
agent: handle.agent,
})
expect({ isError: started.isError, content: started.content }).toEqual({
isError: false,
content: [{ type: 'text', text: 'started background task bash-1' }],
})
// The control surface reads what the producer started: same registry, one
// owner. A per-preset registry would list nothing here even on success.
const listed = await ctx.tools.execute({
signal,
callId: CallId('shipped-task-list'),
name: 'task_list',
arguments: {},
agent: handle.agent,
})
expect(listed.isError).toBe(false)
expect(listed.content).toEqual([
{ type: 'text', text: expect.stringContaining('bash-1 [bash]') as unknown as string },
])
// The full round trip: the output a host-plane producer wrote is collected
// through a preset-plane control, which is the linkage the realm severed.
const collected = await ctx.tools.execute({
signal,
callId: CallId('shipped-task-output'),
name: 'task_output',
arguments: { task_id: 'bash-1', wait: true },
agent: handle.agent,
})
expect(collected.isError).toBe(false)
expect(collected.content).toEqual([
{ type: 'text', text: expect.stringContaining('SHIPPED_BACKGROUND_OK') as unknown as string },
])
} finally {
await handle.dispose()
}
}, 120_000)

View File

@@ -17,11 +17,10 @@
// replacing those nodes.
//
// The round-trip against a loopback host is far too fast to observe, so this
// scenario HOLDS the `session.history` response open at the browser's network
// boundary and asserts the visible frame while it is in flight. That gate is
// what makes the assertions non-vacuous: without the phase exemption, the
// held window is exactly when `settling` would be painted and the composer
// hidden.
// scenario HOLDS the `session.history` response open in the browser's network
// handler and asserts the visible frame while it is in flight. That wait is
// what makes the assertions non-vacuous: without the phase exemption, the held
// window is exactly when `settling` would be painted and the composer hidden.
//
// Zero model calls: registering a workspace and opening its blank session are
// host RPCs with no model involvement. A stray stream would fail loud with

View File

@@ -20,7 +20,7 @@ const EXPECTED = join(process.cwd(), 'apps/web/tests/snapshots/todo-row/parallel
installAssembledBootEnv()
/** Normalize the todo row and the plan strip to a stable text shape: the row's
/** Normalize the todo row and the plan strip to stable text fields: the row's
* title, its truncatable summary, its non-shrinking suffix, then the panel's
* per-status header and every list item with its status. */
function todoShape(row: Element, panel: Element): string {

View File

@@ -1,7 +1,7 @@
// Web e2e scenario: assistant IconActions belong to the settled answer, so
// they arrive with `turn/end` and not before. The recorded turn narrates in
// plain text before its tool call, which is the shape that would hand the
// footer to mid-turn narration for the seconds a tool runs and then move it
// plain text before its tool call, which is the event order that would show the
// footer beside mid-turn narration for the seconds a tool runs and then move it
// down. A `hang` sidecar on the SECOND model call parks the turn after the
// narration and the tool result are durable, so the running state is stable by
// construction rather than by timing; stopping from that park writes the

View File

@@ -31,8 +31,8 @@ const SEED = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', impo
const MODE = webSnapshotMode()
const BROWSER_EXPECTED = join(SNAPSHOT_DIR, 'directory-browser.expected.md')
const SEED_ID = 'workspace-management-web-e2e'
// Both waits exceed ui-primitives' 200ms POINTER_GRACE_MS. Keep them coupled
// to that contract if the shared grace tuning changes.
// Both waits exceed ui-primitives' 200ms POINTER_GRACE_MS. Keep them above
// that value if the shared setting changes.
const POINTER_TRANSIT_MS = 300
const POINTER_HOLD_MS = 600

View File

@@ -31,7 +31,7 @@ function rejectStandaloneServe(): Plugin {
* editing shell code re-hashes only index and returning clients keep the
* cached vendor chunk.
*
* Boundary invariant: every member must be react-free. A package that
* Every member must be React-free. A package that
* imports react/jsx-runtime must never be listed — rollup folds a module
* shared between the entry and a manual chunk into the manual chunk, so one
* react-importing member would drag the single shared react copy into
@@ -77,8 +77,8 @@ const BOOT_GRAMMAR_FILES: readonly string[] = [
const FONT_EXTENSIONS: readonly string[] = ['.woff2', '.woff', '.ttf']
/**
* npm package name of a resolved module id (the segment after the LAST
* `node_modules/` pnpm nests the real package under an inner node_modules).
* npm package name of a resolved module id: the segment after the last
* `node_modules/`. pnpm nests the real package under an inner node_modules.
*/
function npmPackageOf(id: string): string | undefined {
const parts = id.split('/node_modules/')