Merge branch 'master' into fix/web-favicon-dark-mode
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-frontend",
|
||||
"description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "apps/web"
|
||||
},
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./dist/*": "./dist/*",
|
||||
@@ -23,11 +30,12 @@
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cordisjs/plugin-group": "workspace:^",
|
||||
"@deepseek-ai/cordis-plugin-group": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-modules": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-web-react": "workspace:^",
|
||||
"@deepseek-ai/dsh-cmdline": "workspace:^",
|
||||
"@deepseek-ai/dsh-pwsh-local": "workspace:^",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "~18.3.1",
|
||||
|
||||
@@ -161,7 +161,7 @@ describe('web e2e: agent-preset authoring is a host-side copy', () => {
|
||||
expect(composition).toBe(await readFile(join(SHIPPED_PRESETS, 'minimal', 'agent.cordis.yml'), 'utf8'))
|
||||
const metadata = await readFile(join(userRoot, 'my-agent', 'preset.yml'), 'utf8')
|
||||
expect(metadata).toContain('name: 我的模式')
|
||||
expect(metadata).toContain('description: 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。')
|
||||
expect(metadata).toContain('description: 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。')
|
||||
expect(metadata).not.toContain('order:')
|
||||
}, 60_000)
|
||||
|
||||
@@ -176,8 +176,10 @@ describe('web e2e: agent-preset authoring is a host-side copy', () => {
|
||||
|
||||
await expect.poll(async () => dialog.getByText('我的模式').count(), { timeout: 10_000 }).toBe(0)
|
||||
expect(existsSync(join(userRoot, 'my-agent'))).toBe(false)
|
||||
// Custom group gone with its only member; the shipped set stands.
|
||||
expect(await dialog.getByRole('heading', { name: '自定义' }).count()).toBe(0)
|
||||
// The custom group outlives its only member: the heading stays with the
|
||||
// creator entry so the place to author a preset never disappears.
|
||||
expect(await dialog.getByRole('heading', { name: '自定义' }).count()).toBe(1)
|
||||
expect(await dialog.getByRole('button', { name: '用「创造模式」创作自定义预设' }).count()).toBe(1)
|
||||
expect(await dialog.getByText('标准模式').count()).toBeGreaterThan(0)
|
||||
}, 60_000)
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ 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 {
|
||||
SESSION_FORMAT_VERSION, SessionId as sessionId, type SessionEvent, type SessionId,
|
||||
} from '@deepseek-ai/dsh-session'
|
||||
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
|
||||
import {
|
||||
captureStableAria, compareOrRefreshGolden, launchWebScaffold, seedSession, watchConsole,
|
||||
webSnapshotMode, type WebScaffold,
|
||||
@@ -78,6 +82,60 @@ function seedLog(): string {
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* The preset the host reports for the blank session the workspace connect
|
||||
* produced. Addressed by id rather than by scanning the serialized list: the
|
||||
@@ -120,7 +178,8 @@ describe('web e2e: agent-preset selection', () => {
|
||||
// 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')
|
||||
const seededId = await seedSession(scaffold, seedLog(), SEED_ID, 'minimal')
|
||||
await seedSubagent(scaffold, seededId)
|
||||
await seedWorkspaceSkill(scaffold.workspaceCwd)
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
@@ -143,12 +202,12 @@ describe('web e2e: agent-preset selection', () => {
|
||||
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('标准模式')
|
||||
expect(snapshot).toContain('Standard mode')
|
||||
})
|
||||
|
||||
it('names every preset and what it is for', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-agent-preset-menu'))
|
||||
await page.getByRole('button', { name: '标准模式' }).click()
|
||||
await page.getByRole('button', { name: 'Standard mode' }).click()
|
||||
const menu = page.getByRole('menu')
|
||||
await menu.waitFor({ timeout: 10_000 })
|
||||
|
||||
@@ -157,15 +216,15 @@ describe('web e2e: agent-preset selection', () => {
|
||||
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('创造模式')
|
||||
expect(snapshot).toContain('Minimal mode')
|
||||
expect(snapshot).toContain('Creator mode')
|
||||
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()
|
||||
await page.getByRole('button', { name: 'Standard mode' }).click()
|
||||
await page.getByRole('menuitem', { name: /Minimal mode/ }).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.
|
||||
@@ -197,8 +256,8 @@ describe('web e2e: agent-preset selection', () => {
|
||||
// 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.
|
||||
await page.getByRole('button', { name: '极简模式' }).click()
|
||||
await page.getByRole('menuitem', { name: /^标准模式/ }).first().click()
|
||||
await page.getByRole('button', { name: 'Minimal mode' }).click()
|
||||
await page.getByRole('menuitem', { name: /^Standard mode/ }).first().click()
|
||||
await expect.poll(() => livePreset(scaffold.baseUrl), { timeout: 15_000 }).toBe('standard')
|
||||
|
||||
await composer.fill('/')
|
||||
@@ -221,10 +280,12 @@ describe('web e2e: agent-preset selection', () => {
|
||||
const snapshot = await captureStableAria(page, '[class*="titleRow"]', scaffold.workspaceCwd)
|
||||
|
||||
await compareOrRefreshGolden(HEADER_EXPECTED, snapshot, MODE)
|
||||
expect(snapshot).toContain('极简模式')
|
||||
expect(snapshot).toContain('Minimal mode')
|
||||
expect(snapshot).toContain('button "1 subagent"')
|
||||
expect(snapshot.indexOf('Minimal mode')).toBeLessThan(snapshot.indexOf('button "1 subagent"'))
|
||||
// Static chrome, not a control: the header can only report a composition
|
||||
// the host would refuse to change.
|
||||
expect(snapshot).not.toContain('button "极简模式"')
|
||||
expect(snapshot).not.toContain('button "Minimal mode"')
|
||||
})
|
||||
|
||||
it('drove every surface without a page error or a stream warning', () => {
|
||||
|
||||
@@ -70,7 +70,12 @@ let unmount: (() => void) | undefined
|
||||
export function installAssembledBootEnv(): void {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
localStorage.setItem('dsh.locale', 'en')
|
||||
// The locale service derives its provisional locale from the browser and
|
||||
// takes an explicit choice only from Host settings, which this lane's
|
||||
// fixture transport does not serve; pinning the navigator is what selects
|
||||
// English here.
|
||||
Object.defineProperty(navigator, 'languages', { value: ['en-US'], configurable: true })
|
||||
Object.defineProperty(navigator, 'language', { value: 'en-US', configurable: true })
|
||||
document.title = 'DeepSeek Harness'
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
|
||||
@@ -88,6 +93,11 @@ export function installAssembledBootEnv(): void {
|
||||
document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
|
||||
document.title = ''
|
||||
history.replaceState(null, '', '/')
|
||||
// Deleting the own properties uncovers jsdom's own accessors again
|
||||
// (Navigator declares both readonly, hence the erased receiver).
|
||||
const ownNavigator = navigator as unknown as Record<string, unknown>
|
||||
delete ownNavigator.languages
|
||||
delete ownNavigator.language
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import type { AgentHandle } from '@deepseek-ai/dsh-agent'
|
||||
import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { assertFixtureInventory, launchWebScaffold, type WebScaffold } from './scaffold.ts'
|
||||
|
||||
const CORE_WEB_OVERLAY = fileURLToPath(new URL('../../cli/config/core-web.cordis.yml', import.meta.url))
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/core-web-profile', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
const PROMPT = 'Reply exactly CORE_WEB_REQUEST_OK and stop.'
|
||||
|
||||
describe('core Web profile', () => {
|
||||
let scaffold: WebScaffold
|
||||
let agentHandle: AgentHandle
|
||||
|
||||
beforeAll(async () => {
|
||||
const systemPrompt = process.env.DSH_SYSTEM_PROMPT
|
||||
Reflect.deleteProperty(process.env, 'DSH_SYSTEM_PROMPT')
|
||||
try {
|
||||
scaffold = await launchWebScaffold({ extraOverlayPath: CORE_WEB_OVERLAY, replayFixture: FIXTURE })
|
||||
} finally {
|
||||
if (systemPrompt !== undefined) process.env.DSH_SYSTEM_PROMPT = systemPrompt
|
||||
}
|
||||
agentHandle = await scaffold.ctx.agents.create({
|
||||
sessionId: SessionId('core-web-profile-smoke'),
|
||||
meta: { cwd: scaffold.workspaceCwd },
|
||||
agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const failures: unknown[] = []
|
||||
await agentHandle?.dispose().catch((error: unknown) => failures.push(error))
|
||||
await scaffold?.close().catch((error: unknown) => failures.push(error))
|
||||
if (failures.length === 1) throw failures[0]
|
||||
if (failures.length > 1) throw new AggregateError(failures, 'core Web profile smoke teardown failed')
|
||||
})
|
||||
|
||||
it('sends the RL prompt and tool schemas through a real request, then executes both tools', async () => {
|
||||
agentHandle.agent.followup(createUserMessage({
|
||||
content: [{ type: 'text', text: PROMPT }],
|
||||
source: { kind: 'user' },
|
||||
}))
|
||||
await agentHandle.agent.whenIdle()
|
||||
|
||||
const requestHeader = agentHandle.agent.session.requestHeader()
|
||||
if (requestHeader === undefined) throw new Error('the core Web agent issued no model request')
|
||||
|
||||
const seedPath = join(scaffold.workspaceCwd, 'profile-smoke.txt')
|
||||
await writeFile(seedPath, 'CORE_WEB_EDITOR_OK\n')
|
||||
const signal = new AbortController().signal
|
||||
const bash = await scaffold.ctx.tools.execute({
|
||||
signal,
|
||||
callId: CallId('core-web-bash-smoke'),
|
||||
name: 'bash',
|
||||
arguments: { command: "printf 'CORE_WEB_BASH_OK\\n'" },
|
||||
agent: agentHandle.agent,
|
||||
})
|
||||
const editor = await scaffold.ctx.tools.execute({
|
||||
signal,
|
||||
callId: CallId('core-web-editor-smoke'),
|
||||
name: 'str_replace_editor',
|
||||
arguments: { command: 'view', path: seedPath },
|
||||
agent: agentHandle.agent,
|
||||
})
|
||||
|
||||
const text = (result: typeof bash): string => result.content
|
||||
.filter(block => block.type === 'text')
|
||||
.map(block => block.text)
|
||||
.join('')
|
||||
.replaceAll(scaffold.workspaceCwd, '{{cwd}}')
|
||||
.trimEnd()
|
||||
|
||||
expect({
|
||||
prompt: requestHeader.system,
|
||||
tools: requestHeader.tools?.map(tool => tool.name),
|
||||
bash: text(bash),
|
||||
editor: text(editor),
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"bash": "CORE_WEB_BASH_OK",
|
||||
"editor": "Here's the content of {{cwd}}/profile-smoke.txt with line numbers (which has a total of 2 lines):
|
||||
1 CORE_WEB_EDITOR_OK
|
||||
2",
|
||||
"prompt": "You are a helpful software engineer assistant.",
|
||||
"tools": [
|
||||
"bash",
|
||||
"str_replace_editor",
|
||||
],
|
||||
}
|
||||
`)
|
||||
expect(requestHeader.tools).toEqual(scaffold.ctx.tools.schemas(agentHandle.agent))
|
||||
|
||||
const entries = [...scaffold.ctx.loader.entries()]
|
||||
expect(entries.find(entry => entry.options.id === 'persistent-bash')?.fiber).toBeDefined()
|
||||
expect(entries.find(entry => entry.options.id === 'pty-local')?.fiber).toBeDefined()
|
||||
expect(entries.find(entry => entry.options.id === 'str-replace-editor')?.fiber).toBeDefined()
|
||||
expect(entries.find(entry => entry.options.id === 'web-runtime')?.fiber).toBeDefined()
|
||||
expect(entries.find(entry => entry.options.id === 'workspace-context')?.fiber).toBeUndefined()
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl'])
|
||||
})
|
||||
|
||||
it('uses DSH_SYSTEM_PROMPT as the complete prompt when configured', async () => {
|
||||
const previous = process.env.DSH_SYSTEM_PROMPT
|
||||
process.env.DSH_SYSTEM_PROMPT = 'RL prompt override'
|
||||
let overrideScaffold: WebScaffold | undefined
|
||||
let overrideAgent: AgentHandle | undefined
|
||||
try {
|
||||
overrideScaffold = await launchWebScaffold({ extraOverlayPath: CORE_WEB_OVERLAY, replayFixture: FIXTURE })
|
||||
overrideAgent = await overrideScaffold.ctx.agents.create({
|
||||
sessionId: SessionId('core-web-profile-override'),
|
||||
meta: { cwd: overrideScaffold.workspaceCwd },
|
||||
agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
})
|
||||
overrideAgent.agent.followup(createUserMessage({
|
||||
content: [{ type: 'text', text: PROMPT }],
|
||||
source: { kind: 'user' },
|
||||
}))
|
||||
await overrideAgent.agent.whenIdle()
|
||||
expect(overrideAgent.agent.session.requestHeader()?.system).toBe('RL prompt override')
|
||||
} finally {
|
||||
try {
|
||||
await overrideAgent?.dispose()
|
||||
} finally {
|
||||
try {
|
||||
await overrideScaffold?.close()
|
||||
} finally {
|
||||
if (previous === undefined) Reflect.deleteProperty(process.env, 'DSH_SYSTEM_PROMPT')
|
||||
else process.env.DSH_SYSTEM_PROMPT = previous
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
101
apps/web/tests/feedback-command.e2e.ts
Normal file
101
apps/web/tests/feedback-command.e2e.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
// Keyless assembled-browser coverage for the /feedback command over the
|
||||
// shipped Web bundles and the real host wire. The command plane settles
|
||||
// without a model turn: the host appends the log-only command/run +
|
||||
// feedback/record + command/done lifecycle, and the transcript renders the
|
||||
// acknowledgement — the recorded session id plus the session-sharing
|
||||
// disclosure — as a persistent command row. The scaffold mounts the shipped
|
||||
// telemetry row in FULL mode against a local dead endpoint (no record leaves
|
||||
// the process), so the golden pins the shipped default sentence
|
||||
// `Session sharing is enabled.`; the per-status sentences are pinned by the
|
||||
// package and OTel unit tests.
|
||||
import { readFile } from 'node:fs/promises'
|
||||
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 {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/feedback-command', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
const ACK_EXPECTED = join(SNAPSHOT_DIR, 'ack.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
// Discard port: loopback listener never binds, so FULL telemetry discloses
|
||||
// the shipped default policy without any record reaching a collector.
|
||||
const TELEMETRY_URL = 'http://127.0.0.1:9/v1/logs'
|
||||
|
||||
const PROMPT = 'Reply with the single word LIGHTHOUSE and stop.'
|
||||
|
||||
describe('web e2e: /feedback command acknowledgement', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({
|
||||
telemetryUrl: TELEMETRY_URL,
|
||||
...(MODE === 'record' ? {} : { replayFixture: FIXTURE }),
|
||||
})
|
||||
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 })
|
||||
// Fresh world: connecting a workspace births the blank session whose
|
||||
// live composer accepts the slash line.
|
||||
await connectFreshWorkspace(page, scaffold.workspaceCwd)
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('drives the recorded prompt to a settled turn (all modes)', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-feedback-drive'))
|
||||
if (MODE !== 'record') {
|
||||
// Drift guard: the committed fixture must carry exactly the drive prompt.
|
||||
expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([PROMPT])
|
||||
}
|
||||
const input = page.locator('textarea').first()
|
||||
await input.waitFor({ timeout: 10_000 })
|
||||
// Arm the turn-boundary waiter BEFORE sending, so a burst replay cannot
|
||||
// miss the turn/end that settles the recorded turn.
|
||||
const settled = scaffold.whenTurnSettled()
|
||||
await input.fill(PROMPT)
|
||||
await input.press('Enter')
|
||||
const sessionId = await settled
|
||||
if (MODE === 'record') {
|
||||
await recordFixture(scaffold, sessionId, FIXTURE)
|
||||
}
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('records feedback and renders the acknowledgement with session id and sharing status', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-feedback-command'))
|
||||
// The drive test settled the recorded turn: the transcript is active (a
|
||||
// command row does not render while a fresh session is still blank) and
|
||||
// the replayed reply is on screen.
|
||||
await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
|
||||
const input = page.locator('textarea').first()
|
||||
await input.fill('/feedback the diff view is unreadable')
|
||||
await input.press('Enter')
|
||||
// The command plane settles without a model turn: the ack row names the
|
||||
// recorded session and the mounted FULL backend's disclosure.
|
||||
await page.getByText(/Feedback recorded for session/).waitFor({ timeout: 10_000 })
|
||||
expect(await page.getByText(/Session sharing is enabled/).count()).toBe(1)
|
||||
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(ACK_EXPECTED, snapshot, MODE)
|
||||
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl', 'ack.expected.md'])
|
||||
})
|
||||
})
|
||||
@@ -6,8 +6,8 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { chromium } from 'playwright'
|
||||
import { expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import type { Fiber } from 'cordis'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import type { Fiber } from '@deepseek-ai/cordis'
|
||||
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
|
||||
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
|
||||
import { REPO_ROOT } from './support.ts'
|
||||
|
||||
@@ -14,7 +14,7 @@ installAssembledBootEnv()
|
||||
|
||||
/** Open the fixture history session (the alpha log carrying the turn-72 image pair) and wait for its gallery. */
|
||||
async function openFixtureSession(): Promise<void> {
|
||||
const tree = await screen.findByRole('tree', { name: '会话' }, { timeout: 10_000 })
|
||||
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
|
||||
const group = (await within(tree).findAllByText('fixture'))
|
||||
.map(el => el.closest<HTMLElement>('[role="treeitem"]'))
|
||||
.find(el => el?.getAttribute('aria-expanded') !== null)
|
||||
@@ -33,7 +33,6 @@ async function openFixtureSession(): Promise<void> {
|
||||
}
|
||||
|
||||
it('renders the history image pair through the authorized attachment route and opens the lightbox', async () => {
|
||||
localStorage.setItem('dsh.locale', 'zh')
|
||||
mountAssembledApp()
|
||||
await openFixtureSession()
|
||||
|
||||
@@ -73,24 +72,23 @@ it('renders the history image pair through the authorized attachment route and o
|
||||
fireEvent.doubleClick(frame)
|
||||
const lightbox = await screen.findByRole('dialog')
|
||||
expect(within(lightbox).getByRole('img').getAttribute('src')?.split(':')[0]).toBe('blob')
|
||||
fireEvent.click(within(lightbox).getByRole('button', { name: /关闭/ }))
|
||||
fireEvent.click(within(lightbox).getByRole('button', { name: /Close/ }))
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts pasted images into the composer rail in order and removes them', async () => {
|
||||
localStorage.setItem('dsh.locale', 'zh')
|
||||
mountAssembledApp()
|
||||
|
||||
const tree = await screen.findByRole('tree', { name: '会话' }, { timeout: 10_000 })
|
||||
const start = tree.querySelector<HTMLButtonElement>('button[aria-label="在“fixture”中新建会话"]')
|
||||
const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
|
||||
const start = tree.querySelector<HTMLButtonElement>('button[aria-label="New session in fixture"]')
|
||||
if (start === null) throw new Error('fixture Workspace new-session action missing')
|
||||
fireEvent.click(start)
|
||||
|
||||
// Image-only send arming is pinned at package level (input-bar.spec.tsx);
|
||||
// this assembled lane pins the intake chain over the built graph.
|
||||
const textarea = await screen.findByPlaceholderText('描述你想要构建的内容', {}, { timeout: 10_000 })
|
||||
const textarea = await screen.findByPlaceholderText('Describe what you want to build', {}, { timeout: 10_000 })
|
||||
const image = new File([new Uint8Array([137, 80, 78, 71])], 'pasted.png', { type: 'image/png' })
|
||||
fireEvent.paste(textarea, {
|
||||
clipboardData: {
|
||||
@@ -102,7 +100,7 @@ it('accepts pasted images into the composer rail in order and removes them', asy
|
||||
// The rail is an accessible group holding the draft thumbnail (queried via
|
||||
// DOM: jsdom's a11y-visibility computation hides the composer subtree).
|
||||
const rail = await waitFor(() => {
|
||||
const el = document.querySelector('[role="group"][aria-label="待发送图片"]')
|
||||
const el = document.querySelector('[role="group"][aria-label="Pending images"]')
|
||||
if (el === null) throw new Error('attachment rail missing')
|
||||
return el
|
||||
}, { timeout: 5_000 })
|
||||
@@ -129,10 +127,10 @@ it('accepts pasted images into the composer rail in order and removes them', asy
|
||||
.toEqual(['pasted.png', 'second.png'])
|
||||
})
|
||||
|
||||
const remove = [...rail.querySelectorAll('button[aria-label^="移除图片"]')]
|
||||
const remove = [...rail.querySelectorAll('button[aria-label^="Remove image"]')]
|
||||
if (remove.length !== 2) throw new Error('remove buttons missing')
|
||||
for (const button of remove) fireEvent.click(button)
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('[role="group"][aria-label="待发送图片"]')).toBeNull()
|
||||
expect(document.querySelector('[role="group"][aria-label="Pending images"]')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -128,6 +128,7 @@ describe('web e2e: message IconActions and clocks on settled history', () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-message-actions-aria'))
|
||||
await page.getByRole('button', { name: /^Select model, current/ })
|
||||
.waitFor({ timeout: 10_000 })
|
||||
await page.getByText(/Cache hit \d+%/u).first().waitFor({ timeout: 10_000 })
|
||||
// Keep a footer focused so opacity-hidden actions stay in the a11y tree
|
||||
// as an active/focused control during the capture.
|
||||
await page.getByRole('button', { name: 'Copy' }).first().focus()
|
||||
|
||||
115
apps/web/tests/minimal-preset.snapshot.ts
Normal file
115
apps/web/tests/minimal-preset.snapshot.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import type { AgentHandle } from '@deepseek-ai/dsh-agent'
|
||||
import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type {} from '@deepseek-ai/dsh-agent-presets'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { assertFixtureInventory, launchWebScaffold, type WebScaffold } from './scaffold.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/minimal-preset', import.meta.url))
|
||||
const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl')
|
||||
const PROMPT = 'Reply exactly MINIMAL_PRESET_REQUEST_OK and stop.'
|
||||
|
||||
describe('minimal agent preset', () => {
|
||||
let scaffold: WebScaffold
|
||||
let agentHandle: AgentHandle
|
||||
let disposeInjectedPrompt: () => void
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({ replayFixture: FIXTURE })
|
||||
disposeInjectedPrompt = scaffold.ctx.systemPrompt.section({
|
||||
name: 'test:injected-prompt',
|
||||
order: 999,
|
||||
text: 'THIS TEXT MUST NOT REACH THE MODEL.',
|
||||
})
|
||||
agentHandle = await scaffold.ctx.agents.create({
|
||||
sessionId: SessionId('minimal-preset-smoke'),
|
||||
meta: { cwd: scaffold.workspaceCwd, agentPreset: 'minimal' },
|
||||
agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
setup: agentCtx => scaffold.ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined),
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const failures: unknown[] = []
|
||||
await agentHandle?.dispose().catch((error: unknown) => failures.push(error))
|
||||
try {
|
||||
disposeInjectedPrompt?.()
|
||||
} catch (error: unknown) {
|
||||
failures.push(error)
|
||||
}
|
||||
await scaffold?.close().catch((error: unknown) => failures.push(error))
|
||||
if (failures.length === 1) throw failures[0]
|
||||
if (failures.length > 1) throw new AggregateError(failures, 'minimal preset smoke teardown failed')
|
||||
})
|
||||
|
||||
it('sends the exact RL prompt and schemas, then executes the persistent shell and editor', async () => {
|
||||
agentHandle.agent.followup(createUserMessage({
|
||||
content: [{ type: 'text', text: PROMPT }],
|
||||
source: { kind: 'user' },
|
||||
}))
|
||||
await agentHandle.agent.whenIdle()
|
||||
|
||||
const requestHeader = agentHandle.agent.session.requestHeader()
|
||||
if (requestHeader === undefined) throw new Error('the minimal agent issued no model request')
|
||||
|
||||
const stateDir = join(scaffold.workspaceCwd, 'persistent-state')
|
||||
await mkdir(stateDir)
|
||||
const signal = new AbortController().signal
|
||||
await scaffold.ctx.tools.execute({
|
||||
signal,
|
||||
callId: CallId('minimal-bash-state-setup'),
|
||||
name: 'bash',
|
||||
arguments: { command: `cd ${JSON.stringify(stateDir)} && export DSH_MINIMAL_STATE=PERSISTED` },
|
||||
agent: agentHandle.agent,
|
||||
})
|
||||
const bash = await scaffold.ctx.tools.execute({
|
||||
signal,
|
||||
callId: CallId('minimal-bash-state-read'),
|
||||
name: 'bash',
|
||||
arguments: { command: 'printf \'%s:%s\n\' "$DSH_MINIMAL_STATE" "$PWD"' },
|
||||
agent: agentHandle.agent,
|
||||
})
|
||||
const seedPath = join(scaffold.workspaceCwd, 'preset-smoke.txt')
|
||||
await writeFile(seedPath, 'MINIMAL_EDITOR_OK\n')
|
||||
const editor = await scaffold.ctx.tools.execute({
|
||||
signal,
|
||||
callId: CallId('minimal-editor-smoke'),
|
||||
name: 'str_replace_editor',
|
||||
arguments: { command: 'view', path: seedPath },
|
||||
agent: agentHandle.agent,
|
||||
})
|
||||
|
||||
const text = (result: typeof bash): string => result.content
|
||||
.filter(block => block.type === 'text')
|
||||
.map(block => block.text)
|
||||
.join('')
|
||||
.replaceAll(scaffold.workspaceCwd, '{{cwd}}')
|
||||
.trimEnd()
|
||||
|
||||
expect({
|
||||
prompt: requestHeader.system,
|
||||
tools: requestHeader.tools?.map(tool => tool.name),
|
||||
bash: text(bash),
|
||||
editor: text(editor),
|
||||
}).toMatchInlineSnapshot(`
|
||||
{
|
||||
"bash": "PERSISTED:{{cwd}}/persistent-state",
|
||||
"editor": "Here's the content of {{cwd}}/preset-smoke.txt with line numbers (which has a total of 2 lines):
|
||||
1 MINIMAL_EDITOR_OK
|
||||
2",
|
||||
"prompt": "You are a helpful software engineer assistant.",
|
||||
"tools": [
|
||||
"bash",
|
||||
"str_replace_editor",
|
||||
],
|
||||
}
|
||||
`)
|
||||
expect(requestHeader.tools?.toSorted((left, right) => left.name.localeCompare(right.name)))
|
||||
.toEqual(scaffold.ctx.tools.schemas(agentHandle.agent).toSorted((left, right) => left.name.localeCompare(right.name)))
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['session.jsonl'])
|
||||
})
|
||||
})
|
||||
@@ -29,13 +29,12 @@ import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import type { Page } from 'playwright'
|
||||
import { expect } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import Include, { type PatchOptions } from '@cordisjs/plugin-include'
|
||||
import Group from '@cordisjs/plugin-group'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import Loader from '@deepseek-ai/cordis-plugin-loader'
|
||||
import Include, { type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
import Group from '@deepseek-ai/cordis-plugin-group'
|
||||
import { scrubRequestHeaders, stabilizeFixtureMessageIds } from '@deepseek-ai/dsh-acp-snapshot'
|
||||
import {
|
||||
addHarnessSourceSection,
|
||||
assertEntriesLoaded,
|
||||
composeEntries,
|
||||
healProfilesModuleFallback,
|
||||
@@ -65,6 +64,7 @@ import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
|
||||
// Empty type imports carry the httpServer/agents/sessionPersistence Context merges.
|
||||
import type {} from '@deepseek-ai/dsh-host-webserver'
|
||||
import type {} from '@deepseek-ai/dsh-agent'
|
||||
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
|
||||
import { REPO_ROOT, requireDist } from './support.ts'
|
||||
|
||||
/** Snapshot mode for the lane, from $DSH_SNAPSHOT (same vocabulary as the other snapshot suites). */
|
||||
@@ -245,12 +245,21 @@ export interface LaunchOptions {
|
||||
}
|
||||
/** Leave the current welcome notice unacknowledged; ordinary scenarios publish it as complete before browser boot. */
|
||||
welcomeNoticePending?: boolean
|
||||
/**
|
||||
* Mount the shipped telemetry row in FULL mode against this exporter URL
|
||||
* instead of disabling it. Used to pin a real backend disclosure in
|
||||
* assembled coverage; point the URL at a local dead endpoint so no record
|
||||
* leaves the process.
|
||||
*/
|
||||
telemetryUrl?: string
|
||||
/**
|
||||
* Browse through a trusted non-loopback hostname that the browser resolves
|
||||
* to loopback (for example `*.localhost`). The test server stays bound to
|
||||
* 127.0.0.1; a non-resolving authority fails before Host trust is exercised.
|
||||
*/
|
||||
remoteAuthority?: string
|
||||
/** Reuse an existing harness home so a second Host can verify user settings across origins. */
|
||||
harnessHome?: string
|
||||
}
|
||||
|
||||
/** Dispose the booted tree and remove both owned temp roots, reporting every independent cleanup failure. */
|
||||
@@ -297,16 +306,19 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
// Isolated harness home: the settings/credentials rows resolve $DSH_HOME
|
||||
// paths at load, and an in-process boot must NEVER touch the developer's
|
||||
// real ~/.dsh document or credential file.
|
||||
const harnessHome = join(workspaceCwd, '.dsh-home')
|
||||
const harnessHome = options.harnessHome ?? join(workspaceCwd, '.dsh-home')
|
||||
// Skill discovery is model-visible input, and its roots now resolve inside a
|
||||
// PRESET — a subtree this lane's include patches cannot reach, because the
|
||||
// roster mounts it directly per session rather than as a row of the booted
|
||||
// tree. The row's documented fallback is the environment, so pin that: the
|
||||
// whole scaffold lifetime, not just the boot, since presets mount when a
|
||||
// session is created. Without this a developer's real ~/.dsh/skills silently
|
||||
// enters replay requests and goldens while CI sees none.
|
||||
// enters replay requests and goldens while CI sees none. `DSH_HOME` follows
|
||||
// the resolved harness home so a scaffold sharing another's home — the
|
||||
// cross-port persistence scenario — pins the same roots the settings and
|
||||
// credentials rows were configured with.
|
||||
const skillRootEnvironment = {
|
||||
DSH_HOME: join(workspaceCwd, '.dsh-home'),
|
||||
DSH_HOME: harnessHome,
|
||||
DSH_AGENTS_HOME: join(workspaceCwd, '.agents-home'),
|
||||
DSH_BUNDLED_SKILL_DIR: join(workspaceCwd, '.bundled-skills'),
|
||||
}
|
||||
@@ -329,6 +341,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
} catch (error) {
|
||||
const failures: unknown[] = [error]
|
||||
await rm(workspaceCwd, { recursive: true, force: true }).catch((cleanupError: unknown) => failures.push(cleanupError))
|
||||
restoreSkillRootEnvironment()
|
||||
if (failures.length > 1) throw new AggregateError(failures, 'web scaffold temp-root setup failed')
|
||||
throw error
|
||||
}
|
||||
@@ -390,8 +403,11 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
{ id: 'session-title-llm', disabled: true },
|
||||
// Fixture sessions must never leave the process: the shipped row defaults
|
||||
// to the production OTLP endpoint (or whatever DSH_TELEMETRY_OTLP_URL
|
||||
// names in the ambient environment).
|
||||
{ id: 'telemetry-otel', disabled: true },
|
||||
// names in the ambient environment). A scenario that pins a real backend
|
||||
// disclosure passes a local dead endpoint instead of disabling the row.
|
||||
options.telemetryUrl === undefined
|
||||
? { id: 'telemetry-otel', disabled: true }
|
||||
: { id: 'telemetry-otel', config: { exporter: { url: options.telemetryUrl }, shutdownTimeoutMillis: 1_000 } },
|
||||
{
|
||||
id: 'webserver',
|
||||
config: { host: '127.0.0.1', port: 0 },
|
||||
@@ -454,19 +470,26 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
ctx.baseUrl = pathToFileURL(profileDir).href + '/'
|
||||
// This direct Loader harness supplies the same root-path capability as app-boot.
|
||||
ctx.provide('dshHomePath', dshHomePath)
|
||||
// A host with no command line still provides one: the web bundle's startup
|
||||
// row releases the rows waiting on it, and with no arguments each starts on
|
||||
// the values this scaffold composed above. An exit request can only come
|
||||
// from a rejected argument, which a fixed empty list has none of.
|
||||
provideCmdline(ctx, {
|
||||
args: [],
|
||||
exit: (code) => {
|
||||
throw new Error(`web e2e scaffold: the web app requested exit ${String(code)} with no arguments to reject`)
|
||||
},
|
||||
})
|
||||
await ctx.plugin(Loader)
|
||||
ctx.loader.builtins.include = Include
|
||||
// `cordis:group` beside it, exactly as `boot()` registers it: a group row is
|
||||
// how a preset gives one `isolate` realm to a provider and its consumers,
|
||||
// and a preset resolving package names from its own directory cannot reach
|
||||
// `@cordisjs/plugin-group` by name.
|
||||
// `@deepseek-ai/cordis-plugin-group` by name.
|
||||
ctx.loader.builtins.group = Group
|
||||
// The shipped CLI deliberately has no dependency on this opt-in package.
|
||||
// Keep the Loader row real without broadening the product installation.
|
||||
if (options.cordisTools === true) ctx.loader.builtins['tool-cordis'] = ToolCordis
|
||||
if (surfaceContext) {
|
||||
ctx.inject(['systemPrompt'], (promptCtx) => { addHarnessSourceSection(promptCtx, REPO_ROOT) })
|
||||
}
|
||||
await ctx.loader.create({
|
||||
name: 'cordis:include',
|
||||
config: { path: pathToFileURL(rootConfig).href, patches },
|
||||
|
||||
@@ -468,9 +468,9 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
if (done?.type !== 'command/done') throw new Error('feedback command did not settle')
|
||||
const [sessionLine, userLine, extraLine] = done.data.text?.split('\n') ?? []
|
||||
expect(sessionLine).toBe(`Feedback recorded for session ${SEED_ID}`)
|
||||
expect(userLine).toMatch(/^User: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)
|
||||
expect(userLine).toMatch(/^User: [0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\./i)
|
||||
expect(extraLine).toBeUndefined()
|
||||
const userId = userLine?.slice('User: '.length)
|
||||
const userId = userLine?.match(/^User: ([0-9a-f-]+)/i)?.[1]
|
||||
if (userId === undefined) throw new Error('feedback command omitted the user id')
|
||||
|
||||
const snapshot = (await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd))
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// Web e2e scenarios: the settings surface — the modal shell (trigger, nav,
|
||||
// section switching, both close paths), the Appearance preference row (the
|
||||
// real theme gesture — click 深色 and the whole cascade runs: ThemeService preference -> localStorage dsh.theme
|
||||
// real theme gesture — click 深色 and the whole cascade runs: ThemeService preference -> Host settings
|
||||
// -> theme/change -> ui-layout's presenter -> body attribute -> alias token +
|
||||
// browser theme-color metadata)
|
||||
// the Language row (settings-scoped localization + persisted dsh.locale),
|
||||
// the busy-state Enter preference, plus Permission as the persisted default
|
||||
// for subsequently created sessions.
|
||||
// the Language row and busy-state Enter preference (both Host-backed), plus
|
||||
// Permission as the persisted default for subsequently created sessions.
|
||||
// Zero model calls: everything is pure client + persistence state on a blank
|
||||
// frame, so there is no fixture and a stray stream would fail loud on the
|
||||
// open llm seam.
|
||||
@@ -153,23 +152,24 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('flips the theme through the Appearance cubes and persists across reload', async () => {
|
||||
it('flips the theme through the Appearance cubes and persists across reload and a distinct port', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-appearance'))
|
||||
interface ThemeState {
|
||||
attr: boolean
|
||||
background: string
|
||||
stored: string | null
|
||||
/** Pre-migration localStorage key; the Host-backed world never writes it. */
|
||||
legacy: string | null
|
||||
themeColor: string | null
|
||||
themeColorCount: number
|
||||
token: string
|
||||
}
|
||||
const readState = async (): Promise<ThemeState> => await page.evaluate(() => {
|
||||
const readState = async (target: Page = page): Promise<ThemeState> => await target.evaluate(() => {
|
||||
const metas = document.head.querySelectorAll<HTMLMetaElement>('meta[name="theme-color"]')
|
||||
const computed = getComputedStyle(document.body)
|
||||
return {
|
||||
attr: document.body.hasAttribute('data-ds-dark-theme'),
|
||||
background: computed.backgroundColor,
|
||||
stored: localStorage.getItem('dsh.theme'),
|
||||
legacy: localStorage.getItem('dsh.theme'),
|
||||
themeColor: metas[0]?.content ?? null,
|
||||
themeColorCount: metas.length,
|
||||
token: computed.getPropertyValue('--dsw-alias-bg-base').trim(),
|
||||
@@ -193,27 +193,51 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
const darkCube = dialog.getByRole('button', { name: '深色' })
|
||||
expect(await darkCube.getAttribute('aria-pressed')).toBe('false')
|
||||
await darkCube.click()
|
||||
// The full cascade: pressed state, persisted preference, body attribute,
|
||||
// The full cascade: pressed state, Host-backed preference, body attribute,
|
||||
// alias token flip — all from one real user gesture.
|
||||
await expect.poll(() => darkCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true')
|
||||
const dark = await readState()
|
||||
expect(dark.attr).toBe(true)
|
||||
expect(dark.stored).toBe('dark')
|
||||
expect(dark.legacy).toBeNull()
|
||||
expect(dark.token).not.toBe(light.token)
|
||||
expectThemeColorSynchronized(dark)
|
||||
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
|
||||
.toMatch(/ui-theme:\n\s+preference: dark/)
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
// Reload: the preference survives boot (restore + presenter initial apply).
|
||||
// Reload: the preference survives the background Host read + presenter update.
|
||||
const warningStart = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
await page.emulateMedia({ colorScheme: 'light' })
|
||||
await expect.poll(async () => (await readState()).attr, { timeout: 5_000 }).toBe(true)
|
||||
const reloaded = await readState()
|
||||
expect(reloaded.attr).toBe(true)
|
||||
expect(reloaded.stored).toBe('dark')
|
||||
expect(reloaded.legacy).toBeNull()
|
||||
expectThemeColorSynchronized(reloaded)
|
||||
|
||||
// A second live Host binds another ephemeral port but shares the same
|
||||
// user-settings home. Its fresh origin has no theme localStorage and still
|
||||
// converges to dark before the settings dialog opens.
|
||||
const second = await launchWebScaffold({ harnessHome: scaffold.harnessHome })
|
||||
const secondPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
|
||||
const secondTripwire = watchConsole(secondPage)
|
||||
try {
|
||||
expect(second.baseUrl).not.toBe(scaffold.baseUrl)
|
||||
await secondPage.emulateMedia({ colorScheme: 'light' })
|
||||
await secondPage.goto(second.baseUrl, { waitUntil: 'load' })
|
||||
await secondPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await expect.poll(async () => (await readState(secondPage)).attr, { timeout: 5_000 }).toBe(true)
|
||||
const secondState = await readState(secondPage)
|
||||
expect(secondState.legacy).toBeNull()
|
||||
expectThemeColorSynchronized(secondState)
|
||||
expect(secondTripwire.pageErrors).toEqual([])
|
||||
expect(secondTripwire.warnings).toEqual([])
|
||||
} finally {
|
||||
await secondPage.close()
|
||||
await second.close()
|
||||
}
|
||||
|
||||
// `system` follows the emulated OS scheme (dark stays dark, light clears).
|
||||
await page.getByRole('button', { name: '设置', exact: true }).click()
|
||||
const systemCube = page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '跟随系统' })
|
||||
@@ -233,7 +257,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 90_000)
|
||||
|
||||
it('persists the busy-state Enter behavior across reload and restores Queue', async () => {
|
||||
it('persists the busy-state Enter behavior across reload and a distinct port', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-enter-behavior'))
|
||||
await page.getByRole('button', { name: '设置', exact: true }).click()
|
||||
const dialog = page.getByRole('dialog', { name: '设置' })
|
||||
@@ -241,7 +265,9 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
await dialog.getByRole('button', { name: '排队发送' }).click()
|
||||
await page.getByRole('menuitem', { name: '插话发送' }).click()
|
||||
await dialog.getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 })
|
||||
expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBe('steer')
|
||||
expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBeNull()
|
||||
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
|
||||
.toMatch(/ui-conversation:\n\s+busyEnter: steer/)
|
||||
await page.keyboard.press('Escape')
|
||||
|
||||
const warningStart = tripwire.warnings.length
|
||||
@@ -251,15 +277,36 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
await page.getByRole('button', { name: '设置', exact: true }).click()
|
||||
const reloaded = page.getByRole('dialog', { name: '设置' })
|
||||
await reloaded.getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 })
|
||||
|
||||
const second = await launchWebScaffold({ harnessHome: scaffold.harnessHome })
|
||||
const secondPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
|
||||
const secondTripwire = watchConsole(secondPage)
|
||||
try {
|
||||
expect(second.baseUrl).not.toBe(scaffold.baseUrl)
|
||||
await secondPage.goto(second.baseUrl, { waitUntil: 'load' })
|
||||
await secondPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await secondPage.getByRole('button', { name: '设置', exact: true }).click()
|
||||
await secondPage.getByRole('dialog', { name: '设置' })
|
||||
.getByRole('button', { name: '插话发送' }).waitFor({ timeout: 10_000 })
|
||||
expect(await secondPage.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBeNull()
|
||||
expect(secondTripwire.pageErrors).toEqual([])
|
||||
expect(secondTripwire.warnings).toEqual([])
|
||||
} finally {
|
||||
await secondPage.close()
|
||||
await second.close()
|
||||
}
|
||||
|
||||
await reloaded.getByRole('button', { name: '插话发送' }).click()
|
||||
await page.getByRole('menuitem', { name: '排队发送' }).click()
|
||||
await reloaded.getByRole('button', { name: '排队发送' }).waitFor({ timeout: 10_000 })
|
||||
expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBe('queue')
|
||||
expect(await page.evaluate(() => localStorage.getItem('dsh.conversation.busyEnter'))).toBeNull()
|
||||
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
|
||||
.toMatch(/ui-conversation:\n\s+busyEnter: queue/)
|
||||
await page.keyboard.press('Escape')
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 90_000)
|
||||
|
||||
it('switches the settings surface language and persists dsh.locale', async () => {
|
||||
it('persists the settings language across reload and a distinct port', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-language'))
|
||||
await page.getByRole('button', { name: '设置', exact: true }).click()
|
||||
const zhDialog = page.getByRole('dialog', { name: '设置' })
|
||||
@@ -276,7 +323,9 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
await enDialog.waitFor({ timeout: 10_000 })
|
||||
expect(await enDialog.getByRole('button', { name: 'General' }).getAttribute('aria-current')).toBe('true')
|
||||
await expect.poll(() => enDialog.getByText('Appearance', { exact: true }).count(), { timeout: 5_000 }).toBe(1)
|
||||
expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBe('en')
|
||||
expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
|
||||
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
|
||||
.toMatch(/locale:\n\s+preference: en/)
|
||||
// Reload keeps English; then restore zh so shared page state (and the
|
||||
// other specs' 设置-anchored selectors + goldens) see the default again.
|
||||
const warningStart = tripwire.warnings.length
|
||||
@@ -285,24 +334,47 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
acknowledgeReloadConnectionLoss(tripwire, warningStart)
|
||||
const enTrigger = page.getByRole('button', { name: 'Settings' })
|
||||
await enTrigger.waitFor({ timeout: 10_000 })
|
||||
|
||||
// A Chinese browser on another port still receives the explicit English
|
||||
// preference from the shared Host settings document.
|
||||
const second = await launchWebScaffold({ harnessHome: scaffold.harnessHome })
|
||||
const secondPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE })
|
||||
const secondTripwire = watchConsole(secondPage)
|
||||
try {
|
||||
expect(second.baseUrl).not.toBe(scaffold.baseUrl)
|
||||
await secondPage.goto(second.baseUrl, { waitUntil: 'load' })
|
||||
await secondPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await secondPage.getByRole('button', { name: 'Settings', exact: true }).click()
|
||||
await secondPage.getByRole('dialog', { name: 'Settings' })
|
||||
.getByRole('button', { name: 'English' }).waitFor({ timeout: 10_000 })
|
||||
expect(await secondPage.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
|
||||
expect(secondTripwire.pageErrors).toEqual([])
|
||||
expect(secondTripwire.warnings).toEqual([])
|
||||
} finally {
|
||||
await secondPage.close()
|
||||
await second.close()
|
||||
}
|
||||
|
||||
await enTrigger.click()
|
||||
await page.getByRole('dialog', { name: 'Settings' }).getByRole('button', { name: 'English' }).click()
|
||||
await page.getByRole('menuitem', { name: '中文' }).click()
|
||||
await page.getByRole('dialog', { name: '设置' }).waitFor({ timeout: 10_000 })
|
||||
expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBe('zh')
|
||||
expect(await page.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
|
||||
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
|
||||
.toMatch(/locale:\n\s+preference: zh/)
|
||||
await page.keyboard.press('Escape')
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 90_000)
|
||||
|
||||
it('opens an English browser in English without any stored preference', async () => {
|
||||
// A second page under a different browser language: nothing is persisted
|
||||
// for it, so the settings surface must follow the browser rather than the
|
||||
// product fallback the shared zh page shows.
|
||||
// A fresh Host home has no locale preference, so its surface follows the
|
||||
// browser rather than the product fallback.
|
||||
const fresh = await launchWebScaffold({})
|
||||
const enPage = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: 'en-US' })
|
||||
const enTripwire = watchConsole(enPage)
|
||||
onTestFailed(() => saveFailureShot(enPage, 'web-e2e-settings-browser-language'))
|
||||
try {
|
||||
await enPage.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await enPage.goto(fresh.baseUrl, { waitUntil: 'load' })
|
||||
await enPage.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
expect(await enPage.evaluate(() => localStorage.getItem('dsh.locale'))).toBeNull()
|
||||
await enPage.getByRole('button', { name: 'Settings', exact: true }).click()
|
||||
@@ -315,6 +387,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
||||
expect(enTripwire.warnings).toEqual([])
|
||||
} finally {
|
||||
await enPage.close()
|
||||
await fresh.close()
|
||||
}
|
||||
}, 90_000)
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ const EXPECTED_TOOLS = [
|
||||
'read',
|
||||
'send_message',
|
||||
'skill',
|
||||
'str_replace_editor',
|
||||
'subagent',
|
||||
'subagent_fork',
|
||||
'task_kill',
|
||||
|
||||
@@ -485,10 +485,13 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
|
||||
child = spawn(
|
||||
process.execPath,
|
||||
[
|
||||
'--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--port', String(port),
|
||||
'--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web',
|
||||
// Launcher flags come first: the first token the launcher does not own
|
||||
// starts the web app's own arguments.
|
||||
// Pin the in-browser picker: the shipped `-auto` row would resolve to
|
||||
// the native OS chooser on this bind, and no page can drive that.
|
||||
'--patch', fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)),
|
||||
'--port', String(port),
|
||||
],
|
||||
{
|
||||
cwd: sessionsDir,
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
- list:
|
||||
- listitem:
|
||||
- 'button "当前使用: 标准模式" [disabled] [pressed]':
|
||||
- text: 标准模式 内置 当前使用 完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。
|
||||
- text: 标准模式 内置 当前使用 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
|
||||
- code: standard
|
||||
- 'button "查看: 标准模式"':
|
||||
- img
|
||||
@@ -30,7 +30,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 代码模式"':
|
||||
- text: 代码模式 内置 标准模式的工具改为 Code Mode 呈现:模型写一段 TypeScript 调用 SDK,一次执行代替多轮工具调用。
|
||||
- text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
|
||||
- code: code
|
||||
- 'button "查看: 代码模式"':
|
||||
- img
|
||||
@@ -40,7 +40,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 极简模式"':
|
||||
- text: 极简模式 内置 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。
|
||||
- text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
|
||||
- code: minimal
|
||||
- 'button "查看: 极简模式"':
|
||||
- img
|
||||
@@ -50,7 +50,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 创造模式"':
|
||||
- text: 创造模式 内置 标准模式加上自指工具集,可以读改自己运行的这套组装,并据此创作新的预设。
|
||||
- text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。
|
||||
- code: cordis
|
||||
- 'button "查看: 创造模式"':
|
||||
- img
|
||||
@@ -62,7 +62,7 @@
|
||||
- list:
|
||||
- listitem:
|
||||
- 'button "设为默认: 我的模式"':
|
||||
- text: 我的模式 自定义 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。
|
||||
- text: 我的模式 自定义 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
|
||||
- code: my-agent
|
||||
- 'button "查看路径: 我的模式"':
|
||||
- img
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
- list:
|
||||
- listitem:
|
||||
- 'button "当前使用: 标准模式" [disabled] [pressed]':
|
||||
- text: 标准模式 内置 当前使用 完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。
|
||||
- text: 标准模式 内置 当前使用 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
|
||||
- code: standard
|
||||
- 'button "查看: 标准模式"':
|
||||
- img
|
||||
@@ -30,7 +30,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 代码模式"':
|
||||
- text: 代码模式 内置 标准模式的工具改为 Code Mode 呈现:模型写一段 TypeScript 调用 SDK,一次执行代替多轮工具调用。
|
||||
- text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
|
||||
- code: code
|
||||
- 'button "查看: 代码模式"':
|
||||
- img
|
||||
@@ -40,7 +40,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 极简模式"':
|
||||
- text: 极简模式 内置 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。
|
||||
- text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
|
||||
- code: minimal
|
||||
- 'button "查看: 极简模式"':
|
||||
- img
|
||||
@@ -50,7 +50,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 创造模式"':
|
||||
- text: 创造模式 内置 标准模式加上自指工具集,可以读改自己运行的这套组装,并据此创作新的预设。
|
||||
- text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。
|
||||
- code: cordis
|
||||
- 'button "查看: 创造模式"':
|
||||
- img
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
- list:
|
||||
- listitem:
|
||||
- 'button "当前使用: 标准模式" [disabled] [pressed]':
|
||||
- text: 标准模式 内置 当前使用 完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。
|
||||
- text: 标准模式 内置 当前使用 功能完整的编码 Agent,支持文件编辑、Shell、文件与网页检索、Skills、计划、目标、子代理和工作流。
|
||||
- code: standard
|
||||
- 'button "查看: 标准模式"':
|
||||
- img
|
||||
@@ -30,7 +30,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 代码模式"':
|
||||
- text: 代码模式 内置 标准模式的工具改为 Code Mode 呈现:模型写一段 TypeScript 调用 SDK,一次执行代替多轮工具调用。
|
||||
- text: 代码模式 内置 具备标准模式的全部能力,并通过 Code Mode SDK 呈现工具,让模型用一个 TypeScript 程序组合多步操作。
|
||||
- code: code
|
||||
- 'button "查看: 代码模式"':
|
||||
- img
|
||||
@@ -40,7 +40,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 极简模式"':
|
||||
- text: 极简模式 内置 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。
|
||||
- text: 极简模式 内置 仅提供持久 bash 与 str_replace_editor 的双工具编码 Agent。
|
||||
- code: minimal
|
||||
- 'button "查看: 极简模式"':
|
||||
- img
|
||||
@@ -50,7 +50,7 @@
|
||||
- text: 复制
|
||||
- listitem:
|
||||
- 'button "设为默认: 创造模式"':
|
||||
- text: 创造模式 内置 标准模式加上自指工具集,可以读改自己运行的这套组装,并据此创作新的预设。
|
||||
- text: 创造模式 内置 用于创建自定义 Agent preset:具备标准模式的全部能力,并提供运行时检查、插件实验和 preset 创作指导。
|
||||
- code: cordis
|
||||
- 'button "查看: 创造模式"':
|
||||
- img
|
||||
@@ -58,6 +58,7 @@
|
||||
- 'button "复制: 创造模式"':
|
||||
- img
|
||||
- text: 复制
|
||||
- heading "自定义" [level=3]
|
||||
- button "用「创造模式」创作自定义预设":
|
||||
- img
|
||||
- text: 用「创造模式」创作自定义预设
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Seeded turn" [disabled]
|
||||
- img
|
||||
- text: 极简模式
|
||||
- text: Minimal mode
|
||||
- button "1 subagent":
|
||||
- text: 1 subagent
|
||||
- img
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- img
|
||||
- text: workspace
|
||||
- img
|
||||
- button "标准模式":
|
||||
- button "Standard mode":
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- img
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- menu:
|
||||
- menuitem "标准模式 完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。":
|
||||
- text: 标准模式 完整的编码 agent:文件读写、shell、检索、计划、委派与工作流。
|
||||
- menuitem "Standard mode Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows.":
|
||||
- text: Standard mode Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows.
|
||||
- img
|
||||
- menuitem "代码模式 标准模式的工具改为 Code Mode 呈现:模型写一段 TypeScript 调用 SDK,一次执行代替多轮工具调用。"
|
||||
- menuitem "极简模式 只向模型呈现 bash 与 str_replace_editor,适合 benchmark 与最小复现。"
|
||||
- menuitem "创造模式 标准模式加上自指工具集,可以读改自己运行的这套组装,并据此创作新的预设。"
|
||||
- menuitem "Code mode All Standard mode capabilities, with tools exposed through the Code Mode SDK so the model can combine multi-step operations in one TypeScript program."
|
||||
- menuitem "Minimal mode Two-tool coding agent with persistent bash and str_replace_editor."
|
||||
- menuitem "Creator mode Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance."
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- 'button "Using ONE run_code program: run" [disabled]'
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use only Cordis tools. First" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
39
apps/web/tests/snapshots/feedback-command/ack.expected.md
Normal file
39
apps/web/tests/snapshots/feedback-command/ack.expected.md
Normal file
@@ -0,0 +1,39 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with the single word" [disabled]
|
||||
- img
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- text: Reply with the single word LIGHTHOUSE and stop. {{clock}}
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Context injection @deepseek-ai/dsh-system-prompt":
|
||||
- img
|
||||
- img
|
||||
- text: Context injection @deepseek-ai/dsh-system-prompt
|
||||
- button "Think The user wants me to reply with a single word. Let me comply.":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user wants me to reply with a single word. Let me comply.
|
||||
- paragraph: LIGHTHOUSE
|
||||
- button "Copy":
|
||||
- img
|
||||
- button "Branch into a new conversation":
|
||||
- img
|
||||
- text: {{clock}} Ran for {{duration}} TTFT {{duration}} {{throughput}} tok/s
|
||||
- 'button "feedback Feedback recorded for session session-{{uuid}} User: {{uuid}}. Session sharing is enabled."':
|
||||
- img
|
||||
- img
|
||||
- text: "feedback Feedback recorded for session session-{{uuid}} User: {{uuid}}. Session sharing is enabled."
|
||||
- textbox "Message the agent"
|
||||
- button "Commands":
|
||||
- img
|
||||
- 'button "Access mode, current: Workspace Write"': Workspace Write
|
||||
- button "Select model, current DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "6% of context used"
|
||||
- button "Send message" [disabled]
|
||||
- text: 1 turns · 1 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 7.8K tok · Output 21 tok
|
||||
17
apps/web/tests/snapshots/feedback-command/session.jsonl
Normal file
17
apps/web/tests/snapshots/feedback-command/session.jsonl
Normal file
@@ -0,0 +1,17 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785015039278,"cwd":"{{cwd}}/workspace"}
|
||||
{"type":"turn/start","seq":0,"time":1785015039291,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user","rpcId":"{{rpcId}}"}}}}
|
||||
{"type":"user/message","seq":1,"time":1785015039292,"data":{"content":[{"type":"text","text":"Reply with the single word LIGHTHOUSE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"session/title","seq":2,"time":1785015039294,"data":{"title":"Reply with the single word","messageSeqs":[1],"source":{"kind":"fallback"}}}
|
||||
{"type":"step/start","seq":3,"time":1785015039362,"data":{"turn":1,"step":1}}
|
||||
{"type":"request/header","seq":4,"time":1785015039363,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"system":"{{system}}","tools":"{{tools}}","messagePrefix":["{{messagePrefix}}"]},"reason":"initial"}}
|
||||
{"type":"assistant/chunk","seq":5,"time":1785015039930,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
|
||||
{"type":"reasoning-chunks","seq0":6,"time0":1785015039930,"data":{"turn":1,"step":1,"index":0,"dt":[162,28,1,0,0,46,1,0,0,0,11,0,0,30],"texts":["The"," user"," wants"," me"," to"," reply"," with"," a"," single"," word","."," Let"," me"," comply","."]}}
|
||||
{"type":"assistant/chunk","seq":21,"time":1785015040209,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
|
||||
{"type":"text-chunks","seq0":22,"time0":1785015040209,"data":{"turn":1,"step":1,"index":1,"dt":[1,0,30,1],"texts":["L","IGH","TH","O","USE"]}}
|
||||
{"type":"assistant/chunk","seq":27,"time":1785015040241,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to reply with a single word. Let me comply."}}}}
|
||||
{"type":"assistant/chunk","seq":28,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"LIGHTHOUSE"}}}}
|
||||
{"type":"assistant/chunk","seq":29,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":109,"outputTokens":21,"cacheReadTokens":7680,"reasoningTokens":15}}}}
|
||||
{"type":"assistant/chunk","seq":30,"time":1785015040242,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
{"type":"assistant/message","seq":31,"time":1785015040244,"data":{"turn":1,"step":1,"content":[{"type":"reasoning","text":"The user wants me to reply with a single word. Let me comply."},{"type":"text","text":"LIGHTHOUSE"}],"provenance":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"usage":{"inputTokens":109,"outputTokens":21,"cacheReadTokens":7680,"reasoningTokens":15}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":32,"time":1785015040246,"data":{"turn":1,"step":1}}
|
||||
{"type":"turn/end","seq":33,"time":1785015040247,"data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the bash tool to" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "workspace" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
- img
|
||||
- text: workspace
|
||||
- img
|
||||
- button "标准模式":
|
||||
- button "Standard mode":
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- img
|
||||
- textbox "Describe what you want to build"
|
||||
- button "Commands":
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
- img
|
||||
- text: workspace
|
||||
- img
|
||||
- button "标准模式":
|
||||
- button "Standard mode":
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- img
|
||||
- textbox "Describe what you want to build"
|
||||
- button "Commands":
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with the single word" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785974400000,"cwd":"{{cwd}}"}
|
||||
{"type":"user/message","seq":0,"time":1785974400001,"data":{"content":[{"type":"text","text":"Reply exactly CORE_WEB_REQUEST_OK and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"session","version":0,"id":"{{sessionId}}","createdAt":1785974400000,"cwd":"{{cwd}}","agentPreset":"minimal"}
|
||||
{"type":"user/message","seq":0,"time":1785974400001,"data":{"content":[{"type":"text","text":"Reply exactly MINIMAL_PRESET_REQUEST_OK and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
|
||||
{"type":"assistant/chunk","seq":1,"time":1785974400002,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
{"type":"assistant/chunk","seq":2,"time":1785974400003,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"CORE_WEB_REQUEST_OK"}}}
|
||||
{"type":"assistant/chunk","seq":3,"time":1785974400004,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CORE_WEB_REQUEST_OK"}}}}
|
||||
{"type":"assistant/chunk","seq":2,"time":1785974400003,"data":{"turn":1,"step":1,"chunk":{"type":"text-delta","index":0,"text":"MINIMAL_PRESET_REQUEST_OK"}}}
|
||||
{"type":"assistant/chunk","seq":3,"time":1785974400004,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"MINIMAL_PRESET_REQUEST_OK"}}}}
|
||||
{"type":"assistant/chunk","seq":4,"time":1785974400005,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":4}}}}
|
||||
{"type":"assistant/chunk","seq":5,"time":1785974400006,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- 'button "Plan a small change: add" [disabled]'
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the ask_user_question tool to" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "workspace" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -38,10 +38,10 @@
|
||||
- text: Context injection AGENTS.md
|
||||
- img
|
||||
- text: permission preset read-only
|
||||
- 'button "feedback Feedback recorded for session {{seededId}} User: {{uuid}}" [expanded]':
|
||||
- 'button "feedback Feedback recorded for session {{seededId}} User: {{uuid}}. Session sharing is not configured." [expanded]':
|
||||
- img
|
||||
- text: "feedback Feedback recorded for session {{seededId}} User: {{uuid}}"
|
||||
- text: "Feedback recorded for session {{seededId}} User: {{uuid}}"
|
||||
- text: "feedback Feedback recorded for session {{seededId}} User: {{uuid}}. Session sharing is not configured."
|
||||
- text: "Feedback recorded for session {{seededId}} User: {{uuid}}. Session sharing is not configured."
|
||||
- textbox "Message the agent"
|
||||
- button "Commands":
|
||||
- img
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "/user-invoke-demo and confirm the fixtur" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the ask_user_question tool to" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the ask_user_question tool to" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the ask_user_question tool to" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the ask_user_question tool to" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
- button "Ask a research subagent to"
|
||||
- text: /
|
||||
- button "event-sourcing researcher" [disabled]
|
||||
- img
|
||||
- text: Standard mode
|
||||
- button "1 subagent":
|
||||
- text: 1 subagent
|
||||
- img
|
||||
@@ -41,7 +43,7 @@
|
||||
- textbox "Message the agent"
|
||||
- button "Commands":
|
||||
- img
|
||||
- 'button "Access mode, current: Workspace Write"': Workspace Write
|
||||
- 'button "Access mode, current: Custom"': Custom
|
||||
- button "6% of context used"
|
||||
- button "Send message" [disabled]
|
||||
- text: 2 turns · 2 steps LLM {{duration}} TTFT avg {{duration}} · {{throughput}} tok/s Cache hit 99% Input 15.6K tok · Output 158 tok
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
- button "Ask a research subagent to"
|
||||
- text: /
|
||||
- button "event-sourcing researcher" [disabled]
|
||||
- img
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -18,6 +20,6 @@
|
||||
- textbox "Parent session offline; sending is unavailable but you can still stop the run" [disabled]
|
||||
- button "Commands" [disabled]:
|
||||
- img
|
||||
- 'button "Access mode, current: Workspace Write" [disabled]': Workspace Write
|
||||
- 'button "Access mode, current: Custom" [disabled]': Custom
|
||||
- button "Stop generating"
|
||||
- button "Send message" [disabled]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Begin your reply with the" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Begin your reply with the" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use web_search to search exactly" [disabled]
|
||||
- img
|
||||
- text: 标准模式
|
||||
- text: Standard mode
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
|
||||
@@ -314,7 +314,7 @@ describe('web e2e: empty-draft Cmd+Enter steers the whole queue', () => {
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await connectFreshWorkspace(page, scaffold.workspaceCwd)
|
||||
await page.getByText('标准模式', { exact: true }).waitFor({ timeout: 10_000 })
|
||||
await page.getByText('Standard mode', { exact: true }).waitFor({ timeout: 10_000 })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -395,7 +395,8 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
expect([
|
||||
Math.round(clickAreaBox!.x - treeBox!.x),
|
||||
Math.round(treeBox!.x + treeBox!.width - clickAreaBox!.x - clickAreaBox!.width),
|
||||
]).toEqual([5, 5])
|
||||
// Menu padding alone insets the rows now that the border is gone.
|
||||
]).toEqual([4, 4])
|
||||
await compareOrRefreshGolden(
|
||||
BRANCHLESS_EXPECTED,
|
||||
await captureStableAria(page, '[role="tree"][aria-label="Subagent sessions"]', scaffold.workspaceCwd),
|
||||
|
||||
@@ -18,18 +18,17 @@ export const REPO_ROOT = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
export const ZH_BROWSER_LOCALE = 'zh-CN'
|
||||
|
||||
/**
|
||||
* Open the standard browser-test page with English selected before client
|
||||
* boot. This keeps role locators and goldens deterministic across localized
|
||||
* component migrations; the scenarios asserting the Chinese surface bypass
|
||||
* this helper and advertise {@link ZH_BROWSER_LOCALE} instead.
|
||||
* Open the standard browser-test page advertising English before client boot.
|
||||
* This keeps role locators and goldens deterministic while leaving the Host
|
||||
* settings document free to override the provisional browser-derived locale;
|
||||
* scenarios asserting the Chinese surface advertise
|
||||
* {@link ZH_BROWSER_LOCALE} instead.
|
||||
* @param browser - Playwright browser owning the page.
|
||||
* @param height - Viewport height; width is fixed to the lane baseline.
|
||||
* @returns the initialized page.
|
||||
*/
|
||||
export async function newEnglishPage(browser: Browser, height = 1000): Promise<Page> {
|
||||
const page = await browser.newPage({ viewport: { width: 1680, height } })
|
||||
await page.addInitScript(() => { localStorage.setItem('dsh.locale', 'en') })
|
||||
return page
|
||||
return await browser.newPage({ viewport: { width: 1680, height }, locale: 'en-US' })
|
||||
}
|
||||
|
||||
/** Fail loud on a stale checkout instead of testing yesterday's bundle. */
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"exclude": [
|
||||
"tests/scaffold.ts",
|
||||
"tests/scaffold-hermetic.e2e.ts",
|
||||
"tests/core-web-profile.snapshot.ts",
|
||||
"tests/minimal-preset.snapshot.ts",
|
||||
"tests/live-interactions.e2e.ts",
|
||||
"tests/question-composer.e2e.ts",
|
||||
"tests/approval-composer.e2e.ts",
|
||||
@@ -64,6 +64,7 @@
|
||||
"tests/agent-preset-selection.e2e.ts",
|
||||
"tests/agent-preset-authoring.e2e.ts",
|
||||
"tests/shipped-composition.e2e.ts",
|
||||
"tests/feedback-command.e2e.ts",
|
||||
"tests/startup-auto-selection.e2e.ts",
|
||||
"tests/produced-files.e2e.ts",
|
||||
"tests/produced-file-mentions.e2e.ts",
|
||||
|
||||
Reference in New Issue
Block a user