diff --git a/AGENTS.md b/AGENTS.md index dabc3a3..1a1fabe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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:`,触发 `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` 会整体替换默认值)。 diff --git a/tests/e2e/mount.e2e.ts b/tests/e2e/mount.e2e.ts index 1ca11b4..70f211e 100644 --- a/tests/e2e/mount.e2e.ts +++ b/tests/e2e/mount.e2e.ts @@ -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 { 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))