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
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,48 @@ try {
await openEvidence();
const refresh = review.getByRole("button", { name: locale === "en" ? "Refresh snapshot" : "刷新快照", exact: true });
const exportButton = review.getByRole("button", { name: locale === "en" ? "Export delivery snapshot" : "导出交付快照", exact: true });
if (locale === "en") {
const requirement = (id, description, status) => ({
id, description, status, planning_satisfied: status === "accepted",
});
const collision = {
schema_version: "loopx_goal_outcomes_v0", revision: 1, current_lifetime: true,
execution_authority_granted: false, goal_completion_assessed: false,
outcomes: [
{
id: "plan", owner_agent_id: "author", objective: "First colliding outcome", revision: 1,
requirements: [
requirement("section:opening", "Colon in the requirement id", "failed"),
requirement("detail", "Second unmet requirement", "failed"),
requirement("closing", "Third unmet requirement", "failed"),
],
todo_ids: [], sources: [], work: [],
},
{
id: "plan:section", owner_agent_id: "author", objective: "Second colliding outcome", revision: 1,
requirements: [requirement("opening", "Unrelated met requirement", "accepted")],
todo_ids: [], sources: [], work: [],
},
],
};
const projected = {...acceptance, goal_acceptance_contract: {outcome_requirements: collision}};
response = {...snapshot, acceptance: projected};
payload.run_history.goals[0].acceptance_observation = projected;
await page.reload({waitUntil: "networkidle"});
const outcomes = page.locator(".goal-outcomes");
await outcomes.waitFor();
const visibleRows = outcomes.locator(".goal-outcome-criteria > li:not([hidden])");
assert.equal(await visibleRows.count(), 3, "Colon-bearing outcome/requirement pairs keep only three preview rows");
await outcomes.locator(":scope > .goal-outcomes-more").click();
assert.equal(await visibleRows.count(), 4, "Expanding reveals the unrelated fourth requirement");
response = structuredClone(snapshot);
payload.run_history.goals[0].acceptance_observation = acceptance;
await page.reload({waitUntil: "networkidle"});
if (await navigation.isVisible()) await navigation.click();
await page.locator(".personal-goal-link").first().click();
await page.getByRole("button", {name: "Overview", exact: true}).click();
await openEvidence();
}
const openAdvanced = async () => {
await openInspection();
const advanced = section.locator(".delivery-check-advanced");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export async function verifyWorkspaceLocales({ browser, url, installApi, outputD
await page.getByRole("radio", { name: /Simplified Chinese/ }).click();
assert.equal(await page.evaluate(() => localStorage.getItem("loopx-pw-locale")), "zh-CN");
await page.reload({ waitUntil: "networkidle" });
await page.getByRole("button", { name: "返回工作区", exact: true }).click();
await page.getByTestId("personal-goal-home").waitFor();
assert.equal(await page.locator("html").getAttribute("lang"), "zh-CN");
assert.match(await page.getByRole("region", { name: "控制面来源" }).innerText(), /本机/);
Expand All @@ -105,6 +106,7 @@ export async function verifyWorkspaceLocales({ browser, url, installApi, outputD
await page.getByRole("button", { name: /语言/ }).click();
await page.getByRole("radio", { name: /English/ }).click();
await page.reload({ waitUntil: "networkidle" });
await page.getByRole("button", { name: "Back to workspace", exact: true }).click();
await page.getByTestId("personal-goal-home").waitFor();
assert.equal(await page.locator("html").getAttribute("lang"), "en");
assert.equal(await page.evaluate(() => localStorage.getItem("loopx-pw-locale")), "en");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ function stateLabel(item: Requirement, fresh: boolean, zh: boolean) {
: stateLabels(zh)[displayState(item)];
return fresh ? label : `${zh ? "历史" : "Recorded"} · ${label}`;
}
function requirementKey(outcome: OutcomeRequirements["outcomes"][number], requirement: Requirement): string {
// Native IDs are opaque and may contain colons. Keep both identities
// unambiguous instead of relying on a separator the vocabulary forbids.
return JSON.stringify([outcome.id, requirement.id]);
}

/** One native declaration/readback owner. The form only appends a requirement. */
export function GoalOutcomeRequirements({goalId, declaration, fresh, canManage, providerRevision, onRefresh}: {
Expand All @@ -38,7 +43,7 @@ export function GoalOutcomeRequirements({goalId, declaration, fresh, canManage,
|| current && requirement.agent_review?.basis_current && requirement.agent_review.conclusion === "unmet")
.sort((a, b) => stateOrder.indexOf(displayState(a.requirement)) - stateOrder.indexOf(displayState(b.requirement)));
const preview = attention.slice(0, 3);
const visible = new Set((all ? rows : preview).map(row => `${row.outcome.id}:${row.requirement.id}`));
const visible = new Set((all ? rows : preview).map(row => requirementKey(row.outcome, row.requirement)));
const labels = stateLabels(zh);
if (!declaration.outcomes.length) return null;
return <section className={`goal-outcomes${all ? " is-expanded" : ""}`} aria-label={zh ? "支撑成果" : "Supporting outcomes"}>
Expand Down Expand Up @@ -92,17 +97,17 @@ function Outcome({goalId, outcome, fresh, canManage, revision, onRefresh, zh, vi
await onRefresh();
} finally {setBusy(false);}
}
const shown = all || outcome.requirements.some(item => visible.has(`${outcome.id}:${item.id}`));
const shown = all || outcome.requirements.some(item => visible.has(requirementKey(outcome, item)));
return <article className="goal-outcome" hidden={!shown}>
<header><h5>{outcome.objective}</h5><span>{outcome.requirements.length} {zh ? "项要求" : "requirements"}</span></header>
{stateOrder.map(state => {
const requirements = outcome.requirements.filter(item => displayState(item) === state);
if (!requirements.length) return null;
const hasVisible = requirements.some(item => visible.has(`${outcome.id}:${item.id}`));
const hasVisible = requirements.some(item => visible.has(requirementKey(outcome, item)));
return <details className="goal-outcome-status-group" key={state} hidden={!hasVisible} open={!all || groupsOpen[state] === true}
onToggle={event => {const open = event.currentTarget.open; if (all) setGroupsOpen(previous => ({...previous, [state]: open}));}}>
<summary hidden={!all}>{!fresh ? (zh ? "历史 · " : "Recorded · ") : ""}{stateLabels(zh)[state]} · {requirements.length}</summary>
<ul className="goal-overview-criteria goal-outcome-criteria">{requirements.map(requirement => <li key={requirement.id} hidden={!visible.has(`${outcome.id}:${requirement.id}`)}>
<ul className="goal-overview-criteria goal-outcome-criteria">{requirements.map(requirement => <li key={requirement.id} hidden={!visible.has(requirementKey(outcome, requirement))}>
<RequirementRow requirement={requirement} fresh={fresh} zh={zh}/>
</li>)}</ul>
</details>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1189,8 +1189,13 @@ export function PersonalWorkspacePage({
setSelection(target);
}
useEffect(() => {
const restore = () => setSelectionState({scope: currentSelectionScope.current.scope,
value: readSettingsRoute(new URL(window.location.href))});
const restore = () => {
const target = readSettingsRoute(new URL(window.location.href));
// Task history has its own route reader. Leaving settings may clear its
// selection, but an unrelated history event must not clear a restored task.
setSelectionState(current => target ? {scope: currentSelectionScope.current.scope, value: target}
: current?.value?.kind === "settings" ? null : current);
};
window.addEventListener("popstate", restore);
return () => window.removeEventListener("popstate", restore);
}, []);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions docs/product/release-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@ expected public tag is `vX.Y.Z` for that version.

Before moving `stable`, maintainers should:

- run the complete packaged Personal Workspace journey against the exact
candidate before publishing a release, including every catalog scenario and
the companion flows in `npm run smoke:personal-workspace-packaged`;
installation, upgrade and Host checks alone do not qualify the page. The
`install_upgrade_host` result uses `release_install_upgrade_host_summary_v1`
and requires `packaged_frontend_passed=true` as well as the three existing
checks. Missing, failed or skipped page validation holds release readiness;
- bump `loopx.__version__` and `pyproject.toml` together when user-visible
release behavior changes;
- before freezing the candidate, align the existing canonical help catalog,
Expand Down
Loading
Loading