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
5 changes: 4 additions & 1 deletion docs/en/developer-guide/wework-e2e-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Tests do not mock backend APIs. When Backend is not running, the login-page smok

## Desktop Task-Flow E2E

`wework/e2e/desktop/task-flow.e2e.mjs` covers the real task lifecycle in a local workspace:
`wework/e2e/desktop/task-flow.e2e.mjs` covers the real task lifecycle in a local workspace and lets product distributions inject an optional desktop scenario:

1. Builds and starts the real Tauri Wework application, opening an isolated workspace with `--open-workspace`.
2. Starts the real `wegent-executor` sidecar, which starts a real `codex app-server`.
Expand All @@ -64,6 +64,7 @@ Tests do not mock backend APIs. When Backend is not running, the login-page smok
5. Sends a follow-up in the same conversation and verifies its request and rendered response.
6. Starts a streaming response, cancels it through the desktop UI, verifies the stopped task state and rendered stop notice, then verifies the composer accepts a subsequent message.
7. Forces one model failure, clicks retry in the rendered error card, and verifies the retried request and final response.
8. Dynamically loads a product scenario when `WEWORK_E2E_DESKTOP_SCENARIO_MODULE` is set. The public runner supplies only HTTP, WebSocket, control, and diagnostic lifecycles; it contains no concrete product protocol or assertions.

The test does not simulate Wework, Executor, or Codex. To keep regression results deterministic and avoid requiring a real account, it starts only a loopback OpenAI Responses-compatible service as a custom Codex model provider. That service returns deterministic tool calls and final text; the tool call is still executed by real Codex in the isolated workspace.

Expand Down Expand Up @@ -111,6 +112,8 @@ Available methods:

The desktop E2E build additionally injects `VITE_WEWORK_DESKTOP_E2E_CONTROL_URL`. Only when E2E mode and this URL are both present does the frontend poll a local loopback controller for `click`, `fill`, and wait assertions; normal development and production builds have no controller endpoint. The controller drives real WebView DOM events and does not replace task, model-selection, Executor, or Codex implementations.

When a built-in action does not handle a command, the public controller delegates it through `@extensions/desktop-control` to a product extension. Without a product extension, unknown actions fail explicitly; public automation does not recognize concrete product protocols.

The controller uses short polling: the server returns `204` when no command is available, and the frontend waits briefly before polling again. This prevents a stale long-poll connection, left behind by a WebView reload, task switch, or stream completion, from consuming later commands. When `fill` targets a Lexical editor, the controller uses the editor's exposed `value` setter so the React/Lexical state is actually committed; do not replace it with raw DOM insertion. Failure diagnostics include delivered `commandHistory` in `scenario-state.json` to aid control-channel debugging.

On macOS, the desktop controller's `capture` command asks Tauri for a native WebKit `WKWebView` snapshot instead of cloning the DOM in the page. The native command is available only when `VITE_WEWORK_E2E=true` and times out after 10 seconds. Capturing `body` returns the full PNG directly; other selectors are cropped from that native snapshot using the element bounds. This keeps failure diagnostics faithful to fonts, native WebView rendering, and the real page state without depending on a hidden iframe load event.
Expand Down
2 changes: 1 addition & 1 deletion docs/en/developer-guide/wework-embedded-browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ This binding keeps the browser the user sees and the browser the agent controls

The public Wework codebase defines only the cloud desktop extension contract and an unavailable default implementation. It does not include a concrete remote desktop protocol, authentication endpoint, proxy, page, or third-party client assets. The workbench and device settings use this capability only through `src/extensions/cloud-desktop-contract.ts`; the default implementation sets `available` to `false`, so no desktop action is shown.

Product distributions may provide an implementation for `@extensions/cloud-desktop` at build time. That implementation owns connection setup and opening its page in the embedded browser, and must use `isCurrent` to ignore asynchronous requests after the project, device, or connection context changes. Do not add concrete VNC APIs, `vnc.html`, or noVNC assets back to public components as a fallback.
Product distributions may provide an implementation for `@extensions/cloud-desktop` at build time. The generic contract exposes `DeviceAction` and `WorkspaceAction` entry points for settings and project workspaces. A concrete implementation owns connection setup, asynchronous state, and opening its page in the embedded browser, and must use `isCurrent` to ignore asynchronous requests after the project, device, or connection context changes. Public Wework provides only an unavailable fallback and must not contain concrete remote-desktop protocols, pages, or assets.

