mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge remote-tracking branch 'origin/master' into worktree/web-theme-settings-integration-fde706
This commit is contained in:
@@ -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(' ')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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 => ({
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}}')
|
||||
|
||||
@@ -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) })
|
||||
|
||||
@@ -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' } } })
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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/')
|
||||
|
||||
Reference in New Issue
Block a user