Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/wework-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ jobs:
CODEX_BIN: ${{ github.workspace }}/wework/src-tauri/binaries/codex/x86_64-unknown-linux-gnu/vendor/x86_64-unknown-linux-musl/bin/codex
run: |
test -x "$CODEX_BIN"
xvfb-run -a --server-args="-screen 0 1280x720x24" pnpm --filter wework e2e:desktop:plugins
xvfb-run -a --server-args="-screen 0 1280x720x24" pnpm --filter wework e2e:desktop
xvfb-run -a --server-args="-screen 0 1280x720x24" pnpm --filter wework e2e:desktop:cloud

Expand Down
9 changes: 9 additions & 0 deletions docs/en/wework/developer-guide/wework-e2e-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ Run only the cloud-project desktop E2E:
pnpm --filter wework e2e:desktop:cloud
```

Run only the plugin marketplace, install, chat-use, and uninstall flow:

```bash
pnpm --filter wework e2e:desktop:plugins
```

The command starts a test-only Vite server through `wework/playwright.config.ts`:

```bash
Expand Down Expand Up @@ -80,6 +86,8 @@ Optional `WEWORK_E2E_EXECUTOR_BIN` and `WEWORK_E2E_APP_BIN` reuse already-built

The cloud-project scenario starts a real Backend, Redis, and a real Executor registered as a remote device. It exercises real authentication, device RPC, task persistence, and project deletion while covering project creation, task execution, conversation restoration, follow-up, and project removal. Only the model Responses API used by Codex is simulated; Backend HTTP and WebSocket APIs must not be mocked. Python 3.11, `uv`, and `redis-server` are required to run this scenario.

The plugin scenario dynamically creates an isolated local Codex marketplace and a plugin with a Skill under the test-results directory. It then uses the real Tauri WebView, Executor, and Codex app-server to verify marketplace discovery, installation, insertion of the plugin reference into the chat composer, and uninstallation. It neither reads the user's Codex home nor mocks plugin APIs; marketplace data, plugin cache, and installation state remain inside the isolated test directory. Screenshots are retained for all four critical stages, with application, Executor, and UI snapshot diagnostics retained on failure.

## Responses API Mock

`wework/e2e/utils/mock-response-api-server.mjs` provides a real HTTP service that validates local-model capability probes:
Expand Down Expand Up @@ -153,6 +161,7 @@ Desktop task-flow E2E requires a Linux runner with a graphical session, for exam

```bash
pnpm --filter wework prepare:codex
xvfb-run -a pnpm --filter wework e2e:desktop:plugins
xvfb-run -a pnpm --filter wework e2e:desktop
xvfb-run -a pnpm --filter wework e2e:desktop:cloud
```
Expand Down
9 changes: 9 additions & 0 deletions docs/zh/wework/developer-guide/wework-e2e-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ pnpm --filter wework e2e:desktop
pnpm --filter wework e2e:desktop:cloud
```

仅运行插件市场、安装、对话使用和卸载链路:

```bash
pnpm --filter wework e2e:desktop:plugins
```

该命令会通过 `wework/playwright.config.ts` 启动测试专用 Vite 服务:

```bash
Expand Down Expand Up @@ -80,6 +86,8 @@ CODEX_BIN=/absolute/path/to/codex pnpm --filter wework e2e:desktop

云端项目场景会启动真实 Backend、Redis 和一个注册为远端设备的真实 Executor,通过真实鉴权、设备 RPC、任务持久化和项目删除接口完成创建项目、执行任务、恢复会话、连续追问与删除项目验证。测试只模拟 Codex 使用的模型 Responses API;不得模拟 Backend HTTP 或 WebSocket 接口。运行该场景需要 Python 3.11、`uv` 和 `redis-server`。

插件场景会在测试结果目录动态创建隔离的本地 Codex marketplace 和带 Skill 的插件,然后通过真实 Tauri WebView、Executor 与 Codex app-server 验证市场展示、安装、在对话编辑器中插入插件引用及卸载。场景不访问个人 Codex home,也不 mock 插件 API;市场、插件缓存和安装状态都随测试结果目录清理。四个关键阶段会保留截图,失败时同时保留应用、Executor 和 UI 快照诊断。

## Responses API Mock

`wework/e2e/utils/mock-response-api-server.mjs` 提供真实 HTTP 服务,用于验证本地模型能力探针请求:
Expand Down Expand Up @@ -153,6 +161,7 @@ pnpm --filter wework e2e