## Annotation Flow

Expand Down
5 changes: 4 additions & 1 deletion docs/zh/developer-guide/wework-e2e-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ node e2e/utils/mock-response-api-server.mjs

## 桌面端任务全链路 E2E

`wework/e2e/desktop/task-flow.e2e.mjs` 覆盖本机工作区中的真实任务生命周期:
`wework/e2e/desktop/task-flow.e2e.mjs` 覆盖本机工作区中的真实任务生命周期,并允许产品发行版注入可选桌面场景:

1. 构建并启动真实 Tauri Wework 应用,使用 `--open-workspace` 打开隔离工作区。
2. 启动真实 `wegent-executor` sidecar,并由它启动真实 `codex app-server`。
Expand All @@ -64,6 +64,7 @@ node e2e/utils/mock-response-api-server.mjs
5. 在同一会话中发送连续追问,并校验对应请求和页面回复。
6. 启动流式回复后通过桌面端 UI 取消,校验任务已停止、停止提示已渲染,并在发送后续消息时恢复输入。
7. 让模型首次请求确定性失败,点击错误卡中的重试,并校验重试请求和最终回复。
8. 如果设置了 `WEWORK_E2E_DESKTOP_SCENARIO_MODULE`,动态加载产品场景;公共 runner 只提供 HTTP、WebSocket、控制和诊断生命周期,不包含具体产品协议或断言。

测试不模拟 Wework、Executor 或 Codex。为了让回归结果确定且不需要真实账号,测试只在 loopback 地址启动一个 OpenAI Responses 兼容服务,作为 Codex 的自定义模型 provider。该服务会返回确定性的工具调用和最终文本;工具调用仍由真实 Codex 在隔离工作区内执行。

Expand Down Expand Up @@ -111,6 +112,8 @@ http://127.0.0.1:9998/v1

桌面端 E2E 构建会额外注入 `VITE_WEWORK_DESKTOP_E2E_CONTROL_URL`。只有在 E2E 模式且该 URL 存在时,前端才会轮询本机 loopback 控制器来执行 `click`、`fill` 和等待断言;常规开发和生产构建不会包含控制端点。控制器只驱动真实 WebView DOM 事件,不替换任务、模型选择、Executor 或 Codex 的实现。

公共控制器在内置动作未处理命令时,通过 `@extensions/desktop-control` 委派给产品扩展。没有产品扩展时,未知动作会明确失败;公共自动化层不识别具体产品协议。

控制器使用短轮询:没有待执行指令时服务端返回 `204`,前端短暂等待后再次请求。这避免了 WebView 刷新、任务切换或流结束时遗留的长轮询连接吞掉后续指令。对 Lexical 编辑器执行 `fill` 时,控制器会使用编辑器暴露的 `value` setter,以便真实提交 React/Lexical 状态;不要用原始 DOM 插入来替代它。失败诊断中的 `scenario-state.json` 会记录已投递的 `commandHistory`,用于定位控制通道问题。

桌面控制器的 `capture` 指令在 macOS 上通过 Tauri 调用 WebKit 原生 `WKWebView` snapshot,而不是在页面内复制 DOM。原生层仅在 `VITE_WEWORK_E2E=true` 时开放该命令,并在 10 秒后超时;截取 `body` 时直接返回完整 PNG,截取其他选择器时由前端按照元素边界裁剪原生快照。这样失败诊断可以覆盖字体、原生 WebView 渲染和真实页面状态,也不会依赖隐藏 iframe 的加载事件。
Expand Down
2 changes: 1 addition & 1 deletion docs/zh/developer-guide/wework-embedded-browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Executor 启动 Codex 时会注入 relay server 配置。模型调用浏览器

公开版 Wework 只定义云桌面的扩展契约和不可用时的默认实现,不包含具体远程桌面协议、鉴权接口、代理、页面或第三方客户端资源。工作台和设备设置页只能通过 `src/extensions/cloud-desktop-contract.ts` 使用该能力;默认实现的 `available` 为 `false`,因此不会展示桌面入口。

