Skip to content

Commit 1ea60ca

Browse files
committed
Merge origin/main into issue 4800 local-state repair
Signed-off-by: Lihua <1017343802@qq.com>
2 parents 85297bc + f679911 commit 1ea60ca

55 files changed

Lines changed: 2100 additions & 220 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/presentation/dashboard/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"smoke:home-browser": "node ../../../examples/dashboard-home-browser-smoke.mjs",
3434
"smoke:home-route": "rm -rf /tmp/loopx-home-route-smoke && tsc --ignoreConfig --target ES2022 --module CommonJS --moduleResolution Node --ignoreDeprecations 6.0 --skipLibCheck --strict --outDir /tmp/loopx-home-route-smoke smoke/home-route-smoke.ts && node /tmp/loopx-home-route-smoke/home-route-smoke.js",
3535
"smoke:delegation-preflight": "rm -rf node_modules/.cache/loopx-delegation-preflight-smoke && tsc --ignoreConfig --target ES2022 --module NodeNext --moduleResolution NodeNext --jsx react-jsx --skipLibCheck --strict --rootDir . --outDir node_modules/.cache/loopx-delegation-preflight-smoke smoke/delegation-preflight-smoke.tsx src/data/delegation-preflight.ts src/features/personal-workspace/delegation-preflight-status.tsx && node node_modules/.cache/loopx-delegation-preflight-smoke/smoke/delegation-preflight-smoke.js",
36+
"smoke:delegation-preflight-browser": "node --experimental-strip-types smoke/delegation-preflight-browser-smoke.mjs",
3637
"smoke:personal-workspace": "npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser-smoke.mjs",
3738
"smoke:workspace-locale": "LOOPX_PERSONAL_WORKSPACE_SCENARIO=workspace-locale node ../../../examples/personal-workspace-browser-smoke.mjs",
3839
"smoke:personal-workspace-packaged": "npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs",
@@ -52,7 +53,7 @@
5253
"smoke:task-board-scroll": "node smoke/task-board-scroll-smoke.mjs",
5354
"smoke:usage-progress": "rm -rf /tmp/loopx-usage-progress-smoke && tsc --ignoreConfig --target ES2022 --module CommonJS --moduleResolution Node --ignoreDeprecations 6.0 --resolveJsonModule --esModuleInterop --jsx react-jsx --skipLibCheck --strict --outDir /tmp/loopx-usage-progress-smoke smoke/usage-progress-smoke.ts src/features/personal-workspace/personal-workspace-model.ts && node /tmp/loopx-usage-progress-smoke/apps/presentation/dashboard/smoke/usage-progress-smoke.js",
5455
"smoke:action-review-plan": "rm -rf .local/action-review-plan-smoke && tsc --ignoreConfig --target ES2022 --module ES2022 --moduleResolution Bundler --skipLibCheck --strict --rootDir ../../.. --outDir .local/action-review-plan-smoke smoke/action-review-plan-smoke.ts ../../../loopx/control_plane/presentation/action_review_plan.ts src/vite-env.d.ts && node .local/action-review-plan-smoke/apps/presentation/dashboard/smoke/action-review-plan-smoke.js",
55-
"smoke:attention-details": "tsc --ignoreConfig --target ES2022 --module CommonJS --moduleResolution Node --ignoreDeprecations 6.0 --resolveJsonModule --esModuleInterop --skipLibCheck --strict --outDir /tmp/loopx-attention-details-smoke smoke/attention-details-smoke.ts && NODE_PATH=\"$PWD/node_modules\" node /tmp/loopx-attention-details-smoke/apps/presentation/dashboard/smoke/attention-details-smoke.js",
56+
"smoke:attention-details": "tsc --ignoreConfig --target ES2022 --module CommonJS --moduleResolution Node --ignoreDeprecations 6.0 --resolveJsonModule --esModuleInterop --jsx react-jsx --skipLibCheck --strict --outDir /tmp/loopx-attention-details-smoke smoke/attention-details-smoke.ts && NODE_PATH=\"$PWD/node_modules\" node /tmp/loopx-attention-details-smoke/apps/presentation/dashboard/smoke/attention-details-smoke.js",
5657
"smoke:delivery-review": "tsc --ignoreConfig --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck --strict --rootDir src --outDir node_modules/.cache/delivery-review src/data/delivery-review.ts src/data/goal-acceptance-observation.ts src/features/personal-workspace/delivery-review-copy.ts && node smoke/delivery-review-smoke.mjs",
5758
"smoke:team-artifact-comparison": "tsc --ignoreConfig --target ES2022 --module ES2022 --moduleResolution Bundler --skipLibCheck --strict --rootDir src --outDir node_modules/.cache/team-comparison src/features/personal-workspace/team-artifact-comparison.ts src/vite-env.d.ts && node smoke/team-artifact-comparison-smoke.mjs",
5859
"smoke:team-report": "tsc --ignoreConfig --target ES2022 --module ES2022 --moduleResolution Bundler --jsx react-jsx --skipLibCheck --strict --rootDir src --outDir node_modules/.cache/team-report src/features/personal-workspace/team-artifact-content.tsx src/vite-env.d.ts && node smoke/team-report-smoke.mjs",