```bash
pnpm --filter wework prepare:codex
xvfb-run -a pnpm --filter wework e2e:desktop:plugins
xvfb-run -a pnpm --filter wework e2e:desktop
xvfb-run -a pnpm --filter wework e2e:desktop:cloud
```
Expand Down
141 changes: 141 additions & 0 deletions wework/e2e/desktop/task-flow.e2e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const RECONNECT_ONLY = process.argv.includes('--reconnect-only')
const VIEW_IMAGE_ONLY = process.argv.includes('--view-image-only')
const ATTACHMENT_ONLY_SIDEBAR = process.argv.includes('--attachment-only-sidebar')
const CLOUD_ONLY = process.argv.includes('--cloud-only')
const PLUGINS_ONLY = process.argv.includes('--plugins-only')
const DESKTOP_SCENARIO_ONLY = process.env.WEWORK_E2E_DESKTOP_SCENARIO_ONLY === 'true'

const scriptDir = dirname(fileURLToPath(import.meta.url))
Expand All @@ -101,6 +102,57 @@ const repoDir = resolve(weworkDir, '..')
const runId = `${new Date().toISOString().replace(/[:.]/g, '-')}-${process.pid}`
const resultDir = join(weworkDir, 'test-results', 'desktop-e2e', runId)

const PLUGIN_MARKETPLACE_NAME = 'desktop-e2e-marketplace'
const PLUGIN_NAME = 'desktop-e2e-plugin'
const PLUGIN_DISPLAY_NAME = 'Desktop E2E Plugin'

async function createPluginMarketplaceFixture(root) {
const marketplaceManifestDir = join(root, '.agents', 'plugins')
const pluginRoot = join(root, 'plugins', PLUGIN_NAME)
await Promise.all([
mkdir(marketplaceManifestDir, { recursive: true }),
mkdir(join(pluginRoot, '.codex-plugin'), { recursive: true }),
mkdir(join(pluginRoot, 'skills', 'desktop-e2e-skill'), { recursive: true }),
])
await Promise.all([
writeFile(
join(marketplaceManifestDir, 'marketplace.json'),
`${JSON.stringify(
{
name: PLUGIN_MARKETPLACE_NAME,
interface: { displayName: 'Desktop E2E Marketplace' },
plugins: [
{
name: PLUGIN_NAME,
source: { source: 'local', path: `./plugins/${PLUGIN_NAME}` },
},
],
},
null,
2
)}\n`
),
writeFile(
join(pluginRoot, '.codex-plugin', 'plugin.json'),
`${JSON.stringify(
{
name: PLUGIN_NAME,
interface: {
displayName: PLUGIN_DISPLAY_NAME,
shortDescription: 'Exercises the real Wework plugin lifecycle',
},
},
null,
2
)}\n`
),
writeFile(
join(pluginRoot, 'skills', 'desktop-e2e-skill', 'SKILL.md'),
`---\nname: desktop-e2e-skill\ndescription: Verifies the installed plugin can be used in chat.\n---\n\nUse this skill to verify the Wework desktop plugin flow.\n`
),
])
}