产品发行版可以在构建时为 `@extensions/cloud-desktop` 提供实现。实现负责完成连接、在内置浏览器中打开页面,并通过 `isCurrent` 忽略项目、设备或连接上下文已经变化的异步请求。不要在公开组件中增加具体 VNC API、`vnc.html` 或 noVNC 资源作为回退路径。
产品发行版可以在构建时为 `@extensions/cloud-desktop` 提供实现。通用契约分别通过 `DeviceAction` 和 `WorkspaceAction` 向设置页及项目工作区提供入口;具体实现负责连接、异步状态和在内置浏览器中打开页面,并通过 `isCurrent` 忽略项目、设备或连接上下文已经变化的异步请求。公共 Wework 只提供不可用的空实现,不应包含具体远程桌面协议、页面或资源。

## 批注流程

Expand Down
57 changes: 57 additions & 0 deletions wework/e2e/desktop/process-lifecycle.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const PROCESS_STOP_TIMEOUT_MS = 10_000

function withTimeout(promise, timeoutMs, message) {
let timeout
const timeoutPromise = new Promise((_, reject) => {
timeout = setTimeout(() => reject(new Error(message)), timeoutMs)
})
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeout))
}

function waitForProcessExit(child, timeoutMs) {
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve()
return withTimeout(
new Promise(resolvePromise => child.once('exit', resolvePromise)),
timeoutMs,
`Timed out waiting for process ${child.pid ?? 'unknown'} to exit`
)
}

export async function stopProcess(child) {
if (!child || child.exitCode !== null || child.signalCode !== null) return
child.kill('SIGTERM')
try {
await waitForProcessExit(child, PROCESS_STOP_TIMEOUT_MS)
} catch {
child.kill('SIGKILL')
await waitForProcessExit(child, PROCESS_STOP_TIMEOUT_MS)
}
}
Comment on lines +11 to +29

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Avoid 20-second teardown hangs for processes that failed to spawn.

If a ChildProcess fails to spawn, child.pid will be undefined, and the process will never emit an exit event. Currently, waitForProcessExit will wait for this non-existent event until the timeout expires twice (once for SIGTERM, once for SIGKILL fallback), causing an unnecessary 20-second delay per failed process during test teardown.

Add an early return for missing pid to prevent these hangs and speed up teardown on failure.

