Merge remote-tracking branch 'origin/master' into worktree/turn-tail-actions

This commit is contained in:
creatixchu
2026-08-07 10:50:49 +08:00
378 changed files with 15733 additions and 3622 deletions

View File

@@ -3,21 +3,27 @@ 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 } from '@deepseek-ai/dsh-llm'
import { CallId, createUserMessage } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import { launchWebScaffold, type WebScaffold } from './scaffold.ts'
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 () => {
scaffold = await launchWebScaffold({
extraOverlayPath: CORE_WEB_OVERLAY,
toolsMode: 'native',
})
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 },
@@ -33,7 +39,16 @@ describe('core Web profile', () => {
if (failures.length > 1) throw new AggregateError(failures, 'core Web profile smoke teardown failed')
})
it('boots and executes both tools through the shipped Web composition', async () => {
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
@@ -60,7 +75,8 @@ describe('core Web profile', () => {
.trimEnd()
expect({
tools: scaffold.ctx.tools.schemas().map(tool => tool.name),
prompt: requestHeader.system,
tools: requestHeader.tools?.map(tool => tool.name),
bash: text(bash),
editor: text(editor),
}).toMatchInlineSnapshot(`
@@ -69,16 +85,53 @@ describe('core Web profile', () => {
"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
}
}
}
})
})

View File

@@ -161,6 +161,58 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
it('never paints the takeover chrome on a configured reload, even with the settings join held open', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-configured-reload'))
// Regression pin for the reload white flash: both steps are satisfied
// (welcome acknowledged, credential configured), yet each must LOAD its
// private join before it can decide not to show. The chrome lives inside
// the step (OnboardingSurface), so the deciding window paints and blocks
// nothing. Holding settings.describe widens that window from loopback
// RTT scale to a deterministic hundreds of milliseconds, removing all
// timing dependence from the sampler assertions below.
//
// The sampler init script persists across this shared page's later
// navigations (init scripts re-run per navigation); that stays harmless
// because no later scenario in this file legitimately shows the
// takeover, and only this test reads __takeoverSightings.
await page.addInitScript(() => {
const sightings: string[] = []
;(window as unknown as { __takeoverSightings: string[] }).__takeoverSightings = sightings
setInterval(() => {
if (document.querySelector('[class*="onboardingStage"], [class*="onboardingMask"]') !== null) {
sightings.push('chrome')
}
if (document.getElementById('root')?.inert === true) sightings.push('inert')
}, 8)
})
// EVERY settings.describe issued before the release is held — not just
// the first — so the pin cannot silently collapse back to loopback
// timing if a second boot-time consumer of the join ever appears.
let released = false
const heldRoutes: Array<() => void> = []
const releaseDescribe = (): void => {
released = true
for (const resolve of heldRoutes.splice(0)) resolve()
}
await page.route('**/api/settings.describe', async (route) => {
if (!released) await new Promise<void>((resolve) => { heldRoutes.push(resolve) })
await route.continue()
})
const warningsBefore = tripwire.warnings.length
await page.reload({ waitUntil: 'commit' })
await page.waitForSelector('[class*="frame"]', { timeout: 15_000 })
// The app is painted and interactive while the steps are still deciding.
await page.waitForTimeout(600)
releaseDescribe()
await page.waitForTimeout(400)
await page.unroute('**/api/settings.describe')
acknowledgeReloadConnectionLoss(tripwire, warningsBefore)
expect(await page.evaluate(() =>
(window as unknown as { __takeoverSightings: string[] }).__takeoverSightings)).toEqual([])
expect(await page.locator('[class*="onboardingStage"]').count()).toBe(0)
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
it('configures arbitrary DeepSeek models and prompts after the selected model is removed', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-deepseek-models'))
// Opened here rather than inherited: the credential test reloads the page

View File

@@ -1,4 +1,4 @@
# Loader overlay for the W5 real-host smoke (`dsh web --config`): pin the
# Loader overlay for the W5 real-host smoke (`dsh web --patch`): pin the
# in-browser directory picker. The shipped row is `-auto`, which resolves to
# the native OS chooser on a loopback bind with a local display — an
# interaction a Playwright page cannot drive, so the resolved backend would

View File

@@ -1,7 +1,8 @@
// Shared scaffold for the keyless browser e2e lane (Agent Note:
// .agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md).
// Boots the REAL web composition — the shipped base plus web overlay through
// the vendored Loader (the same include boot AppCLIEntry drives), patched the
// Boots the REAL web composition — the dsh-base and dsh-web-app bundle
// patches over the empty profile root through the vendored Loader (the same
// layer stack the profile boot composes), patched the
// snapshot way — so a real chromium exercises the real HTTP uplink/WebSocket
// downlink, api-gateway, agent loop, tools, and persistence. Modes ride $DSH_SNAPSHOT:
// replay (default, keyless: normally disables the llm-deepseek row and
@@ -11,7 +12,7 @@
// masking its credential, without making a model call.
//
// Composition divergences from `dsh web`, all deliberate, all via include
// patches after the shipped surface overlay, over the SAME tree (never a
// patches after the shipped bundle layers, over the SAME tree (never a
// second yml): temp persistenceRoot; host-level skill roots confined to the
// temp workspace while project skill discovery remains real; workspace-context
// disabled (recorded fixtures must not embed this repo's AGENTS.md);
@@ -22,9 +23,9 @@
// (the plugin-row path discards the ReplayHandle; the direct install keeps
// assertConsumed for the teardown fixture-consumption check).
import { existsSync } from 'node:fs'
import { mkdtemp, readFile, readdir, realpath, rm, utimes, writeFile } from 'node:fs/promises'
import { mkdir, mkdtemp, readFile, readdir, realpath, rm, utimes, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import type { Page } from 'playwright'
import { expect } from 'vitest'
@@ -32,7 +33,13 @@ import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import Include, { type PatchOptions } from '@cordisjs/plugin-include'
import { scrubRequestHeaders } from '@deepseek-ai/dsh-acp-snapshot'
import { assertEntriesLoaded, loadOverlayPatches } from '@deepseek-ai/dsh-app-boot'
import {
addHarnessSourceSection,
assertEntriesLoaded,
composeEntries,
healProfilesModuleFallback,
loadOverlayPatches,
} from '@deepseek-ai/dsh-app-boot'
import { dshHomePath } from '@deepseek-ai/dsh-paths'
import {
WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_VERSION,
@@ -53,8 +60,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 { prepareWebRuntimeContext } from '../../cli/src/web.ts'
import { DIST_INDEX, REPO_ROOT, requireDist } from './support.ts'
import { REPO_ROOT, requireDist } from './support.ts'
/** Snapshot mode for the lane, from $DSH_SNAPSHOT (same vocabulary as the other snapshot suites). */
export type WebSnapshotMode = 'replay' | 'record' | 'refresh'
@@ -70,9 +76,11 @@ export function webSnapshotMode(): WebSnapshotMode {
throw new Error(`DSH_SNAPSHOT must be replay, record, or refresh; got ${JSON.stringify(value)}`)
}
/** The shipped composition under test: apps/cli's shared base and web overlay. */
const CONFIG_PATH = join(REPO_ROOT, 'apps/cli/config/base.cordis.yml')
const WEB_OVERLAY_PATH = join(REPO_ROOT, 'apps/cli/config/web.cordis.yml')
/** The shipped composition under test: the dsh-base and dsh-web-app bundle patches over the empty profile root. */
const BASE_PATCH_PATH = join(REPO_ROOT, 'packages/bundle/base/cordis.patch.yml')
const WEB_PATCH_PATH = join(REPO_ROOT, 'packages/bundle/web-app/cordis.patch.yml')
/** The installation anchor whose dependency surface the profile module fallback mirrors. */
const INSTALL_ANCHOR = join(REPO_ROOT, 'apps/cli/package.json')
// Replay publishes the provider catalog the gateway routes to (providers
// mode, never catch-all: with llm-deepseek disabled no adapter exists, so a
@@ -117,7 +125,7 @@ export interface WebScaffold {
export interface LaunchOptions {
/**
* Optional product overlay applied after the shipped Web surface and before
* the scaffold's hermetic test patches, matching AppCLIEntry's `--config`
* the scaffold's hermetic test patches, matching the launcher's `--patch`
* ordering.
*/
extraOverlayPath?: string
@@ -240,14 +248,22 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
}
if (maskDeepSeekCredential) Reflect.deleteProperty(process.env, 'DEEPSEEK_API_KEY')
// The include patch set — the same mechanism AppCLIEntry and the ACP
// snapshot overlay use, applied over the SAME shipped tree (a patch id that
// stops matching a row fails the boot sweep loudly instead of drifting).
const surfacePatches = loadOverlayPatches('web e2e scaffold', WEB_OVERLAY_PATH)
// The include patch set — the same layer stack the profile boot composes
// (bundle patches in dsh.profile.bundles order), applied over the SAME empty root (a
// patch id that stops matching a row fails the boot sweep loudly instead of
// drifting).
const basePatches = loadOverlayPatches('web e2e scaffold', BASE_PATCH_PATH)
const surfacePatches = loadOverlayPatches('web e2e scaffold', WEB_PATCH_PATH)
const extraOverlayPatches = options.extraOverlayPath === undefined
? []
: loadOverlayPatches('web e2e scaffold', options.extraOverlayPath)
const composedRows = composeEntries([basePatches, surfacePatches, extraOverlayPatches])
const webRuntimeConfig = composedRows.find(row => row.id === 'web-runtime')?.config as {
surfaceContext?: boolean
} | undefined
const surfaceContext = webRuntimeConfig?.surfaceContext !== false
const patches: PatchOptions[] = [
...basePatches,
...surfacePatches,
...extraOverlayPatches,
{ id: 'session-persistence-jsonl', config: { root: persistenceRoot } },
@@ -280,8 +296,13 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
{ id: 'telemetry-otel', disabled: true },
{
id: 'webserver',
config: { host: '127.0.0.1', port: 0, distIndex: DIST_INDEX },
config: { host: '127.0.0.1', port: 0 },
},
// The bundle's web-runtime row resolves the same built dist under test
// (apps/web IS @deepseek-ai/dsh-frontend); only the URL line is silenced.
// Preserve the composed surface-context choice because a patch replaces
// the row's complete config.
{ id: 'web-runtime', config: { mode: 'production', printUrl: false, surfaceContext } },
...options.remoteAuthority === undefined
? []
: [{ id: 'connection', config: { trustedHosts: [options.remoteAuthority] } }],
@@ -321,7 +342,15 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
let replayHandle: ReplayHandle | undefined
try {
process.chdir(workspaceCwd)
ctx.baseUrl = pathToFileURL(join(resolve(CONFIG_PATH), '..')).href + '/'
// The production resolution shape: 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)
const profileDir = join(harnessHome, 'profiles', 'scaffold')
await mkdir(profileDir, { recursive: true })
const rootConfig = join(profileDir, 'cordis.yml')
await writeFile(rootConfig, '[]\n')
ctx.baseUrl = pathToFileURL(profileDir).href + '/'
// This direct Loader harness supplies the same root-path capability as app-boot.
ctx.provide('dshHomePath', dshHomePath)
await ctx.plugin(Loader)
@@ -329,10 +358,12 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
// 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
prepareWebRuntimeContext(ctx, REPO_ROOT, 'production')
if (surfaceContext) {
ctx.inject(['systemPrompt'], (promptCtx) => { addHarnessSourceSection(promptCtx, REPO_ROOT) })
}
await ctx.loader.create({
name: 'cordis:include',
config: { path: pathToFileURL(resolve(CONFIG_PATH)).href, patches },
config: { path: pathToFileURL(rootConfig).href, patches },
})
await ctx.loader.await()
assertEntriesLoaded(ctx, 'web e2e scaffold')

View File

@@ -487,7 +487,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
'--import', tsxLoader, join(REPO_ROOT, 'apps/cli/src/bin.ts'), 'web', '--port', String(port),
// 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.
'--config', fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)),
'--patch', fileURLToPath(new URL('./pin-browse-picker.overlay.yml', import.meta.url)),
],
{
cwd: sessionsDir,

View File

@@ -0,0 +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":"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":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"}}}}