test(e2e): 落实 Codex 审查意见 — 强制覆盖 editor 懒加载 chunk + 缺失内置 tab 即失败

- seed hello.txt,经 Explorer 打开文件并等待 /sidebar/bundle/editor.js 响应,
  缺失/损坏的 editor chunk 会令门禁变红(已实测:删 chunk → 错误条被检出)
- + 菜单缺失内置 tab 由跳过改为 toHaveCount(1) 硬失败,防深扫悄悄缩水
- AGENTS.md CI 冒烟说明同步
This commit is contained in:
Menghuan1918
2026-08-15 01:52:54 +08:00
parent 7ee0b457a3
commit f903ac77d6
2 changed files with 37 additions and 14 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ better-sidebar 从 v0.4.0 起暴露 `ctx.betterSidebar` 服务(Cordis context
1. `pnpm build && pnpm pack` 产出 tarball(与发布产物一致)。
2. `scripts/e2e-mount.sh` 用官方 CLI 把它装进一个**全新 scratch profile**(`dsh plugin --profile web add file:<tarball>`,触发 `dsh.profile.bundles` 协调),然后启动真实 `dsh web`(keyless,`--port 0`)。
3. `tests/e2e/mount.e2e.ts`(Playwright Chromium)加载页面,断言外壳与 `[data-dsh-better-sidebar]` 挂载、无 `dsh-better-sidebar:` 错误条、无 pageerror/插件 console 错误,并通过「+ 菜单」逐个打开内置 tab(含懒加载 chunk)深扫。
3. `tests/e2e/mount.e2e.ts`(Playwright Chromium)加载页面,断言外壳与 `[data-dsh-better-sidebar]` 挂载、无 `dsh-better-sidebar:` 错误条、无 pageerror/插件 console 错误,并通过「+ 菜单」逐个打开内置 tab(含终端懒加载 chunk)深扫,再经 Explorer 打开 seed 文件强制加载 editor 懒加载 chunk(`client-editor.js`)——缺失的内置 tab 或 chunk 都会使门禁变红。
本地跑:`pnpm build && pnpm pack && pnpm exec playwright install chromium && pnpm test:mount`(需 PATH 上有 `dsh` 或可经 npx 拉取)。DSH CLI 版本在 CI 钉住 `@deepseek-ai/dsh@0.1.0-rc.6`(与插件 peer 范围同步)。`tests/e2e` 的 spec 命名 `*.e2e.ts` + vitest `exclude` 双保险与 vitest 隔离;**改动 vitest `exclude` 时必须保留默认排除项**(`exclude` 会整体替换默认值)。
+36 -13
View File
@@ -14,14 +14,15 @@
* 3. asserts the plugin's crash markers never appear (no RenderBoundary /
* fail() strips, no `pageerror`, no plugin-prefixed console errors);
* 4. sweeps every built-in tab (Explorer / Source Control / Tasks /
* Terminal / Browser), which exercises tab activation including the
* lazily-fetched terminal/editor chunks.
* Terminal / Browser) — including the lazily-fetched terminal chunk —
* and then opens a seeded file through the Explorer to force the
* lazily-fetched editor chunk (client-editor.js) to load as well.
*
* Deterministic by construction: every wait is on a DOM/network marker, the
* suite is serial (one server instance), and any crash trips the very next
* assertion.
*/
import { mkdirSync } from 'node:fs'
import { mkdirSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { test, expect, request, type APIRequestContext } from '@playwright/test'
@@ -34,6 +35,10 @@ if (!BASE_URL) {
/** Workspace the sidebar renders against (created by the lane's seeding). */
const WORKSPACE_PATH = process.env.DSH_E2E_WORKSPACE ?? join(tmpdir(), 'dsh-e2e-workspace')
/** A file seeded into the workspace, opened through the Explorer to force the
* lazily-packed editor chunk (client-editor.js) to load. */
const SEEDED_FILE = 'hello.txt'
/**
* The plugin's crash markers. The client mounts inside an error boundary that
* renders a strip whose text starts with these prefixes instead of crashing
@@ -46,9 +51,11 @@ const BUILTIN_TABS = ['Explorer', 'Source Control', 'Tasks', 'Terminal', 'Browse
let api: APIRequestContext
/** Seed one workspace + one session through the host's unary RPC surface. */
/** Seed one workspace + one session (plus a file for the editor-chunk probe)
* through the host's unary RPC surface. */
async function seedSession(): Promise<void> {
mkdirSync(WORKSPACE_PATH, { recursive: true })
writeFileSync(join(WORKSPACE_PATH, SEEDED_FILE), 'hello from the mount lane\n')
const workspace = await api.post(`${BASE_URL}/api/workspace.create`, {
data: { type: 'client-request', rpcId: 'e2e-workspace', method: 'workspace.create', payload: { path: WORKSPACE_PATH } },
})
@@ -145,19 +152,16 @@ test('plugin mounts into the DSH shell and survives a built-in tab sweep', async
// Sweep every built-in tab through the "+" menu (the sidebar's own open-tab
// affordance, reachable from any pane state). Each open may fetch a lazy
// chunk (/sidebar/bundle/client-terminal.js / client-editor.js) and mount a
// real viewer — the highest-risk crash surfaces. A failure anywhere must
// surface as a pageerror or a console error, both of which the next
// assertion sees.
// real viewer — the highest-risk crash surfaces. The pinned plugin must
// offer every listed built-in: a missing or renamed descriptor is a real
// regression and fails the lane loudly instead of silently narrowing the
// sweep. A failure anywhere surfaces as a pageerror or a console error,
// both of which the next assertion sees.
const newTabButton = sidebar.getByRole('button', { name: 'New tab' }).first()
for (const title of BUILTIN_TABS) {
await newTabButton.click()
const item = page.getByRole('menuitem', { name: title }).first()
if ((await item.count()) === 0) {
console.warn(`[e2e] built-in tab "${title}" not offered by this DSH build; skipping`)
// Close the menu so the next iteration's + click re-opens it.
await page.keyboard.press('Escape')
continue
}
await expect(item, `built-in tab "${title}" is not offered by the + menu — descriptor removed or its label changed`).toHaveCount(1)
await item.click()
// Let the activation commit (including any lazy-chunk fetch) before the
// crash assertions run.
@@ -165,6 +169,25 @@ test('plugin mounts into the DSH shell and survives a built-in tab sweep', async
await assertNoCrash()
}
// The editor tab is hidden from the + menu — its CodeMirror chunk
// (client-editor.js) only loads when a file is opened. Exercise that path
// explicitly: reopen Explorer, open the seeded file, and require the chunk
// round-trip, so a missing/corrupt editor chunk fails the lane.
await newTabButton.click()
const explorerItem = page.getByRole('menuitem', { name: 'Explorer' }).first()
await expect(explorerItem, 'Explorer must be re-openable for the editor-chunk probe').toHaveCount(1)
await explorerItem.click()
const editorChunk = page.waitForResponse(
(response) => response.url().includes('/sidebar/bundle/editor.js'),
{ timeout: 30_000 },
)
const fileRow = sidebar.locator(`[role="button"][title$="${SEEDED_FILE}"]`)
await expect(fileRow, `the seeded "${SEEDED_FILE}" file must appear in the Explorer tree`).toHaveCount(1, { timeout: 30_000 })
await fileRow.click()
await editorChunk
await page.waitForTimeout(1_500)
await assertNoCrash()
// The plugin's own console prefix must never appear in errors, and no
// unhandled rejection may escape the sweep.
const pluginErrors = consoleErrors.filter((text) => /dsh-better-sidebar|Unhandled/.test(text))