‎apps/presentation/dashboard/smoke/attention-details-smoke.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { createElement } from "react";
2+
import { renderToStaticMarkup } from "react-dom/server";
3+
import { AttentionDetailCard } from "../src/features/personal-workspace/attention-detail-card";
4+
import { WorkspaceI18nProvider } from "../src/features/personal-workspace/i18n";
15
import { todoItemSchema } from "../src/data/status";
26
import { attentionDetails, attentionSuccessor, canReviewAttention, refreshAttention, sourceAttention } from "../src/features/personal-workspace/attention-details";
37
import { normalizePersonalHomeModel, type WorkspaceAttention } from "../src/features/personal-workspace/personal-workspace-model";
@@ -55,3 +59,14 @@ const healthySource = sourceAttention(row, "source-b", true, "Healthy Goal");
5559
assert(canReviewAttention(refreshAttention(healthySource, [failedSource, healthySource])), "another source failure cannot fence healthy source");
5660
const healthyGoal = sourceAttention({ ...row, goalId: "healthy-goal" }, "source-a", true);
5761
assert(canReviewAttention(refreshAttention(healthyGoal, [failedSource, healthyGoal])), "another Goal read failure cannot fence healthy Goal");
62+
63+
const longRequest = "Review the public release evidence. ".repeat(12) + "Publish version 2.0 to stable only after acceptance.";
64+
const longSource = todoItemSchema.parse({ ...source, text: longRequest });
65+
const longRow = { ...row, text: "Release review", details: attentionDetails(longSource) };
66+
const longModel = normalizePersonalHomeModel({ blockingTodoCount: 1, goals: [], openUserTodoCount: 1, userTodos: [longRow], attentionHistory: [longRow] });
67+
assert(longModel.userTodos[0]?.details?.requestText === longRequest, "App decision detail retains the object after the short card label");
68+
assert(longModel.userTodos[0]?.details?.evidence === source.evidence, "App decision evidence retained with the full request");
69+
70+
const markup = renderToStaticMarkup(createElement(WorkspaceI18nProvider, null, createElement(AttentionDetailCard, { item: longModel.userTodos[0] })));
71+
assert(markup.includes("Publish version 2.0 to stable only after acceptance."), "rendered App detail contains object beyond compact label");
72+
assert(markup.includes(source.evidence), "rendered App detail contains evidence");
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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+
}

