Merge remote-tracking branch 'origin/master' into fix/remove-badge
This commit is contained in:
@@ -28,14 +28,14 @@ import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './suppor
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/live-interactions', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
// One golden pins the stable mid-turn loading state; the other three capture
|
||||
// what the user is left looking at after cancel, after a non-retryable failure
|
||||
// (pins the FIXME(web-error-surface) gap as a reviewable artifact: NO error
|
||||
// copy in the tree), and after retry recovery.
|
||||
// what the user is left looking at after cancel, after a non-retryable failure,
|
||||
// and after retry recovery.
|
||||
const CANCEL_EXPECTED = join(SNAPSHOT_DIR, 'cancel.expected.md')
|
||||
const LOADING_EXPECTED = join(SNAPSHOT_DIR, 'loading.expected.md')
|
||||
const ERROR_EXPECTED = join(SNAPSHOT_DIR, 'error-auth.expected.md')
|
||||
const RETRY_EXPECTED = join(SNAPSHOT_DIR, 'retry.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
const AUTH_PROVIDER_MESSAGE = 'Authentication Fails, Your api key: sk-preview-secret is invalid'
|
||||
|
||||
// The recorded base: one text-only turn whose derived script the sidecars
|
||||
// patch. Kept deliberately tool-free so the derived script is exactly one
|
||||
@@ -158,7 +158,7 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
|
||||
|
||||
it.skipIf(MODE === 'record')('surfaces a non-retryable AUTH failure without retrying', async () => {
|
||||
await launch(() => ({
|
||||
patches: [{ at: 0, entry: { kind: 'throw', chunks: [], message: 'invalid api key', code: 'AUTH' } }],
|
||||
patches: [{ at: 0, entry: { kind: 'throw', chunks: [], message: AUTH_PROVIDER_MESSAGE, code: 'AUTH' } }],
|
||||
}))
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-error-auth'))
|
||||
const { settled } = await sendPrompt()
|
||||
@@ -166,28 +166,28 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
|
||||
expect(turnEndReasons(sessionEvents).at(-1)).toBe('error')
|
||||
// AUTH is outside llm-retry's retryable set: no retry record.
|
||||
expect(sessionEvents.filter(e => e.type === 'llm/retry').length).toBe(0)
|
||||
// Product gap found by this lane, pinned as-is: the client consumes no
|
||||
// agent/error frames and a pre-chunk failure freezes no partial, so THIS
|
||||
// failure renders no error copy anywhere — the user sees the send simply
|
||||
// stop. FIXME(web-error-surface): assert visible error text here once the
|
||||
// web UI grows an error rendering; until then the pinned contract is
|
||||
// "no crash, composer recovers, turn logged as error".
|
||||
await expect.poll(() => page.locator('textarea').first().isEnabled(), { timeout: 10_000 }).toBe(true)
|
||||
expect(await page.locator('[data-streaming="true"]').count()).toBe(0)
|
||||
// The blank workspace also has an enabled composer. Wait for the driven
|
||||
// session's only visible message before capturing its no-error-copy state.
|
||||
await expect.poll(() => page.getByText(PROMPT, { exact: true }).first().isVisible(), { timeout: 10_000 }).toBe(true)
|
||||
// Golden of the same gap: the prompt bubble alone, no error copy in the
|
||||
// tree — the diff that changes when web-error-surface lands.
|
||||
const errorStatus = page.getByRole('status').filter({ hasText: 'This turn failed' })
|
||||
await errorStatus.waitFor({ timeout: 10_000 })
|
||||
expect(await errorStatus.textContent()).toContain('API key is invalid')
|
||||
expect(await errorStatus.textContent()).toContain('AUTH')
|
||||
expect(await page.locator('body').textContent()).not.toContain('sk-preview-secret')
|
||||
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd)
|
||||
await compareOrRefreshGolden(ERROR_EXPECTED, snapshot, MODE)
|
||||
await page.getByRole('tab', { name: 'Trajectory' }).click()
|
||||
const requestMarker = page.locator('tr[data-request-only="true"]').last()
|
||||
.getByRole('button', { name: /Request #/ })
|
||||
await requestMarker.click()
|
||||
await page.getByText('API key is invalid', { exact: true }).waitFor({ timeout: 10_000 })
|
||||
expect(await page.locator('body').textContent()).not.toContain('sk-preview-secret')
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 120_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps a terminal request marker inside the trajectory table', async () => {
|
||||
await launch(() => ({
|
||||
patches: [{ at: 0, entry: { kind: 'throw', chunks: [], message: 'invalid api key', code: 'AUTH' } }],
|
||||
patches: [{ at: 0, entry: { kind: 'throw', chunks: [], message: AUTH_PROVIDER_MESSAGE, code: 'AUTH' } }],
|
||||
}))
|
||||
const { settled } = await sendPrompt()
|
||||
await settled
|
||||
|
||||
83
apps/web/tests/shipped-composition.e2e.ts
Normal file
83
apps/web/tests/shipped-composition.e2e.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
// Boots the shipped Web composition over the built dist this lane already uses
|
||||
// and asserts what that composition produces: the model-visible tool catalog
|
||||
// and the sandbox/approval knobs it ships with. No browser and no model call —
|
||||
// these are composition facts, and the browser scenarios in this lane cover the
|
||||
// surface itself.
|
||||
import { tmpdir } from 'node:os'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox'
|
||||
// Empty type imports carry the tools/sandboxPolicy/approval Context merges.
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-sandbox-policy'
|
||||
import type {} from '@deepseek-ai/dsh-user-approval'
|
||||
import { launchWebScaffold, type WebScaffold } from './scaffold.ts'
|
||||
|
||||
/**
|
||||
* The catalog the shipped Web composition puts in front of the model, minus the
|
||||
* ripgrep-dependent pair below. The absences are deliberate, not incidental
|
||||
* gaps: the `cordis_*` toolset executes model-written JavaScript that no
|
||||
* sandbox row confines, `web_fetch` chooses its own request target, and
|
||||
* `mcp_*` servers spawn outside `ctx.bash`. The composition Agent Note owns the
|
||||
* rationale and its sources.
|
||||
*/
|
||||
const EXPECTED_TOOLS = [
|
||||
'ask_user_question',
|
||||
'bash',
|
||||
'create_goal',
|
||||
'edit',
|
||||
'exit_plan_mode',
|
||||
'get_goal',
|
||||
'ralph',
|
||||
'read',
|
||||
'session_event_read',
|
||||
'session_event_search',
|
||||
'session_event_trace',
|
||||
'session_search',
|
||||
'session_trace',
|
||||
'skill',
|
||||
'str_replace_editor',
|
||||
'subagent',
|
||||
'subagent_fork',
|
||||
'task_kill',
|
||||
'task_list',
|
||||
'task_output',
|
||||
'todo_write',
|
||||
'update_goal',
|
||||
'web_search',
|
||||
'workflow',
|
||||
'write',
|
||||
]
|
||||
|
||||
/**
|
||||
* `glob` and `grep` come from `dsh-tool-fs-search`, which probes `command -v rg`
|
||||
* through the mounted bash executor at load and registers neither tool when
|
||||
* ripgrep is absent. That is a host dependency, not a composition decision, so the
|
||||
* pair is asserted separately — present together or absent together.
|
||||
*/
|
||||
const RIPGREP_TOOLS = ['glob', 'grep']
|
||||
|
||||
let scaffold: WebScaffold | undefined
|
||||
|
||||
afterEach(async () => {
|
||||
await scaffold?.close()
|
||||
scaffold = undefined
|
||||
})
|
||||
|
||||
it('assembles the shipped Web catalog and keeps its access default', async () => {
|
||||
scaffold = await launchWebScaffold()
|
||||
const names = scaffold.ctx.tools.schemas().map(schema => schema.name).sort()
|
||||
expect(names.filter(name => !RIPGREP_TOOLS.includes(name))).toEqual(EXPECTED_TOOLS)
|
||||
expect([[], RIPGREP_TOOLS]).toContainEqual(names.filter(name => RIPGREP_TOOLS.includes(name)))
|
||||
// `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
|
||||
// "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())]),
|
||||
)
|
||||
// The Web surface keeps its shipped access default; the base's confined one
|
||||
// reaches the TUI. Pinning both keeps a base change from moving Web silently.
|
||||
expect(scaffold.ctx.sandboxPolicy.defaultMode).toBe('danger-full-access')
|
||||
expect(scaffold.ctx.approval.config.policy).toBe('never')
|
||||
}, 120_000)
|
||||
@@ -13,6 +13,9 @@
|
||||
- img
|
||||
- img
|
||||
- text: Context injection
|
||||
- status:
|
||||
- text: This turn failedAPI key is invalid
|
||||
- code: AUTH
|
||||
- textbox "Message the agent"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"tests/skill-invocation-policy.e2e.ts",
|
||||
"tests/permission-policy-context.e2e.ts",
|
||||
"tests/access-confirmation.e2e.ts",
|
||||
"tests/shipped-composition.e2e.ts",
|
||||
"tests/startup-auto-selection.e2e.ts"
|
||||
],
|
||||
"references": [
|
||||
|
||||
Reference in New Issue
Block a user