From 114af09aaec860a8a5a176713afb7776a84e3bac Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Mon, 10 Aug 2026 19:42:43 +0800 Subject: [PATCH] test(web): cover the plugin configuration section end to end Three scenarios over the real wire: the cards this deployment's exposed namespaces produce, one field edited down to the settings document with the override badge that layering produces, and the reset that clears it back to the composed default. Every other settings golden gains the same nav entry and nothing else. --- apps/web/tests/plugin-config.e2e.ts | 130 ++++++++++++++++++ .../created.expected.md | 3 + .../damaged.expected.md | 3 + .../section.expected.md | 3 + .../models-settings/configured.expected.md | 3 + .../models-settings/declared.expected.md | 3 + .../models-settings/empty.expected.md | 3 + .../models.expected.md | 3 + .../plugin-config/section.expected.md | 34 +++++ .../settings-chrome/dialog.expected.md | 3 + apps/web/tsconfig.json | 1 + tsconfig.host.json | 1 + 12 files changed, 190 insertions(+) create mode 100644 apps/web/tests/plugin-config.e2e.ts create mode 100644 apps/web/tests/snapshots/plugin-config/section.expected.md diff --git a/apps/web/tests/plugin-config.e2e.ts b/apps/web/tests/plugin-config.e2e.ts new file mode 100644 index 0000000000..b0c95c969a --- /dev/null +++ b/apps/web/tests/plugin-config.e2e.ts @@ -0,0 +1,130 @@ +// Web e2e scenario: the Plugins settings section — the cards a deployment's +// exposed host-plane namespaces produce, one field edited through the real +// wire down to `$DSH_HOME/settings.yaml`, and the override badge and reset +// that layering produces. Zero model calls: everything is client state plus +// the settings document on a blank frame, so there is no fixture and a stray +// stream would fail loud on the open llm seam. +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import type { Browser, Page } from 'playwright' +import { chromium } from 'playwright' +import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' +import { join } from 'node:path' +import { + assertFixtureInventory, captureStableAria, compareOrRefreshGolden, + launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, +} from './scaffold.ts' +import { ZH_BROWSER_LOCALE, saveFailureShot } from './support.ts' + +const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/plugin-config', import.meta.url)) +const SECTION_EXPECTED = join(SNAPSHOT_DIR, 'section.expected.md') +const MODE = webSnapshotMode() + +describe('web e2e: plugin configuration section', () => { + let scaffold: WebScaffold + let browser: Browser + let page: Page + let tripwire: ReturnType + + beforeAll(async () => { + scaffold = await launchWebScaffold({}) + browser = await chromium.launch() + // Chinese browser: the section asserts the localized copy the client + // derives from it, as the rest of the settings surface does. + page = await browser.newPage({ viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE }) + tripwire = watchConsole(page) + await page.goto(scaffold.baseUrl, { waitUntil: 'load' }) + await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + }, 120_000) + + afterAll(async () => { + await browser?.close() + await scaffold?.close() + }) + + /** + * Open the settings dialog on the Plugins section. The scenarios share one + * page so the settings document accumulates across them, so this leaves any + * dialog a previous scenario opened closed first — its mask would otherwise + * swallow the trigger click. + */ + async function openPlugins() { + if (await page.getByRole('dialog', { name: '设置' }).count() > 0) { + await page.keyboard.press('Escape') + await expect.poll(() => page.getByRole('dialog', { name: '设置' }).count(), { timeout: 5_000 }).toBe(0) + } + await page.getByRole('button', { name: '设置', exact: true }).click() + const dialog = page.getByRole('dialog', { name: '设置' }) + await dialog.waitFor({ timeout: 10_000 }) + await dialog.getByRole('button', { name: '插件' }).click() + await expect + .poll(() => dialog.getByRole('button', { name: '插件' }).getAttribute('aria-current'), { timeout: 5_000 }) + .toBe('true') + return dialog + } + + /** The settings document as the Host has written it so far. */ + async function settingsDocument(): Promise { + return readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8').catch(() => '') + } + + it('shows one card per exposed host-plane namespace', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-config-cards')) + const dialog = await openPlugins() + + // Every card the shipped web composition exposes: the shell executor, the + // agent loop, and the DeepSeek search provider. + await dialog.getByText('终端', { exact: true }).waitFor({ timeout: 10_000 }) + expect(await dialog.getByText('Agent 循环', { exact: true }).count()).toBe(1) + expect(await dialog.getByText('网页搜索', { exact: true }).count()).toBe(1) + // Collapsed: a card's fields appear only once it is expanded. + expect(await dialog.getByLabel('命令超时(毫秒)').count()).toBe(0) + + const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd) + await compareOrRefreshGolden(SECTION_EXPECTED, snapshot, MODE) + expect(tripwire.pageErrors).toEqual([]) + }, 60_000) + + it('writes an edited field to the settings document and marks it overridden', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-config-write')) + const dialog = await openPlugins() + await dialog.getByText('终端', { exact: true }).click() + + const timeout = dialog.getByLabel('命令超时(毫秒)') + await timeout.waitFor({ timeout: 10_000 }) + // The composed default this deployment ships, before any user layer. + expect(await timeout.inputValue()).toBe('60000') + await timeout.fill('12000') + await timeout.blur() + + await expect.poll(async () => (await settingsDocument()).includes('timeoutMs: 12000'), { timeout: 10_000 }) + .toBe(true) + // Presence in the user layer is what the badge reports, and the reset is + // offered only for a field that has one. + await expect.poll(() => dialog.getByText('已覆盖').count(), { timeout: 5_000 }).toBe(1) + expect(await dialog.getByRole('button', { name: '恢复默认' }).count()).toBe(1) + expect(tripwire.pageErrors).toEqual([]) + }, 60_000) + + it('clears the field back to the composed default on reset', async () => { + onTestFailed(() => saveFailureShot(page, 'web-e2e-plugin-config-reset')) + const dialog = await openPlugins() + await dialog.getByText('终端', { exact: true }).click() + const timeout = dialog.getByLabel('命令超时(毫秒)') + await timeout.waitFor({ timeout: 10_000 }) + expect(await timeout.inputValue()).toBe('12000') + + await dialog.getByRole('button', { name: '恢复默认' }).click() + + await expect.poll(async () => (await settingsDocument()).includes('timeoutMs'), { timeout: 10_000 }) + .toBe(false) + await expect.poll(() => timeout.inputValue(), { timeout: 5_000 }).toBe('60000') + expect(await dialog.getByText('已覆盖').count()).toBe(0) + expect(tripwire.pageErrors).toEqual([]) + }, 60_000) + + it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => { + expect(tripwire.warnings).toEqual([]) + await assertFixtureInventory(SNAPSHOT_DIR, ['section.expected.md']) + }) +}) diff --git a/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md index e5cefe28ef..aece772fe4 100644 --- a/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md +++ b/apps/web/tests/snapshots/agent-preset-authoring/created.expected.md @@ -10,6 +10,9 @@ - button "Agent 预设": - img - text: Agent 预设 + - button "插件": + - img + - text: 插件 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md index 8269dc2993..7620e679c8 100644 --- a/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md +++ b/apps/web/tests/snapshots/agent-preset-authoring/damaged.expected.md @@ -10,6 +10,9 @@ - button "Agent 预设": - img - text: Agent 预设 + - button "插件": + - img + - text: 插件 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md b/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md index ac5d6f6736..dade3d7b84 100644 --- a/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md +++ b/apps/web/tests/snapshots/agent-preset-authoring/section.expected.md @@ -10,6 +10,9 @@ - button "Agent 预设": - img - text: Agent 预设 + - button "插件": + - img + - text: 插件 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/models-settings/configured.expected.md b/apps/web/tests/snapshots/models-settings/configured.expected.md index 02746c7f76..3b57d5ac6f 100644 --- a/apps/web/tests/snapshots/models-settings/configured.expected.md +++ b/apps/web/tests/snapshots/models-settings/configured.expected.md @@ -10,6 +10,9 @@ - button "Agent 预设": - img - text: Agent 预设 + - button "插件": + - img + - text: 插件 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/models-settings/declared.expected.md b/apps/web/tests/snapshots/models-settings/declared.expected.md index 857bfaf13e..dd3f03776f 100644 --- a/apps/web/tests/snapshots/models-settings/declared.expected.md +++ b/apps/web/tests/snapshots/models-settings/declared.expected.md @@ -10,6 +10,9 @@ - button "Agent 预设": - img - text: Agent 预设 + - button "插件": + - img + - text: 插件 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/models-settings/empty.expected.md b/apps/web/tests/snapshots/models-settings/empty.expected.md index 03169f8e72..16cc93581b 100644 --- a/apps/web/tests/snapshots/models-settings/empty.expected.md +++ b/apps/web/tests/snapshots/models-settings/empty.expected.md @@ -10,6 +10,9 @@ - button "Agent 预设": - img - text: Agent 预设 + - button "插件": + - img + - text: 插件 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md b/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md index e50c347966..b5f25aaaa6 100644 --- a/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md +++ b/apps/web/tests/snapshots/onboarding-deepseek-config/models.expected.md @@ -10,6 +10,9 @@ - button "Agent 预设": - img - text: Agent 预设 + - button "插件": + - img + - text: 插件 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tests/snapshots/plugin-config/section.expected.md b/apps/web/tests/snapshots/plugin-config/section.expected.md new file mode 100644 index 0000000000..54ef09851f --- /dev/null +++ b/apps/web/tests/snapshots/plugin-config/section.expected.md @@ -0,0 +1,34 @@ +- dialog "设置": + - navigation: + - text: 设置 + - button "通用设置": + - img + - text: 通用设置 + - button "模型": + - img + - text: 模型 + - button "Agent 预设": + - img + - text: Agent 预设 + - button "插件": + - img + - text: 插件 + - button "打开配置文件" + - button "关闭": + - img + - text: 关闭 + - heading "插件配置" [level=2] + - paragraph: 本部署所组装插件自己拥有的设置。你在这里设的值会覆盖组装默认值,并在下一次使用时生效。 + - list: + - listitem: + - button "终端 限制 agent 运行的每一条命令。": + - img + - text: 终端 限制 agent 运行的每一条命令。 + - listitem: + - button "Agent 循环 Agent 如何派发工具调用。": + - img + - text: Agent 循环 Agent 如何派发工具调用。 + - listitem: + - button "网页搜索 DeepSeek 搜索提供方。": + - img + - text: 网页搜索 DeepSeek 搜索提供方。 diff --git a/apps/web/tests/snapshots/settings-chrome/dialog.expected.md b/apps/web/tests/snapshots/settings-chrome/dialog.expected.md index cf87f5acbd..3ac0245395 100644 --- a/apps/web/tests/snapshots/settings-chrome/dialog.expected.md +++ b/apps/web/tests/snapshots/settings-chrome/dialog.expected.md @@ -10,6 +10,9 @@ - button "Agent 预设": - img - text: Agent 预设 + - button "插件": + - img + - text: 插件 - button "打开配置文件" - button "关闭": - img diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index da25f0cc59..8106350a9e 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -35,6 +35,7 @@ "tests/trajectory-virtualization.e2e.ts", "tests/lifecycle-chrome.e2e.ts", "tests/details-session-lifecycle.e2e.ts", + "tests/plugin-config.e2e.ts", "tests/settings-chrome.e2e.ts", "tests/models-settings.e2e.ts", "tests/default-model.e2e.ts", diff --git a/tsconfig.host.json b/tsconfig.host.json index d9bf1c29e4..1817ccab26 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -24,6 +24,7 @@ "apps/web/tests/trajectory-virtualization.e2e.ts", "apps/web/tests/lifecycle-chrome.e2e.ts", "apps/web/tests/details-session-lifecycle.e2e.ts", + "apps/web/tests/plugin-config.e2e.ts", "apps/web/tests/settings-chrome.e2e.ts", "apps/web/tests/models-settings.e2e.ts", "apps/web/tests/onboarding-deepseek-config.e2e.ts",