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",