Merge origin/master into fix/web-sources-scroll
This commit is contained in:
@@ -58,25 +58,34 @@
|
||||
# ── TUI-only rows ───────────────────────────────────────────────────────────
|
||||
|
||||
- insert:
|
||||
# Relational runtime checks over the authoritative event streams; each
|
||||
# companion registers the assertions its own package owns.
|
||||
- id: invariants
|
||||
name: '@deepseek-ai/dsh-invariants'
|
||||
- id: session-invariant
|
||||
name: '@deepseek-ai/dsh-session/invariant'
|
||||
- id: agent-invariant
|
||||
name: '@deepseek-ai/dsh-agent/invariant'
|
||||
- id: scope-invariant
|
||||
name: '@deepseek-ai/dsh-scope/invariant'
|
||||
- id: agent-loop-invariant
|
||||
name: '@deepseek-ai/dsh-agent-loop/invariant'
|
||||
|
||||
# The derived query index behind `/resume`. The launcher provides a unique
|
||||
# process-local path because this SQLite backend has one writer owner; the
|
||||
# project-local fallback applies when no launcher sets the typed slot.
|
||||
- id: session-reference
|
||||
name: '@deepseek-ai/dsh-session-reference'
|
||||
|
||||
# The projection registry plus its durable checkpoint cache (over the same
|
||||
# storage root the web surface uses): `/resume` reads titles from the
|
||||
# zero-I/O checkpoint row or a tail-only cold read instead of scanning
|
||||
# whole logs, and checkpoints written by either surface serve both.
|
||||
- id: session-projection
|
||||
name: '@deepseek-ai/dsh-session-projection'
|
||||
- id: storage
|
||||
name: '@deepseek-ai/dsh-storage'
|
||||
- id: storage-json
|
||||
name: '@deepseek-ai/dsh-storage-json'
|
||||
config:
|
||||
root: !!js dshHomePath('storages')
|
||||
- id: storage-domain
|
||||
name: '@deepseek-ai/dsh-storage-domain'
|
||||
config:
|
||||
backend: json
|
||||
- id: session-projection-cache
|
||||
name: '@deepseek-ai/dsh-session-projection-cache'
|
||||
config:
|
||||
writeEveryEvents: 200
|
||||
writeIntervalMs: 5000
|
||||
|
||||
# Terminal-multiplexer context, mounted only where a terminal exists.
|
||||
- id: tmux-context
|
||||
name: '@deepseek-ai/dsh-tmux-context'
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
"@deepseek-ai/dsh-host-directory-picker-browse": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-directory-picker-native": "workspace:^",
|
||||
"@deepseek-ai/dsh-host-webserver": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm-deepseek": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm-pi-ai": "workspace:^",
|
||||
|
||||
@@ -94,6 +94,8 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
expect(stdout).toContain('model: deepseek-v4-pro')
|
||||
expect(stdout).toContain('cwd: !!js process.cwd()')
|
||||
expect(stdout).toContain("name: '@deepseek-ai/dsh-tui'")
|
||||
expect(stdout).not.toMatch(/name: ['"]@deepseek-ai\/dsh-invariants['"]/)
|
||||
expect(stdout).not.toMatch(/name: ['"]@deepseek-ai\/dsh-[^'"]+\/invariant['"]/)
|
||||
expect(stdout).toContain([
|
||||
'- id: tool-web',
|
||||
" name: '@deepseek-ai/dsh-tool-web'",
|
||||
@@ -140,6 +142,8 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
expect(code).toBe(0)
|
||||
expect(stdout).toContain("name: '@deepseek-ai/dsh-host-webserver'")
|
||||
expect(stdout).not.toContain("name: '@deepseek-ai/dsh-tui'")
|
||||
expect(stdout).not.toMatch(/name: ['"]@deepseek-ai\/dsh-invariants['"]/)
|
||||
expect(stdout).not.toMatch(/name: ['"]@deepseek-ai\/dsh-[^'"]+\/invariant['"]/)
|
||||
}, 30_000)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -52,10 +52,10 @@ const EXPECTED_TUI_TOOLS = [
|
||||
]
|
||||
|
||||
/**
|
||||
* `glob` and `grep` come from `dsh-tool-fs-search`, which probes `command -v rg`
|
||||
* through the mounted bash executor at load and registers neither tool when
|
||||
* ripgrep is absent. That is a host dependency, not a composition decision, so the
|
||||
* pair is asserted separately — present together or absent together.
|
||||
* `glob` and `grep` come from `dsh-tool-fs-search`, which spawns the PACKAGED
|
||||
* ripgrep binary (`@vscode/ripgrep`) through the subprocess seam, so the pair
|
||||
* is always present on every host — asserted as fixed members, not a host
|
||||
* dependency.
|
||||
*/
|
||||
const RIPGREP_TOOLS = ['glob', 'grep']
|
||||
|
||||
@@ -123,7 +123,9 @@ describe('shipped dsh composition (real Loader tree in a PTY)', () => {
|
||||
expect(output).toContain(COMPOSITION_REPLY_TEXT)
|
||||
expect(output).toContain(PERMISSION_SUMMARY)
|
||||
expect(observed?.names.filter(name => !RIPGREP_TOOLS.includes(name))).toEqual(EXPECTED_TUI_TOOLS)
|
||||
expect([[], RIPGREP_TOOLS]).toContainEqual(observed?.names.filter(name => RIPGREP_TOOLS.includes(name)))
|
||||
// The packaged ripgrep binary ships with the dependency, so the pair is a
|
||||
// fixed roster member on every host.
|
||||
expect(observed?.names.filter(name => RIPGREP_TOOLS.includes(name))).toEqual(RIPGREP_TOOLS)
|
||||
expect(observed?.bashArguments).toHaveProperty('sandbox_permissions')
|
||||
expect(observed?.bashArguments).toHaveProperty('justification')
|
||||
expect(observed?.permissionEvents).toEqual([
|
||||
|
||||
53
apps/web/tests/remote-welcome.e2e.ts
Normal file
53
apps/web/tests/remote-welcome.e2e.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// Trusted non-loopback Web access must not wedge on the loopback-only
|
||||
// settings API while the mandatory product notice owns the viewport.
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
acknowledgeReloadConnectionLoss, launchWebScaffold, watchConsole, webSnapshotMode,
|
||||
type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { ZH_BROWSER_LOCALE } from './support.ts'
|
||||
import { WELCOME_NOTICE_COPY } from '@deepseek-ai/dsh-client-ui-settings-general'
|
||||
|
||||
const MODE = webSnapshotMode()
|
||||
|
||||
describe.skipIf(MODE === 'record')('web e2e: remote welcome notice', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({ remoteAuthority: 'remote.localhost', welcomeNoticePending: true })
|
||||
browser = await chromium.launch()
|
||||
page = await browser.newPage({ viewport: { width: 1440, height: 960 }, locale: ZH_BROWSER_LOCALE })
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('#root', { timeout: 30_000 })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('advances process-locally and presents the notice again after reload', async () => {
|
||||
const welcome = page.getByRole('region', { name: WELCOME_NOTICE_COPY.zh.title })
|
||||
await welcome.waitFor({ timeout: 15_000 })
|
||||
expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(true)
|
||||
|
||||
await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click()
|
||||
await welcome.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
await expect.poll(
|
||||
() => page.locator('#root').evaluate(root => (root as HTMLElement).inert),
|
||||
{ timeout: 15_000 },
|
||||
).toBe(false)
|
||||
const reloadWarnings = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
acknowledgeReloadConnectionLoss(tripwire, reloadWarnings)
|
||||
await welcome.waitFor({ timeout: 15_000 })
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
})
|
||||
@@ -89,7 +89,7 @@ const REPLAY_PROVIDERS = [{
|
||||
export interface WebScaffold {
|
||||
/** The active snapshot mode this scaffold booted under. */
|
||||
mode: WebSnapshotMode
|
||||
/** Browser-facing origin (http://127.0.0.1:<bound port>). */
|
||||
/** Browser-facing origin for the bound test server. */
|
||||
baseUrl: string
|
||||
/** Settled root context (the in-process barrier seam; headless event subscription is its sanctioned use). */
|
||||
ctx: Context
|
||||
@@ -166,6 +166,12 @@ export interface LaunchOptions {
|
||||
}
|
||||
/** Leave the current welcome notice unacknowledged; ordinary scenarios publish it as complete before browser boot. */
|
||||
welcomeNoticePending?: boolean
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
/** Dispose the booted tree and remove both owned temp roots, reporting every independent cleanup failure. */
|
||||
@@ -185,6 +191,7 @@ async function cleanupScaffoldWorld(ctx: Context, workspaceCwd: string, persiste
|
||||
export async function launchWebScaffold(options: LaunchOptions = {}): Promise<WebScaffold> {
|
||||
requireDist()
|
||||
const mode = webSnapshotMode()
|
||||
const browserHost = options.remoteAuthority ?? '127.0.0.1'
|
||||
if (mode === 'record') {
|
||||
// Both owning vitest configs (web unconditionally, snapshot in record
|
||||
// mode) load the repo-root .env before this file runs.
|
||||
@@ -261,7 +268,13 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
// to the production OTLP endpoint (or whatever DSH_TELEMETRY_OTLP_URL
|
||||
// names in the ambient environment).
|
||||
{ id: 'telemetry-otel', disabled: true },
|
||||
{ id: 'webserver', config: { host: '127.0.0.1', port: 0, distIndex: DIST_INDEX } },
|
||||
{
|
||||
id: 'webserver',
|
||||
config: { host: '127.0.0.1', port: 0, distIndex: DIST_INDEX },
|
||||
},
|
||||
...options.remoteAuthority === undefined
|
||||
? []
|
||||
: [{ id: 'connection', config: { trustedHosts: [options.remoteAuthority] } }],
|
||||
{ id: 'settings', config: { dshHome: harnessHome } },
|
||||
{ id: 'credentials', config: { dshHome: harnessHome } },
|
||||
// The shipped directory-picker row is the -auto chooser, which resolves
|
||||
@@ -352,7 +365,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
return {
|
||||
harnessHome,
|
||||
mode,
|
||||
baseUrl: `http://127.0.0.1:${port}`,
|
||||
baseUrl: `http://${browserHost}:${port}`,
|
||||
ctx,
|
||||
workspaceCwd,
|
||||
persistenceRoot,
|
||||
|
||||
@@ -47,10 +47,10 @@ const EXPECTED_TOOLS = [
|
||||
]
|
||||
|
||||
/**
|
||||
* `glob` and `grep` come from `dsh-tool-fs-search`, which probes `command -v rg`
|
||||
* through the mounted bash executor at load and registers neither tool when
|
||||
* ripgrep is absent. That is a host dependency, not a composition decision, so the
|
||||
* pair is asserted separately — present together or absent together.
|
||||
* `glob` and `grep` come from `dsh-tool-fs-search`, which spawns the PACKAGED
|
||||
* ripgrep binary (`@vscode/ripgrep`) through the subprocess seam, so the pair
|
||||
* is always present on every host — asserted as fixed members, not a host
|
||||
* dependency.
|
||||
*/
|
||||
const RIPGREP_TOOLS = ['glob', 'grep']
|
||||
|
||||
@@ -65,7 +65,9 @@ it('assembles the shipped Web catalog with the confined access default', async (
|
||||
scaffold = await launchWebScaffold()
|
||||
const names = scaffold.ctx.tools.schemas().map(schema => schema.name).sort()
|
||||
expect(names.filter(name => !RIPGREP_TOOLS.includes(name))).toEqual(EXPECTED_TOOLS)
|
||||
expect([[], RIPGREP_TOOLS]).toContainEqual(names.filter(name => RIPGREP_TOOLS.includes(name)))
|
||||
// The packaged ripgrep binary ships with the dependency, so the pair is a
|
||||
// fixed roster member on every host.
|
||||
expect(names.filter(name => RIPGREP_TOOLS.includes(name))).toEqual(RIPGREP_TOOLS)
|
||||
// `workspace-write` is not "the workspace and nothing else": the shared roots
|
||||
// helper always admits the temp directories too. Pinning it against an
|
||||
// explicit mode keeps the claim independent of this surface's default, and
|
||||
|
||||
@@ -400,6 +400,17 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
|
||||
const tree = page.getByRole('tree', { name: 'Subagent sessions' })
|
||||
const nestedRow = tree.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) })
|
||||
expect(await nestedRow.locator(':scope > *').count()).toBe(1)
|
||||
const clickArea = nestedRow.locator(':scope > *')
|
||||
const [treeBox, clickAreaBox] = await Promise.all([
|
||||
tree.boundingBox(),
|
||||
clickArea.boundingBox(),
|
||||
])
|
||||
expect(treeBox).not.toBeNull()
|
||||
expect(clickAreaBox).not.toBeNull()
|
||||
expect([
|
||||
Math.round(clickAreaBox!.x - treeBox!.x),
|
||||
Math.round(treeBox!.x + treeBox!.width - clickAreaBox!.x - clickAreaBox!.width),
|
||||
]).toEqual([5, 5])
|
||||
await compareOrRefreshGolden(
|
||||
BRANCHLESS_EXPECTED,
|
||||
await captureStableAria(page, '[role="tree"][aria-label="Subagent sessions"]', scaffold.workspaceCwd),
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"tests/settings-chrome.e2e.ts",
|
||||
"tests/models-settings.e2e.ts",
|
||||
"tests/onboarding-deepseek-config.e2e.ts",
|
||||
"tests/remote-welcome.e2e.ts",
|
||||
"tests/workspace-management.e2e.ts",
|
||||
"tests/replay-round-trip.e2e.ts",
|
||||
"tests/hmr-live.e2e.ts",
|
||||
|
||||
Reference in New Issue
Block a user