‎apps/presentation/dashboard/smoke/delegation-preflight-smoke.tsx‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,18 @@ for (const [zh, expectedLabel, expectedBoundary] of [
3636
}
3737
if (Object.values(unavailable.effects).some(Boolean)) throw new Error("authority-unavailable response must report zero effects");
3838

39-
console.log("delegation authority-unavailable preflight smoke passed");
39+
for (const workspace_state of ["missing", "not_directory", "unavailable"] as const) {
40+
const check: DelegationPreflight = {...unavailable, state: "workspace_unavailable",
41+
workspace_state, workspace_next_action: "review_operator_workspace_binding",
42+
authority_ready: null, authority_reason: null, authority_state: "uninspected",
43+
authority_next_action: "none"};
44+
for (const zh of [true, false]) {
45+
const html = renderToStaticMarkup(<DelegationPreflightStatus check={check} zh={zh}/>);
46+
if (!html.includes(zh ? "绑定工作目录不可用" : "Bound workspace unavailable")) throw new Error("missing workspace fault label");
47+
if (!html.includes(zh ? "核对原执行配置的工作目录" : "review the workspace in the original execution configuration")) throw new Error("missing workspace recovery action");
48+
if (!html.includes(zh ? "权限、验收与运行时尚未检查" : "Authority, acceptance and runtime uninspected")) throw new Error("workspace fault misreports inspection");
49+
if (/晋级|promotion|Canonical authority unavailable|缺少规范权限状态/.test(html)) throw new Error("workspace fault suggested authority repair/promotion");
50+
}
51+
}
52+
53+
console.log("delegation authority/workspace-unavailable preflight smoke passed");

‎apps/presentation/dashboard/src/data/delegation-preflight.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
export type DelegationPreflight = {
2-
state: "authority_unavailable" | "turn_blocked" | "acceptance_unavailable" | "runtime_unavailable" | "runtime_unverified" | "launchable";
2+
state: "workspace_unavailable" | "authority_unavailable" | "turn_blocked" | "acceptance_unavailable" | "runtime_unavailable" | "runtime_unverified" | "launchable";
3+
workspace_state?: "missing" | "not_directory" | "unavailable";
4+
workspace_next_action?: "review_operator_workspace_binding";
35
turn_eligible: boolean;
46
acceptance_ready: boolean;
57
turn_route: string | null;
6-
authority_ready: boolean;
8+
authority_ready: boolean | null;
79
authority_reason: string | null;
8-
authority_state: "promotion_required" | "unavailable" | "promoted";
10+
authority_state: "promotion_required" | "unavailable" | "promoted" | "uninspected";
911
authority_next_action: "preview_reviewed_goal_authority_promotion" | "repair_canonical_authority" | "none";
1012
promotion_from_surface_allowed: false;
1113
executor: {host: string; available: boolean | null; reason: string | null; profile: string | null} | null;

‎apps/presentation/dashboard/src/features/personal-workspace/attention-detail-card.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export function AttentionDetailCard({ item, successor, onSelect }: {
1010
const detail = item.details;
1111
return <section className="personal-detail-card" aria-label={t("attentionDetail.title")}>
1212
<h3>{t("attentionDetail.title")}</h3>
13+
{detail?.requestText && detail.requestText !== item.text ? <p style={{ whiteSpace: "pre-wrap", overflowWrap: "anywhere" }}>{detail.requestText}</p> : null}
1314
<dl>
1415
<div><dt>{t("attentionDetail.request")}</dt><dd>{t(detail?.interaction === "decision" ? "attentionDetail.decision" : "attentionDetail.unknownRequest")}</dd></div>
1516
<div><dt>{t("common.status")}</dt><dd>{t(`attentionDetail.${detail?.lifecycle ?? "unknown"}`)}</dd></div>

‎apps/presentation/dashboard/src/features/personal-workspace/attention-details.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { WorkspaceAttention } from "./personal-workspace-model";
44
export type AttentionDetails = {
55
interaction: "decision" | "unknown";
66
lifecycle: "open" | "closed" | "deferred" | "superseded" | "unknown" | "unavailable";
7+
requestText?: string | null;
78
reason: string | null;
89
evidence: string | null;
910
blocksAgent: string | null;
@@ -28,6 +29,7 @@ export function attentionDetails(todo: Record<string, unknown>): AttentionDetail
2829
: todo.status === "deferred" ? "deferred"
2930
: todo.done === true || ["done", "completed", "closed", "archived"].includes(String(todo.status)) ? "closed"
3031
: todo.status === "open" || todo.status === "blocked" ? "open" : "unknown",
32+
requestText: text(todo.text),
3133
reason: text(todo.note),
3234
evidence: text(todo.evidence),
3335
blocksAgent: text(todo.blocks_agent),

0 commit comments

Comments
 (0)