⚡ Proposed fix
 function waitForProcessExit(child, timeoutMs) {
-  if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve()
+  if (child.pid === undefined || child.exitCode !== null || child.signalCode !== null) return Promise.resolve()
   return withTimeout(
     new Promise(resolvePromise => child.once('exit', resolvePromise)),
     timeoutMs,
     `Timed out waiting for process ${child.pid ?? 'unknown'} to exit`
   )
 }
 
 export async function stopProcess(child) {
-  if (!child || child.exitCode !== null || child.signalCode !== null) return
+  if (!child || child.pid === undefined || child.exitCode !== null || child.signalCode !== null) return
   child.kill('SIGTERM')
   try {
     await waitForProcessExit(child, PROCESS_STOP_TIMEOUT_MS)
   } catch {
     child.kill('SIGKILL')
     await waitForProcessExit(child, PROCESS_STOP_TIMEOUT_MS)
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function waitForProcessExit(child, timeoutMs) {
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve()
return withTimeout(
new Promise(resolvePromise => child.once('exit', resolvePromise)),
timeoutMs,
`Timed out waiting for process ${child.pid ?? 'unknown'} to exit`
)
}
export async function stopProcess(child) {
if (!child || child.exitCode !== null || child.signalCode !== null) return
child.kill('SIGTERM')
try {
await waitForProcessExit(child, PROCESS_STOP_TIMEOUT_MS)
} catch {
child.kill('SIGKILL')
await waitForProcessExit(child, PROCESS_STOP_TIMEOUT_MS)
}
}
function waitForProcessExit(child, timeoutMs) {
if (child.pid === undefined || child.exitCode !== null || child.signalCode !== null) return Promise.resolve()
return withTimeout(
new Promise(resolvePromise => child.once('exit', resolvePromise)),
timeoutMs,
`Timed out waiting for process ${child.pid ?? 'unknown'} to exit`
)
}
export async function stopProcess(child) {
if (!child || child.pid === undefined || child.exitCode !== null || child.signalCode !== null) return
child.kill('SIGTERM')
try {
await waitForProcessExit(child, PROCESS_STOP_TIMEOUT_MS)
} catch {
child.kill('SIGKILL')
await waitForProcessExit(child, PROCESS_STOP_TIMEOUT_MS)
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/e2e/desktop/process-lifecycle.mjs` around lines 11 - 29, Update
waitForProcessExit to return immediately when the ChildProcess has no pid,
alongside its existing exited-process checks. Preserve the current exit-event
waiting and timeout behavior for processes with a valid pid.


export async function stopProcessGroup(child) {
if (!child) return
if (process.platform === 'win32' || !Number.isInteger(child.pid)) {
await stopProcess(child)
return
}

signalProcessGroup(child.pid, 'SIGTERM')
if (child.exitCode === null && child.signalCode === null) {
try {
await waitForProcessExit(child, PROCESS_STOP_TIMEOUT_MS)
} catch {
signalProcessGroup(child.pid, 'SIGKILL')
await waitForProcessExit(child, PROCESS_STOP_TIMEOUT_MS)
return
}
}
signalProcessGroup(child.pid, 'SIGKILL')
}

function signalProcessGroup(processGroupId, signal) {
try {
process.kill(-processGroupId, signal)
} catch (error) {
if (error?.code !== 'ESRCH') throw error
}
}
10 changes: 10 additions & 0 deletions wework/e2e/desktop/scenario-loader.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export async function loadDesktopScenario(moduleUrl, options) {
if (!moduleUrl) return null

const scenarioModule = await import(moduleUrl)
if (typeof scenarioModule.createDesktopScenario !== 'function') {
throw new Error(`Desktop scenario module must export createDesktopScenario: ${moduleUrl}`)
}

return scenarioModule.createDesktopScenario(options)
}
78 changes: 47 additions & 31 deletions wework/e2e/desktop/task-flow.e2e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import { constants } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

import { loadDesktopScenario } from './scenario-loader.mjs'
import { stopProcess, stopProcessGroup } from './process-lifecycle.mjs'

const DESKTOP_READY_TIMEOUT_MS = 60_000
const WORKBENCH_READY_TIMEOUT_MS = 180_000
const UI_TIMEOUT_MS = 120_000
const PROCESS_STOP_TIMEOUT_MS = 10_000
const COMPOSER_READY_STABILITY_MS = 750
const TASK_PROMPT = 'WEWORK_DESKTOP_E2E_TASK: create the requested verification file.'
const COMPLETION_TEXT = 'WEWORK_DESKTOP_E2E_COMPLETE'
Expand Down Expand Up @@ -38,11 +40,11 @@ const MODEL_PROVIDER_ID = 'wework-e2e'
const MODEL_ID = 'gpt-5.4'
const DEFAULT_MODEL_ID = 'gpt-5.4-mini'
const BLOCKED_CLOUD_MODEL_PATH = '/api/models/unified'
const CLOUD_DEVICE_ID = 'wework-e2e-cloud-device'
const FRESH_CHAT_PROMPT = 'WEWORK_DESKTOP_E2E_FRESH_CHAT: confirm this is a new conversation.'
const FRESH_CHAT_COMPLETION_TEXT = 'WEWORK_DESKTOP_E2E_FRESH_CHAT_COMPLETE'
const ATTACHMENT_ONLY_COMPLETION_TEXT = 'WEWORK_DESKTOP_E2E_ATTACHMENT_ONLY_COMPLETE'
const ATTACHMENT_ONLY_FILENAME = 'same-name-attachment.png'
const CLOUD_DEVICE_ID = 'wework-e2e-cloud-device'
const CLOUD_TASK_PROMPT =
'WEWORK_DESKTOP_E2E_CLOUD_TASK: create the requested cloud verification file.'
const CLOUD_COMPLETION_TEXT = 'WEWORK_DESKTOP_E2E_CLOUD_COMPLETE'
Expand All @@ -60,6 +62,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 DESKTOP_SCENARIO_ONLY = process.env.WEWORK_E2E_DESKTOP_SCENARIO_ONLY === 'true'

const scriptDir = dirname(fileURLToPath(import.meta.url))
const weworkDir = resolve(scriptDir, '..', '..')
Expand Down Expand Up @@ -171,26 +174,6 @@ async function resolveExecutable(configuredPath, fallbackCommand, description) {
return resolved
}

function waitForProcessExit(child, timeoutMs) {
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve()
return withTimeout(
new Promise(resolvePromise => child.once('exit', resolvePromise)),
timeoutMs,
`Timed out waiting for process ${child.pid ?? 'unknown'} to exit`
)
}

async function stopProcess(child) {
if (!child || child.exitCode !== null || child.signalCode !== null) return
child.kill('SIGTERM')
try {
await waitForProcessExit(child, PROCESS_STOP_TIMEOUT_MS)
} catch {
child.kill('SIGKILL')
await waitForProcessExit(child, PROCESS_STOP_TIMEOUT_MS)
}
}

async function appendProcessOutput(stream, destination) {
if (!stream) return
stream.on('data', chunk => {
Expand Down Expand Up @@ -895,12 +878,15 @@ class RealCloudEnvironment {
BIND_SHELL: 'claudecode',
LOCAL_WORKSPACE_ROOT: dirname(this.workspacePath),
WEWORK_E2E_MODEL_API_KEY: MODEL_API_KEY,
DEVICE_SESSION_GATEWAY_HOST: '127.0.0.1',
DEVICE_SESSION_GATEWAY_PORT: '0',
}
delete remoteEnv.WEGENT_APP_IPC_DEVICE_ID
this.remoteExecutor = spawn(this.executorBinary, [], {
cwd: weworkDir,
env: remoteEnv,
stdio: ['ignore', 'pipe', 'pipe'],
detached: process.platform !== 'win32',
})
await Promise.all([
appendProcessOutput(this.remoteExecutor.stdout, this.remoteExecutorLogPath),
Expand Down Expand Up @@ -984,19 +970,21 @@ class RealCloudEnvironment {
`Cloud E2E cleanup could not cancel running tasks: ${String(error)}\n`
)
}
await stopProcess(this.remoteExecutor)
await stopProcessGroup(this.remoteExecutor)
await stopProcess(this.backend)
await stopProcess(this.redis)
}
}

class DesktopE2EServer {
constructor(workspacePath, cloudWorkspacePath = workspacePath) {
constructor(workspacePath, cloudWorkspacePath = workspacePath, desktopScenario = null) {
this.workspacePath = workspacePath
this.cloudWorkspacePath = cloudWorkspacePath
this.desktopScenario = desktopScenario
this.server = createServer((request, response) => {
void this.handle(request, response)
})
this.desktopScenario?.attachServer?.(this.server)
this.controlServer = createServer((request, response) => {
void this.handleControl(request, response)
})
Expand Down Expand Up @@ -1071,6 +1059,7 @@ class DesktopE2EServer {
async close() {
for (const response of this.blockedCloudResponses) response.destroy()
this.blockedCloudResponses.clear()
this.desktopScenario?.close?.()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Unawaited optional scenario hooks can mask failures or drop diagnostics.

this.desktopScenario?.close?.() (Line 1079) isn't awaited, unlike handleHttp which is (Line 1258). If close() rejects, control.close()'s returned promise rejects too; since this runs in the finally block of main(), that would replace the original test failure that was about to propagate via throw error — the real cause becomes invisible in CI.

Separately, desktopScenario?.diagnostics?.() (Line 2580) is invoked without awaiting inside the failure diagnostics payload. If a scenario author implements diagnostics() asynchronously, the stored value will be a Promise object, which JSON.stringify serializes as {}, silently losing scenario diagnostics exactly when they're most needed (on failure).

🔧 Proposed fix
   async close() {
     for (const response of this.blockedCloudResponses) response.destroy()
     this.blockedCloudResponses.clear()
-    this.desktopScenario?.close?.()
+    try {
+      await this.desktopScenario?.close?.()
+    } catch (error) {
+      console.error('Desktop scenario close() failed:', error)
+    }
     this.server.closeAllConnections?.()
-          desktopScenario: desktopScenario?.diagnostics?.() ?? null,
+          desktopScenario: (await desktopScenario?.diagnostics?.()) ?? null,

Also applies to: 2580-2580

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/e2e/desktop/task-flow.e2e.mjs` at line 1079, Await both optional
desktop scenario hooks: update the cleanup call around desktopScenario.close and
the failure diagnostics construction around desktopScenario.diagnostics to use
optional awaiting. Ensure asynchronous diagnostics are resolved before
serialization, and preserve the original test error when cleanup in main’s
finally block also fails by preventing the cleanup rejection from replacing the
already-propagating failure.

this.server.closeAllConnections?.()
this.controlServer.closeAllConnections?.()
await Promise.all([
Expand Down Expand Up @@ -1249,6 +1238,7 @@ class DesktopE2EServer {

const url = new URL(request.url ?? '/', this.url)
if (await this.handleControlRoute(request, response, url)) return
if (await this.desktopScenario?.handleHttp?.(request, response, url)) return

if (request.method === 'GET' && url.pathname === '/api/users/me') {
json(response, 200, {
Expand Down Expand Up @@ -1899,7 +1889,14 @@ async function main() {
cwd: workspacePath,
})

const control = new DesktopE2EServer(workspacePath, workspacePath)
const desktopScenario = await loadDesktopScenario(
process.env.WEWORK_E2E_DESKTOP_SCENARIO_MODULE,
{ uiTimeoutMs: UI_TIMEOUT_MS }
)
if (DESKTOP_SCENARIO_ONLY && !desktopScenario) {
throw new Error('Desktop scenario-only mode requires WEWORK_E2E_DESKTOP_SCENARIO_MODULE')
}
const control = new DesktopE2EServer(workspacePath, workspacePath, desktopScenario)
let app
let appBundlePath
let cloudEnvironment
Expand Down Expand Up @@ -1929,7 +1926,7 @@ async function main() {
const desktopApp = await buildDesktopApp(
control.controlUrl,
cloudEnvironment?.backendUrl ?? control.url,
cloudEnvironment?.authToken ?? 'wework-desktop-e2e-cloud-token',
cloudEnvironment?.authToken ?? desktopScenario?.authToken ?? 'wework-desktop-e2e-cloud-token',
appIdentifier
)
const appBinary = desktopApp.binaryPath
Expand All @@ -1956,6 +1953,7 @@ async function main() {
WEWORK_EXECUTOR_SIDECAR: executorBinary,
},
stdio: ['ignore', 'pipe', 'pipe'],
detached: process.platform !== 'win32',
})
await Promise.all([
appendProcessOutput(app.stdout, appLogPath),
Expand Down Expand Up @@ -1997,6 +1995,20 @@ async function main() {
control.failBlockedCloudModels()
await triggerModelReloadUntilCloudFailure(control)

if (desktopScenario) {
phase = 'desktop-extension-scenario'
await desktopScenario.verify(control)
Comment on lines +1998 to +2000

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run the injected scenario in cloud-only mode

When this runner is invoked with the existing --cloud-only mode, it returns from the cloud-project branch before reaching this new scenario invocation, so setting WEWORK_E2E_DESKTOP_SCENARIO_MODULE silently never runs the product scenario (and WEWORK_E2E_DESKTOP_SCENARIO_ONLY=true is likewise ignored after passing the module-presence check). This prevents product desktop scenarios that require the real cloud environment from being exercised by e2e:desktop:cloud; invoke the scenario before that return or explicitly reject the unsupported combination.

Useful? React with 👍 / 👎.

if (DESKTOP_SCENARIO_ONLY) {
await writeFile(
join(resultDir, 'model-requests.json'),
`${JSON.stringify(control.modelRequests, null, 2)}\n`,
'utf8'
)
console.log(`Wework desktop extension scenario E2E passed. Evidence: ${resultDir}`)
return
}
}

phase = 'remote-project-dialog'
await control.command('click', '[data-testid="projects-create-button"]')
await control.command('click', '[data-testid="project-create-remote-option"]')
Expand Down Expand Up @@ -2549,6 +2561,7 @@ async function main() {
phase,
scenario: control.scenario,
modelStage: control.modelStage,
desktopScenario: desktopScenario?.diagnostics?.() ?? null,
cloudModelStage: control.cloudModelStage,
scenarioRequestCounts: Object.fromEntries(
[...control.scenarioRequests.entries()].map(([name, requests]) => [
Expand Down Expand Up @@ -2582,15 +2595,18 @@ async function main() {
throw error
} finally {
await cloudEnvironment?.stop()
await stopProcess(app)
await stopProcessGroup(app)
await control.close()
if (appBundlePath) {
spawnSync(MACOS_LAUNCH_SERVICES_REGISTER, ['-u', appBundlePath])
}
}
}

main().catch(error => {
console.error(error instanceof Error ? (error.stack ?? error.message) : error)
process.exitCode = 1
})
main().then(
() => process.stdout.write('', () => process.exit(0)),
error => {
const message = error instanceof Error ? (error.stack ?? error.message) : String(error)
process.stderr.write(`${message}\n`, () => process.exit(1))
}
)
Loading
Loading