function withTimeout(promise, timeoutMs, message) {
let timeout
const timeoutPromise = new Promise((_, reject) => {
Expand Down Expand Up @@ -339,6 +391,86 @@ async function captureVerificationScreenshot(control, name, selector = 'body') {
return screenshotPath
}

async function verifyPluginLifecycle(control, marketplacePath) {
await control.command('click', '[data-testid="plugins-button"]')
await control.command('waitFor', '[data-testid="plugins-workspace"]', {
timeoutMs: WORKBENCH_READY_TIMEOUT_MS,
})

const initialSnapshot = JSON.parse(await control.command('snapshot', 'body'))
if (initialSnapshot.testIds.includes('plugins-add-custom-marketplace-empty-button')) {
await control.command('click', '[data-testid="plugins-add-custom-marketplace-empty-button"]')
} else {
await control.command('click', '[data-testid="plugins-add-marketplace-button"]')
await control.command('click', '[data-testid="plugins-add-custom-marketplace-button"]')
}
await control.command('fill', '[data-testid="plugins-marketplace-path-input"]', {
value: marketplacePath,
})
await control.command('clickWhenEnabled', '[data-testid="plugins-marketplace-save-button"]', {
stableMs: COMPOSER_READY_STABILITY_MS,
timeoutMs: UI_TIMEOUT_MS,
})

const marketplaceSnapshot = await waitForSnapshot(
control,
snapshot =>
snapshot.text.includes(PLUGIN_DISPLAY_NAME) &&
snapshot.testIds.some(testId => testId.startsWith('plugin-marketplace-row-')),
'The local plugin marketplace did not expose its plugin'
)
const rowTestId = marketplaceSnapshot.testIds.find(testId =>
testId.startsWith('plugin-marketplace-row-')
)
assert.ok(rowTestId, 'The plugin marketplace row did not have a stable test id')
const pluginId = rowTestId.slice('plugin-marketplace-row-'.length)
const installSelector = `[data-testid="plugin-marketplace-install-${pluginId}"]`
const actionsSelector = `[data-testid="plugin-marketplace-actions-${pluginId}"]`
await captureVerificationScreenshot(control, 'plugins-01-marketplace.png')

await control.command('click', installSelector)
await waitForSnapshot(
control,
snapshot => snapshot.testIds.includes(`plugin-marketplace-actions-${pluginId}`),
'The plugin was not shown as installed after the real app-server request'
)
assert.match(
await control.command('getText', installSelector),
/Try in chat|在对话中试用/,
'The installed plugin did not expose its chat action'
)
await captureVerificationScreenshot(control, 'plugins-02-installed.png')

await control.command('click', installSelector)
await control.command('waitFor', ACTIVE_COMPOSER_SELECTOR, {
timeoutMs: WORKBENCH_READY_TIMEOUT_MS,
})
await waitForSnapshot(
control,
snapshot => snapshot.text.includes(PLUGIN_DISPLAY_NAME),
'Trying the installed plugin did not place its reference in the composer',
UI_TIMEOUT_MS,
ACTIVE_WORKBENCH_SELECTOR
)
await captureVerificationScreenshot(control, 'plugins-03-used-in-chat.png')

await control.command('click', '[data-testid="plugins-button"]')
await control.command('waitFor', actionsSelector, { timeoutMs: WORKBENCH_READY_TIMEOUT_MS })
await control.command('click', actionsSelector)
await control.command('click', `[data-testid="plugin-marketplace-uninstall-${pluginId}"]`)
await waitForSnapshot(
control,
snapshot => !snapshot.testIds.includes(`plugin-marketplace-actions-${pluginId}`),
'The plugin remained installed after the uninstall request'
)
assert.match(
await control.command('getText', installSelector),
/Install|安装/,
'The marketplace did not return to the install state after uninstall'
)
await captureVerificationScreenshot(control, 'plugins-04-uninstalled.png')
}

function processIsAlive(processId) {
try {
process.kill(processId, 0)
Expand Down Expand Up @@ -2762,6 +2894,7 @@ async function main() {
const workspacePath = join(resultDir, 'workspace')
const homePath = join(resultDir, 'home')
const executorHome = join(resultDir, 'executor-home')
const pluginMarketplacePath = join(resultDir, 'plugin-marketplace')
const appLogPath = join(resultDir, 'app.log')
const executorLogPath = join(resultDir, 'executor.log')
await Promise.all([
Expand All @@ -2774,6 +2907,7 @@ async function main() {
join(workspacePath, IMAGE_ARTIFACT_NAME),
Buffer.from(IMAGE_ARTIFACT_BASE64, 'base64')
)
await createPluginMarketplaceFixture(pluginMarketplacePath)
await runChecked('git', ['init'], { cwd: workspacePath })
await runChecked('git', ['config', 'user.name', 'Wework Desktop E2E'], { cwd: workspacePath })
await runChecked('git', ['config', 'user.email', 'desktop-e2e@wework.local'], {
Expand Down Expand Up @@ -2893,6 +3027,13 @@ async function main() {
control.failBlockedCloudModels()
await triggerModelReloadUntilCloudFailure(control)

if (PLUGINS_ONLY) {
phase = 'plugin-lifecycle'
await verifyPluginLifecycle(control, pluginMarketplacePath)
console.log(`Wework desktop plugin E2E passed. Evidence: ${resultDir}`)
return
}

if (desktopScenario) {
phase = 'desktop-extension-scenario'
await desktopScenario.verify(control)
Expand Down
1 change: 1 addition & 0 deletions wework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"e2e": "playwright test",
"e2e:desktop": "node e2e/desktop/task-flow.e2e.mjs",
"e2e:desktop:cloud": "node e2e/desktop/task-flow.e2e.mjs --cloud-only",
"e2e:desktop:plugins": "node e2e/desktop/task-flow.e2e.mjs --plugins-only",
"e2e:desktop:lifecycle": "node e2e/desktop/task-flow.e2e.mjs --lifecycle-only",
"e2e:desktop:reconnect": "node e2e/desktop/task-flow.e2e.mjs --reconnect-only",
"e2e:desktop:attachment-only-sidebar": "node e2e/desktop/task-flow.e2e.mjs --attachment-only-sidebar",
Expand Down
Loading