Merge commit '2f717c0467d7174d7fe0011c4a787450f7328a78' into codex/product-subagent-one-shot-background
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh",
|
||||
"description": "dsh CLI: profile boot, plugin management, and the browser UI alias",
|
||||
"version": "0.0.1-rc.5",
|
||||
"version": "0.1.0-rc.3",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write apps/cli/reference/README.md
|
||||
README.md: 0b23db293c304807b5959f5eae9a2faf868caa62
|
||||
README.zh.md: d812c7cd0d1efaa69b135925f3e922668539781b
|
||||
README.md: 0be64fdfc0ad4e81d23f25a26881fa89f37565b0
|
||||
README.zh.md: 649bf15df814abf4875fed794c2e76c494bfcc25
|
||||
|
||||
@@ -61,7 +61,7 @@ dsh web --dump-config
|
||||
dsh web --help
|
||||
```
|
||||
|
||||
The production Web runner needs built package and frontend artifacts (`pnpm run build`). It serves `http://127.0.0.1:3080` by default. Binding all interfaces also trusts the machine's discovered LAN IP literals; `--trusted-host` adds named authorities accepted by the `/api` browser-trust fence.
|
||||
The production Web runner needs built package and frontend artifacts (`pnpm run build`). It serves `http://127.0.0.1:3080` by default. The CLI intentionally does not support `--host 0.0.0.0` yet and exits with a usage error; `--trusted-host` adds named authorities accepted by the `/api` browser-trust fence.
|
||||
|
||||
Process shutdown gives the plugin tree up to five seconds to dispose. The first `SIGINT`/`SIGTERM` starts that graceful drain — `SIGTERM` is a supervisor's ordinary stop request and exits 0 on every surface, `SIGINT` reports 130; a second signal forces immediate exit. If one-shot normal completion is already stuck in disposal, the first `Ctrl+C` is the escalation and exits immediately instead of being swallowed.
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ dsh web --dump-config
|
||||
dsh web --help
|
||||
```
|
||||
|
||||
生产 Web 运行器需要已构建的包和前端产物(`pnpm run build`)。默认服务地址是 `http://127.0.0.1:3080`。绑定所有网络接口时,还会信任机器自动发现的 LAN IP 字面量;`--trusted-host` 可添加 `/api` 浏览器信任围栏接受的具名 authority。
|
||||
生产 Web 运行器需要已构建的包和前端产物(`pnpm run build`)。默认服务地址是 `http://127.0.0.1:3080`。CLI 目前有意不支持 `--host 0.0.0.0`,并会以用法错误退出;`--trusted-host` 可添加 `/api` 浏览器信任围栏接受的具名 authority。
|
||||
|
||||
进程关闭时,插件树最多有 5 秒完成 dispose。首次收到 `SIGINT` 或 `SIGTERM` 时会开始优雅排空:`SIGTERM` 是监督进程发出的常规停止请求,在所有运行模式下都以 0 退出;`SIGINT` 则报告 130。第二次收到信号时会立即强制退出。如果一次性运行在正常结束时已经卡在 dispose 阶段,第一次按下 `Ctrl+C` 就会直接升级为强制退出,而不会被忽略。
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ describe('parseDshArgs', () => {
|
||||
.toEqual({ mode: 'profile', profile: 'tui', patches: [], args: ['--resume', 'abc'] })
|
||||
expect(parse(['--profile', 'web', '-h']))
|
||||
.toEqual({ mode: 'profile', profile: 'web', patches: [], args: ['-h'] })
|
||||
expect(parse(['web', '--host', '0.0.0.0', '--port', '8080', '--dev']))
|
||||
.toEqual({ mode: 'profile', profile: 'web', patches: [], args: ['--host', '0.0.0.0', '--port', '8080', '--dev'] })
|
||||
expect(parse(['web', '--host', '127.0.0.1', '--port', '8080', '--dev']))
|
||||
.toEqual({ mode: 'profile', profile: 'web', patches: [], args: ['--host', '127.0.0.1', '--port', '8080', '--dev'] })
|
||||
expect(parse(['--profile', 'headless', 'run', 'the', 'tests']))
|
||||
.toEqual({ mode: 'profile', profile: 'headless', patches: [], args: ['run', 'the', 'tests'] })
|
||||
// Launcher flags placed after that boundary belong to the app too.
|
||||
|
||||
@@ -339,6 +339,15 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
|
||||
expect(web.stdout).toContain('--port <port>')
|
||||
expect(web.stdout).not.toContain('dsh web: http://')
|
||||
|
||||
const wildcardHost = await runBuiltBin(['web', '--host', '0.0.0.0'], {
|
||||
DSH_HOME: home,
|
||||
DSH_TELEMETRY_DISABLED: '1',
|
||||
})
|
||||
expect(wildcardHost.code).toBe(1)
|
||||
expect(wildcardHost.stdout).toBe('')
|
||||
expect(wildcardHost.stderr).toContain('--host 0.0.0.0 is intentionally not supported yet for safety: it would expose remote code execution to the network; use 127.0.0.1 instead')
|
||||
expect(wildcardHost.stderr).not.toContain('dsh web: http://')
|
||||
|
||||
const headlessHelp = await runBuiltBin(['--profile', 'headless', '--help'], {
|
||||
DSH_HOME: home,
|
||||
DSH_TELEMETRY_DISABLED: '1',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-web-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-rc.5",
|
||||
"version": "0.1.0-rc.3",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write apps/web/tests/README.md
|
||||
README.md: 4d7236ef69189845dbc2847289d497fc54c94ae9
|
||||
README.zh.md: 21c2f2ce7ccc8fe5314d766c06235453aa292031
|
||||
README.md: acb0c300bafe221f6a92f0168908bebf965377b9
|
||||
README.zh.md: fd3d950a106375bb038b205415407b4a31bd2b32
|
||||
|
||||
@@ -30,7 +30,8 @@ artifact it produces itself.
|
||||
When a scenario needs a Client-owned constant or pure function, mirror it here
|
||||
instead, next to the commented-out import that names the source module. A drift
|
||||
then surfaces as a missed selector or a stale mirrored value — a loud failure,
|
||||
never a silent pass.
|
||||
never a silent pass. `scaffold.ts` follows this rule for the welcome-notice
|
||||
namespace, acknowledgement field, version, and asserted Chinese copy.
|
||||
|
||||
Two kinds of Client import stand. `assembled-boot.ts` drives the shell itself, so
|
||||
it imports `AppWebEntry` from `@deepseek-ai/dsh-client-web` and the boot-manifest
|
||||
|
||||
@@ -24,7 +24,7 @@ Client face,而该 face 必须等 Host tsdown 生成 `@deepseek-ai/dsh-goal/re
|
||||
|
||||
当某个场景需要 Client 持有的常量或纯函数时,改为在此处镜像一份,并紧挨着一条注释掉的
|
||||
import 点明源模块。这样漂移会表现为选择器未命中或镜像值过期——是响亮的失败,绝不会是静默
|
||||
通过。
|
||||
通过。`scaffold.ts` 按此规则镜像欢迎声明的 namespace、确认字段、版本和被断言的中文文案。
|
||||
|
||||
有两类 Client import 是长期成立的。`assembled-boot.ts` 驱动 shell 本身,因此它从
|
||||
`@deepseek-ai/dsh-client-web` import `AppWebEntry`、从
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('web e2e: goal bar clear convergence', () => {
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY })
|
||||
scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY, welcomeNoticePending: true })
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Keyless browser e2e: the shipped DeepSeek adapter stays mounted while its
|
||||
// credential is absent, onboarding routes to the real Models editor, and its
|
||||
// write lands in an isolated harness home without a reload or model call.
|
||||
// credential is absent, both ordered steps share the shipped modal chrome,
|
||||
// and the inline key write lands in an isolated harness home without a reload
|
||||
// or model call.
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@@ -8,13 +9,17 @@ 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 { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import {
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_COPY, WELCOME_NOTICE_SETTINGS_NAMESPACE,
|
||||
WELCOME_NOTICE_VERSION,
|
||||
} from './scaffold.ts'
|
||||
import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/onboarding-deepseek-config', import.meta.url))
|
||||
const WELCOME_EXPECTED = join(SNAPSHOT_DIR, 'welcome.expected.md')
|
||||
const MISSING_EXPECTED = join(SNAPSHOT_DIR, 'missing.expected.md')
|
||||
const MODELS_EXPECTED = join(SNAPSHOT_DIR, 'models.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
@@ -27,7 +32,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
const browserConsole: string[] = []
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({ deepSeekMissingCredential: true })
|
||||
scaffold = await launchWebScaffold({ deepSeekMissingCredential: true, welcomeNoticePending: true })
|
||||
browser = await chromium.launch()
|
||||
// The scenario asserts the shipped Chinese copy, so the browser asks for it.
|
||||
page = await browser.newPage({ viewport: { width: 1440, height: 960 }, locale: ZH_BROWSER_LOCALE })
|
||||
@@ -44,53 +49,40 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
|
||||
it('stores a key write-only and observes configured state without restarting', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-deepseek-config'))
|
||||
const credentialStep = page.getByRole('region', { name: '添加一个 API Key 开始使用' })
|
||||
await credentialStep.waitFor({ timeout: 15_000 })
|
||||
const welcome = page.getByRole('dialog', { 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)
|
||||
for (const paragraph of WELCOME_NOTICE_COPY.zh.body.split('\n\n')) {
|
||||
expect(await welcome.getByText(paragraph, { exact: true }).count()).toBe(1)
|
||||
}
|
||||
expect(await welcome.getByRole('button').allTextContents()).toEqual([
|
||||
WELCOME_NOTICE_COPY.zh.continueLabel,
|
||||
])
|
||||
const welcomeAria = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(WELCOME_EXPECTED, welcomeAria, MODE)
|
||||
|
||||
const mask = page.locator('[class*="onboardingMask"]')
|
||||
expect(await mask.count()).toBe(1)
|
||||
const maskStyles = await mask.evaluate((mask) => {
|
||||
const style = getComputedStyle(mask)
|
||||
const rect = mask.getBoundingClientRect()
|
||||
return {
|
||||
position: style.position,
|
||||
left: style.left,
|
||||
right: style.right,
|
||||
top: style.top,
|
||||
bottom: style.bottom,
|
||||
background: style.backgroundColor,
|
||||
backdropFilter: style.backdropFilter,
|
||||
rect: { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom },
|
||||
}
|
||||
})
|
||||
expect(maskStyles).toEqual({
|
||||
position: 'absolute',
|
||||
left: '0px',
|
||||
right: '0px',
|
||||
top: '80px',
|
||||
bottom: '0px',
|
||||
background: 'rgba(0, 0, 0, 0.24)',
|
||||
backdropFilter: 'blur(2px)',
|
||||
rect: { left: 0, top: 80, right: 1440, bottom: 960 },
|
||||
})
|
||||
// Observation is not acknowledgement: the exact version is persisted
|
||||
// only by the explicit action, so a reload still presents this dialog.
|
||||
const firstReloadWarnings = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
acknowledgeReloadConnectionLoss(tripwire, firstReloadWarnings)
|
||||
await welcome.waitFor({ timeout: 15_000 })
|
||||
|
||||
expect(await credentialStep.getByRole('textbox').count()).toBe(0)
|
||||
const initial = await captureStableAria(page, '[role="region"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(MISSING_EXPECTED, initial, MODE)
|
||||
await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click()
|
||||
await welcome.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
|
||||
await credentialStep.getByRole('button', { name: '前往配置' }).click()
|
||||
await credentialStep.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
const settings = page.getByRole('dialog', { name: '设置' })
|
||||
await settings.waitFor({ timeout: 10_000 })
|
||||
expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(false)
|
||||
const keyInput = settings.getByLabel('API 密钥', { exact: true })
|
||||
const credentialStep = page.getByRole('dialog', { name: '添加一个 API Key 开始使用' })
|
||||
await credentialStep.waitFor({ timeout: 15_000 })
|
||||
const keyInput = credentialStep.getByLabel('API 密钥', { exact: true })
|
||||
await keyInput.waitFor({ timeout: 10_000 })
|
||||
const initial = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(MISSING_EXPECTED, initial, MODE)
|
||||
|
||||
const secret = `dsh_onboarding_${randomBytes(12).toString('hex')}`
|
||||
await keyInput.fill(secret)
|
||||
await settings.getByRole('button', { name: '保存', exact: true }).click()
|
||||
await keyInput.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
await credentialStep.getByRole('button', { name: '保存并继续' }).click()
|
||||
await credentialStep.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(false)
|
||||
|
||||
const stored = await readFile(join(scaffold.harnessHome, '.credentials.yaml'), 'utf8')
|
||||
expect(stored.includes(`DEEPSEEK_API_KEY: ${secret}`)).toBe(true)
|
||||
@@ -98,8 +90,15 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
expect((await page.locator('body').ariaSnapshot()).includes(secret)).toBe(false)
|
||||
expect(browserConsole.some(line => line.includes(secret))).toBe(false)
|
||||
|
||||
// The same open Models surface reuses the refreshed join and exposes the
|
||||
const acknowledgedSettings = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
|
||||
expect(acknowledgedSettings).toContain(`${WELCOME_NOTICE_ACK_FIELD}: ${WELCOME_NOTICE_VERSION}`)
|
||||
|
||||
// The ordinary Models surface reuses the refreshed join and exposes the
|
||||
// configured write-only placeholder without a reload.
|
||||
await page.getByRole('button', { name: '设置', exact: true }).click()
|
||||
const settings = page.getByRole('dialog', { name: '设置' })
|
||||
await settings.waitFor({ timeout: 10_000 })
|
||||
await settings.getByRole('button', { name: '模型' }).click()
|
||||
const deepSeekRow = settings.getByText('DeepSeek', { exact: true }).first()
|
||||
await deepSeekRow.waitFor({ timeout: 10_000 })
|
||||
await deepSeekRow.locator('xpath=ancestor::li').getByRole('button', { name: '编辑' }).click()
|
||||
@@ -114,7 +113,21 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
acknowledgeReloadConnectionLoss(tripwire, secondReloadWarnings)
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 15_000 })
|
||||
expect(await page.getByRole('region', { name: '添加一个 API Key 开始使用' }).count()).toBe(0)
|
||||
expect(await page.getByRole('dialog', { name: WELCOME_NOTICE_COPY.zh.title }).count()).toBe(0)
|
||||
expect(await page.getByRole('dialog', { name: '添加一个 API Key 开始使用' }).count()).toBe(0)
|
||||
|
||||
// An old acknowledgement means materially revised copy: welcome returns,
|
||||
// while the already-configured provider step remains complete.
|
||||
await scaffold.ctx.settings.mutate(settingsNamespace(WELCOME_NOTICE_SETTINGS_NAMESPACE), [{
|
||||
op: 'set', path: [WELCOME_NOTICE_ACK_FIELD], value: 'previous-copy-version',
|
||||
}])
|
||||
const thirdReloadWarnings = tripwire.warnings.length
|
||||
await page.reload({ waitUntil: 'load' })
|
||||
acknowledgeReloadConnectionLoss(tripwire, thirdReloadWarnings)
|
||||
await welcome.waitFor({ timeout: 15_000 })
|
||||
await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click()
|
||||
await welcome.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
expect(await page.getByRole('dialog', { name: '添加一个 API Key 开始使用' }).count()).toBe(0)
|
||||
|
||||
expect((await page.content()).includes(secret)).toBe(false)
|
||||
expect((await page.locator('body').ariaSnapshot()).includes(secret)).toBe(false)
|
||||
@@ -125,10 +138,9 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
|
||||
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: the credential step is
|
||||
// satisfied (credential configured), yet it 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
|
||||
// Regression pin for the reload flash: both steps are satisfied, yet each
|
||||
// must load private facts before deciding not to show. Dialog chrome lives
|
||||
// inside each visible branch, 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.
|
||||
@@ -141,7 +153,10 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
const sightings: string[] = []
|
||||
;(window as unknown as { __takeoverSightings: string[] }).__takeoverSightings = sightings
|
||||
setInterval(() => {
|
||||
if (document.querySelector('[class*="onboardingStage"], [class*="onboardingMask"]') !== null) {
|
||||
if (document.querySelector(
|
||||
'[role="dialog"][aria-label="内测声明"], '
|
||||
+ '[role="dialog"][aria-label="添加一个 API Key 开始使用"]',
|
||||
) !== null) {
|
||||
sightings.push('chrome')
|
||||
}
|
||||
if (document.getElementById('root')?.inert === true) sightings.push('inert')
|
||||
@@ -171,7 +186,8 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
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(await page.getByRole('dialog', { name: WELCOME_NOTICE_COPY.zh.title }).count()).toBe(0)
|
||||
expect(await page.getByRole('dialog', { name: '添加一个 API Key 开始使用' }).count()).toBe(0)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
@@ -228,7 +244,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
|
||||
it('keeps the fixture inventory closed', async () => {
|
||||
await assertFixtureInventory(
|
||||
SNAPSHOT_DIR,
|
||||
['missing.expected.md', 'models.expected.md'],
|
||||
['welcome.expected.md', 'missing.expected.md', 'models.expected.md'],
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -44,14 +44,17 @@ describe.skipIf(MODE === 'record')('web e2e: another usable provider ends first-
|
||||
|
||||
it('closes the setup card without discarding the add card beside it', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-setup-card-cancel'))
|
||||
const credentialStep = page.getByRole('region', { name: CREDENTIAL_STEP })
|
||||
const credentialStep = page.getByRole('dialog', { name: CREDENTIAL_STEP })
|
||||
await credentialStep.waitFor({ timeout: 15_000 })
|
||||
await credentialStep.getByRole('button', { name: '前往配置' }).click()
|
||||
await credentialStep.getByRole('button', { name: '稍后配置' }).click()
|
||||
await credentialStep.waitFor({ state: 'detached', timeout: 15_000 })
|
||||
|
||||
await page.getByRole('button', { name: '设置', exact: true }).click()
|
||||
const settings = page.getByRole('dialog', { name: '设置' })
|
||||
await settings.waitFor({ timeout: 10_000 })
|
||||
// Nothing is reachable yet, so DeepSeek presents itself as its open card.
|
||||
// The onboarding step no longer navigates into Settings on dismissal, so
|
||||
// enter the Models section explicitly before exercising its normal cards.
|
||||
await settings.getByRole('button', { name: '模型' }).click()
|
||||
const setupKey = settings.getByRole('textbox', { name: 'API 密钥', exact: true })
|
||||
await setupKey.waitFor({ timeout: 10_000 })
|
||||
|
||||
@@ -66,15 +69,14 @@ describe.skipIf(MODE === 'record')('web e2e: another usable provider ends first-
|
||||
{ timeout: 10_000 },
|
||||
).toBe(2)
|
||||
|
||||
// Cancelling the setup card is the regression: it used to leave itself open
|
||||
// and close the add card, discarding that draft.
|
||||
// Cancelling the setup card must not close the independent add-provider
|
||||
// draft beside it.
|
||||
await settings.getByRole('button', { name: '取消', exact: true }).first().click()
|
||||
expect(await settings.getByLabel('提供方').count()).toBe(1)
|
||||
await expect.poll(
|
||||
async () => settings.getByRole('textbox', { name: 'API 密钥', exact: true }).count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(1)
|
||||
// DeepSeek is now an ordinary row: a missing-key dot and an Edit button.
|
||||
await settings.getByRole('button', { name: '编辑 DeepSeek (deepseek-official)' }).waitFor({ timeout: 10_000 })
|
||||
const dismissed = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(DISMISSED_EXPECTED, dismissed, MODE)
|
||||
@@ -104,10 +106,9 @@ describe.skipIf(MODE === 'record')('web e2e: another usable provider ends first-
|
||||
// The regression: the step read only the official route's credential, so a
|
||||
// fully configured user was taken over on every blank session.
|
||||
await expect.poll(
|
||||
async () => page.getByRole('region', { name: CREDENTIAL_STEP }).count(),
|
||||
async () => page.getByRole('dialog', { name: CREDENTIAL_STEP }).count(),
|
||||
{ timeout: 10_000 },
|
||||
).toBe(0)
|
||||
expect(await page.locator('[class*="onboardingStage"]').count()).toBe(0)
|
||||
expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(false)
|
||||
|
||||
// The Models page agrees: DeepSeek stays a row rather than reopening its
|
||||
|
||||
60
apps/web/tests/remote-welcome.e2e.ts
Normal file
60
apps/web/tests/remote-welcome.e2e.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// Trusted non-loopback Web access cannot call the loopback-only settings API;
|
||||
// the notice therefore advances for this browser process and returns on reload.
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
acknowledgeReloadConnectionLoss, launchWebScaffold, watchConsole, webSnapshotMode,
|
||||
WELCOME_NOTICE_COPY,
|
||||
type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { ZH_BROWSER_LOCALE } from './support.ts'
|
||||
|
||||
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('dialog', { 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)
|
||||
})
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
loadOverlayPatches,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
|
||||
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
||||
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
import type {
|
||||
LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk,
|
||||
@@ -62,6 +63,25 @@ import type {} from '@deepseek-ai/dsh-agent'
|
||||
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
|
||||
import { REPO_ROOT, requireDist } from './support.ts'
|
||||
|
||||
// Host-side web e2e cannot import a browser package: doing so would pull that
|
||||
// package's complete TS project into this graph. Mirrored from
|
||||
// packages/client/ui-settings-models/src/onboarding-copy.ts; drift makes the
|
||||
// default pre-acknowledgement stop suppressing the notice and fails loudly.
|
||||
// import {
|
||||
// WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE,
|
||||
// WELCOME_NOTICE_VERSION, WELCOME_NOTICE_COPY,
|
||||
// } from '@deepseek-ai/dsh-client-ui-settings-models'
|
||||
export const WELCOME_NOTICE_SETTINGS_NAMESPACE = 'ui-onboarding'
|
||||
export const WELCOME_NOTICE_ACK_FIELD = 'welcomeNoticeVersion'
|
||||
export const WELCOME_NOTICE_VERSION = '2026-08-13.1'
|
||||
export const WELCOME_NOTICE_COPY = {
|
||||
zh: {
|
||||
title: '内测声明',
|
||||
body: 'DeepSeek Harness 目前的 0.1 版本仍处在面向 Harness 开发者进行测试的阶段,还有许多地方需要持续改进和打磨,希望听取广大开发者的反馈建议。预计 DeepSeek Harness 的核心插件以及基础 API 都会在接下来的一段时间内快速迭代、持续演化。\n\n我们期待与全球开发者一起,在开源、开放、可复用、可组合的基础设施之上,共同探索智能上限。欢迎全球 Harness 开发者加入 DSH 插件生态。',
|
||||
continueLabel: '继续',
|
||||
},
|
||||
} as const
|
||||
|
||||
/** Snapshot mode for the lane, from $DSH_SNAPSHOT (same vocabulary as the other snapshot suites). */
|
||||
export type WebSnapshotMode = 'replay' | 'record' | 'refresh'
|
||||
|
||||
@@ -213,6 +233,8 @@ export interface LaunchOptions {
|
||||
* keyless first-run configuration lane; the default disables the adapter.
|
||||
*/
|
||||
deepSeekMissingCredential?: boolean
|
||||
/** Leave the current welcome notice pending; ordinary scenarios pre-acknowledge it before browser boot. */
|
||||
welcomeNoticePending?: boolean
|
||||
/**
|
||||
* Patch the shipped DeepSeek search row to a deterministic endpoint and
|
||||
* credential reference. Browser search scenarios keep the real provider and
|
||||
@@ -510,6 +532,11 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
})
|
||||
await ctx.loader.await()
|
||||
assertEntriesLoaded(ctx, 'web e2e scaffold')
|
||||
if (options.welcomeNoticePending !== true) {
|
||||
await ctx.settings.mutate(settingsNamespace(WELCOME_NOTICE_SETTINGS_NAMESPACE), [{
|
||||
op: 'set', path: [WELCOME_NOTICE_ACK_FIELD], value: WELCOME_NOTICE_VERSION,
|
||||
}])
|
||||
}
|
||||
const boundPort = ctx.get('webServer')?.port
|
||||
if (boundPort === undefined) {
|
||||
throw new Error('web e2e scaffold: webServer service missing after settled boot')
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
- treeitem "Ungrouped" [expanded]:
|
||||
- img
|
||||
- text: Ungrouped
|
||||
- treeitem "Use the read tool twice 1min"
|
||||
- treeitem "Use the read tool twice (1) now"
|
||||
- treeitem "Use the read tool twice (2) now" [selected]
|
||||
- treeitem "Use the read tool twice (1) now"
|
||||
- treeitem "Use the read tool twice 1min"
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
- region "添加一个 API Key 开始使用":
|
||||
- dialog "添加一个 API Key 开始使用":
|
||||
- heading "添加一个 API Key 开始使用" [level=2]
|
||||
- paragraph: 配置 DeepSeek 官方模型,即可开始使用。
|
||||
- text: API 密钥
|
||||
- textbox "API 密钥":
|
||||
- /placeholder: 输入 API 密钥
|
||||
- button "稍后配置"
|
||||
- button "前往配置"
|
||||
- button "保存并继续" [disabled]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
- dialog "内测声明":
|
||||
- heading "内测声明" [level=2]
|
||||
- paragraph: DeepSeek Harness 目前的 0.1 版本仍处在面向 Harness 开发者进行测试的阶段,还有许多地方需要持续改进和打磨,希望听取广大开发者的反馈建议。预计 DeepSeek Harness 的核心插件以及基础 API 都会在接下来的一段时间内快速迭代、持续演化。
|
||||
- paragraph: 我们期待与全球开发者一起,在开源、开放、可复用、可组合的基础设施之上,共同探索智能上限。欢迎全球 Harness 开发者加入 DSH 插件生态。
|
||||
- button "继续"
|
||||
@@ -2,5 +2,5 @@
|
||||
- treeitem "workspace" [expanded]:
|
||||
- img
|
||||
- text: workspace
|
||||
- treeitem "New Session" [selected]
|
||||
- treeitem "1 subagent running Delegate a background job. now"
|
||||
- treeitem "New Session" [selected]
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
- treeitem "workspace" [expanded]:
|
||||
- img
|
||||
- text: workspace
|
||||
- treeitem "Ask a research subagent to now"
|
||||
- treeitem "Explain event sourcing in one (1) now" [selected]
|
||||
- treeitem "Ask a research subagent to now"
|
||||
|
||||
Reference in New Issue
Block a user