|
| 1 | +/** Exercise the existing packaged Team execution UI with the shared TS read model. */ |
| 2 | +import assert from "node:assert/strict"; |
| 3 | +import {mkdir} from "node:fs/promises"; |
| 4 | +import {resolve} from "node:path"; |
| 5 | +import {delegationPreflight} from "../../../../loopx/control_plane/collaboration/delegation.ts"; |
| 6 | +import {launchBrowser, loadPlaywright, waitForHttp} from "../../../../examples/dashboard-browser-smoke-support.mjs"; |
| 7 | +import {outputDir, packaged, port, startServer} from "../../../../examples/personal-workspace-browser/fixture.mjs"; |
| 8 | +import {openWorkspacePage} from "../../../../examples/personal-workspace-browser/scenario-context.mjs"; |
| 9 | + |
| 10 | +await mkdir(outputDir, {recursive: true}); |
| 11 | +const server = startServer(); |
| 12 | +let browser; |
| 13 | +try { |
| 14 | + const url = `http://127.0.0.1:${port}/${packaged ? "chat/" : ""}?statusUrl=/status.json`; |
| 15 | + await waitForHttp(url); |
| 16 | + browser = await launchBrowser(loadPlaywright().chromium); |
| 17 | + for (const zh of [true, false]) { |
| 18 | + const context = await openWorkspacePage({newPage: options => browser.newPage({ |
| 19 | + ...options, locale: zh ? "zh-CN" : "en-US", |
| 20 | + })}, url); |
| 21 | + const {api, page} = context; |
| 22 | + let state = "legacy_error"; |
| 23 | + const inspected = []; |
| 24 | + try { |
| 25 | + await page.route("**/api/chat/sessions/*/loopx", async route => { |
| 26 | + const body = route.request().method() === "POST" ? route.request().postDataJSON() : {}; |
| 27 | + if (body.operation !== "inspect" || state === "available") return route.fallback(); |
| 28 | + inspected.push(body.binding_id); |
| 29 | + if (state === "legacy_error") return route.fulfill({status: 409, |
| 30 | + json: {error: "delegation workspace unavailable"}}); |
| 31 | + const binding = {id: body.binding_id, agent_id: "fixture-worker", todo_id: "fixture-task"}; |
| 32 | + const check = delegationPreflight({binding, workspace: {state}, authority: null, |
| 33 | + preview: null, acceptance: null, validation_files_current: false}); |
| 34 | + await route.fulfill({json: check}); |
| 35 | + }); |
| 36 | + await page.locator(".personal-goal-link", {hasText: "Product Release"}).click(); |
| 37 | + await page.getByRole("navigation", {name: zh ? "Goal 视图" : "Goal view"}) |
| 38 | + .getByRole("button", {name: zh ? "对话" : "Chat", exact: true}).click(); |
| 39 | + await page.getByRole("button", {name: zh ? "开启 LoopX 模式" : "Enable LoopX", exact: true}).click(); |
| 40 | + const settings = page.locator(".goal-loopx-mode-settings"); |
| 41 | + await settings.getByLabel(zh ? "已注册的协调身份" : "Registered coordinator").selectOption("lead"); |
| 42 | + await settings.getByLabel(zh ? "协调员总 token 额度" : "Coordinator total token allowance").fill("100000"); |
| 43 | + assert.equal(await settings.getByLabel(zh ? "成员执行绑定文件(Goal 配置)" : "Member execution bindings (Goal configuration)").isEditable(), false); |
| 44 | + await settings.getByRole("button", {name: zh ? "保存设置" : "Save settings", exact: true}).click(); |
| 45 | + await page.getByRole("button", {name: zh ? "团队执行情况" : "Team execution", exact: true}).click(); |
| 46 | + const team = page.getByRole("region", {name: zh ? "团队执行详情" : "Team execution details"}); |
| 47 | + // Render the error response reproduced on the base, not a baseline UI build. |
| 48 | + await team.getByRole("button", {name: zh ? "检查整个团队" : "Check whole team", exact: true}).click(); |
| 49 | + await team.locator(".goal-team-bindings > li > p[role=alert]").nth(2).waitFor(); |
| 50 | + for (const [size, viewport] of [["desktop", {width: 1512, height: 982}], ["mobile", {width: 390, height: 844}]]) { |
| 51 | + await page.setViewportSize(viewport); |
| 52 | + await page.screenshot({path: resolve(outputDir, `workspace-preflight-before-response-${zh ? "zh" : "en"}-${size}.png`), animations: "disabled"}); |
| 53 | + } |
| 54 | + assert.equal(context.errors.length, 3, "Only the three injected baseline HTTP refusals are expected"); |
| 55 | + for (const error of context.errors) assert.match(error, |
| 56 | + /^Failed to load resource: the server responded with a status of 409 \(Conflict\)$/); |
| 57 | + context.errors.splice(0); // Keep subsequent recovery checks error-free. |
| 58 | + for (const fault of ["missing", "not_directory", "unavailable"]) { |
| 59 | + state = fault; |
| 60 | + await team.getByRole("button", {name: zh ? "检查整个团队" : "Check whole team", exact: true}).click(); |
| 61 | + await team.getByRole("button", {name: zh ? "检查整个团队" : "Check whole team", exact: true}).waitFor({state: "visible"}); |
| 62 | + const expected = zh |
| 63 | + ? {missing: "目录不存在", not_directory: "绑定位置不是目录", unavailable: "目录无法读取"}[fault] |
| 64 | + : {missing: "Directory missing", not_directory: "Bound location is not a directory", unavailable: "Directory could not be read"}[fault]; |
| 65 | + const observations = team.locator(".goal-team-bindings > li > p[role=status]"); |
| 66 | + await observations.filter({hasText: expected}).nth(2).waitFor(); |
| 67 | + assert.equal(await observations.count(), 3); |
| 68 | + assert.equal(new Set(inspected.slice(-3)).size, 3); |
| 69 | + const content = await observations.first().innerText(); |
| 70 | + assert.match(content, zh ? /核对原执行配置的工作目录/ : /review the workspace in the original execution configuration/); |
| 71 | + assert.match(content, zh ? /权限、验收与运行时尚未检查/ : /Authority, acceptance and runtime uninspected/); |
| 72 | + assert.doesNotMatch(content, /晋级|promotion|Canonical authority unavailable|缺少规范权限状态/); |
| 73 | + } |
| 74 | + const dialog = page.getByRole("dialog", {name: zh ? "团队执行情况" : "Team execution", exact: true}); |
| 75 | + for (const [size, viewport] of [["desktop", {width: 1512, height: 982}], ["mobile", {width: 390, height: 844}]]) { |
| 76 | + await page.setViewportSize(viewport); |
| 77 | + assert(await dialog.evaluate(el => el.scrollWidth <= el.clientWidth), "Workspace recovery copy must not overflow the dialog"); |
| 78 | + await page.screenshot({path: resolve(outputDir, `workspace-preflight-${zh ? "zh" : "en"}-${size}.png`), animations: "disabled"}); |
| 79 | + } |
| 80 | + const member = team.locator(".goal-team-bindings > li").filter({hasText: "local-analyst"}); |
| 81 | + await member.locator("summary").click(); |
| 82 | + state = "available"; |
| 83 | + await member.getByRole("button", {name: zh ? "重新检查此成员" : "Recheck this member", exact: true}).click(); |
| 84 | + await member.getByText(zh ? /运行时可用性尚未验证/ : /Runtime availability unverified/).waitFor(); |
| 85 | + assert.equal(await member.getByText(zh ? /绑定工作目录不可用/ : /Bound workspace unavailable/).count(), 0); |
| 86 | + assert.equal(api.turnRequests.length, 0, "Inspection/recovery must not start model work"); |
| 87 | + assert.equal(context.errors.length, 0, context.errors.join("\n")); |
| 88 | + console.log(`workspace preflight ${zh ? "Chinese" : "English"}: three faults, exact recheck and desktop/mobile passed`); |
| 89 | + } finally { |
| 90 | + await context.close(); |
| 91 | + } |
| 92 | + } |
| 93 | +} finally { |
| 94 | + await browser?.close(); |
| 95 | + server.kill("SIGTERM"); |
| 96 | +} |
0 commit comments