{outcome.objective}
{outcome.requirements.length} {zh ? "项要求" : "requirements"}{!fresh ? (zh ? "历史 · " : "Recorded · ") : ""}{stateLabels(zh)[state]} · {requirements.length}
-- {requirements.map(requirement =>
-
+ const counts = stateOrder.map(state => ({state, count: outcome.requirements.filter(item => displayState(item) === state).length}));
+ const gaps = outcome.requirements.filter(item => item.agent_review?.basis_current && item.agent_review.conclusion === "unmet").length;
+ return
+ ; } @@ -136,7 +132,7 @@ function RequirementRow({requirement, fresh, zh}: {requirement: Requirement; fre const [open, setOpen] = useState(false); const review = requirement.agent_review; returnsetOpen(event.currentTarget.open)}> +
+{outcome.objective}
+ + {busy || pending && !open ? {busy ? (zh ? "正在保存" : "Saving") : (zh ? "结果待确认" : "Result unconfirmed")} : text && !open ? {zh ? "未发送草稿" : "Unsent draft"} : null} + {gaps ? {zh ? `自查缺口 ${gaps}` : `${gaps} Agent-reported ${gaps === 1 ? "gap" : "gaps"}`} : null} + {counts.filter(item => item.count).map(item => {stateLabels(zh)[item.state]} {item.count})} + {!outcome.requirements.length ? {zh ? "尚无要求" : "No requirements yet"} : null} + +++- {stateOrder.flatMap(state => outcome.requirements.filter(item => displayState(item) === state)).map(requirement =>
-
{zh ? "关联工作与来源" : "Linked work and sources"}
-{outcome.objective}
{outcome.owner_agent_id} · {zh ? "版本" : "Revision"} {outcome.revision}
+{/* Keep an existing draft readable while its same-source write basis is unavailable. */} - {canManage || text || pending ?{zh ? "关联工作与来源" : "Linked work and sources"}
+{outcome.owner_agent_id} · {zh ? "版本" : "Revision"} {outcome.revision}
- {outcome.work.map(work =>
{work.todo_id}· {work.status} )}
{zh ? "尚未关联执行任务" : "No execution task linked"}
: null}- {outcome.sources.map(source =>
- {source.summary} )}
{zh ? "补充要求" : "Add a requirement"}
+ {canManage || text || pending ?: null} {notice ?{zh ? "补充要求" : "Add a requirement"}
{notice}
: null} +setOpen(event.currentTarget.open)}> -
+{requirement.description}{stateLabel(requirement, fresh, zh)}
{review && (review.note.trim() || review.evidence_refs.length > 0) ?{requirement.description}{stateLabel(requirement, zh)} {zh ? "上次 Agent 自查" : "Previous Agent review"} · {({met: zh ? "满足" : "Met", unmet: zh ? "有缺口" : "Unmet", uncertain: zh ? "不确定" : "Uncertain"})[review.conclusion]}
{review.note.trim() ?{review.note}
: null} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.css b/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.css index fc74ef6558..1ea48763c5 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-overview.css @@ -166,11 +166,17 @@ .goal-outcomes .is-stale, .goal-outcomes .is-held { color: var(--pw-caution); } .goal-outcome { padding: 10px 0; border-top: 1px solid var(--pw-line); min-width: 0; } .goal-outcome[hidden], .goal-outcome-criteria > li[hidden], .goal-outcome > details[hidden] { display: none; } -.goal-outcome-status-group > summary[hidden] { display: none; } -.goal-outcome-status-group > summary { color: var(--pw-muted); font-size: var(--text-caption); cursor: pointer; } -.goal-outcome > header { display: flex; align-items: start; gap: 16px; padding-bottom: 4px; } +.goal-outcome .goal-outcome-disclosure { margin: 0; } +.goal-outcome-disclosure > summary { display: grid; grid-template-columns: 15px minmax(0, 1fr) auto; align-items: start; gap: 8px; padding: 6px 0; min-height: 40px; list-style: none; cursor: pointer; } +.goal-outcome-disclosure > summary::-webkit-details-marker { display: none; } +.goal-outcome-disclosure > summary > svg { margin-top: 4px; color: var(--pw-faint); transform: rotate(-90deg); } +.goal-outcome-disclosure[open] > summary > svg { transform: rotate(0); } +.goal-outcome-disclosure > summary:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: 3px; border-radius: 4px; } .goal-outcome h5 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; flex: 1; min-width: 0; font: inherit; font-weight: 500; line-height: var(--leading-body); margin: 0; } -.goal-outcome > header > span { flex: none; color: var(--pw-faint); font-size: var(--text-caption); line-height: var(--leading-body); } +.goal-outcome-disclosure[open] > summary h5 { display: block; } +.goal-outcome-summary-states { display: flex; flex-wrap: wrap; justify-content: end; gap: 4px 12px; max-width: 28em; color: var(--pw-muted); font-size: var(--text-caption); line-height: var(--leading-body); } +.goal-outcome-summary-states > span { white-space: nowrap; } +.goal-outcome-body { padding: 4px 0 4px 23px; } .goal-outcome .goal-outcome-criteria { border: 0; } .goal-outcome .goal-outcome-criteria > li { display: block; padding: 0; min-height: 0; border: 0; } .goal-outcome .goal-outcome-requirement { margin: 0; } @@ -193,7 +199,9 @@ .goal-outcome textarea { width: 100%; min-height: 72px; padding: 8px; resize: vertical; border: 1px solid var(--pw-line); border-radius: 6px; background: transparent; color: inherit; font: inherit; } .goal-outcome code { overflow-wrap: anywhere; } @media (max-width: 640px) { - .goal-outcome > header { gap: 8px; } + .goal-outcome-disclosure > summary { grid-template-columns: 15px minmax(0, 1fr); } + .goal-outcome-summary-states { grid-column: 2; justify-content: start; max-width: none; } + .goal-outcome-body { padding-left: 0; } .goal-outcome-requirement > summary { grid-template-columns: 15px minmax(0, 1fr); gap: 4px 8px; } .goal-outcome-requirement > summary > small { grid-column: 2; max-width: none; padding-top: 0; } } diff --git a/docs/development/design.md b/docs/development/design.md index 15bea6904a..496d6e6d76 100644 --- a/docs/development/design.md +++ b/docs/development/design.md @@ -114,13 +114,17 @@ Owner-facing state follows four rules: Show an unavailable or partial count honestly; put full current briefs, linked work, provenance and original conversations behind row expansion. A Goal's supporting outcomes need the same bounded reading hierarchy: show - requirement-state counts once, then at most three requirements needing attention - in the default overview. Group them by their declared outcome; keep failed, - held, changed and partial observations visible in the summary. Make the full - collection available in one expansion, including unverified and passing work. - Clamp long requirement text only in collapsed rows; expansion restores its - complete text and evidence. Explain Agent review versus independent checks once - at section level. Overview check details stay collapsed at every viewport; + requirement-state counts once, then at most three compact outcome rows in the + default overview, prioritizing declared failures, held or changed checks and + Agent-reported gaps. Each **whole outcome body** starts collapsed: its title + and meaningful states remain visible; expanding it reveals its requirements, + with individual rationale and evidence available in place. Collapsing only + evidence, status groups or a few requirement rows does not satisfy this rule. + Keep failed, held, changed, partial and unconfirmed-operation observations in + the summary. Make the full outcome collection available in one expansion, + including unverified and passing work. Clamp long text only in collapsed rows; + expansion restores it completely. Explain Agent review versus independent + checks once at section level. Overview check details stay collapsed at every viewport; an operation's consequences and actual failures remain visible. - **Owner language first.** Primary copy uses the owner's words. Protocol ids, gate kinds, preview mechanics and executor profiles belong in a collapsed @@ -169,6 +173,14 @@ Keep the summary's actual verification state visible; open the chosen item in place, with its complete content and original evidence reachable. A disclosure must not duplicate the same content above and below its trigger. +Read freshness and verification validity are different facts. Reuse the existing +cached, coalesced read owner for quiet background updates; pause polling in the +background and back off after read failures. Preserve scroll, open items and +drafts across same-source updates. Show an unavailable read or historical source +once for the affected region rather than repeating a refresh instruction on +every row. A fresh read never validates a changed requirement or starts a check, +model or worker; those operations retain their existing explicit controls. + As data grows, preserve access to every item through the existing list, pagination or history, with honest coverage. Do not silently keep only recent or successful items. Reduce default expansion and repetition before adding diff --git a/examples/cli-project-lifecycle-command-modularization-smoke.py b/examples/cli-project-lifecycle-command-modularization-smoke.py index 54ecbfab35..341ecce76d 100644 --- a/examples/cli-project-lifecycle-command-modularization-smoke.py +++ b/examples/cli-project-lifecycle-command-modularization-smoke.py @@ -6,10 +6,18 @@ import subprocess import sys import tempfile +from contextlib import ExitStack from pathlib import Path +from pytest import MonkeyPatch + +from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection +from loopx.control_plane.effect_runtime import restart_effect_runtime +from loopx.control_plane.todos.active_state_todo_parser import parse_todo_source +from loopx.control_plane.todos.todo_summary import canonical_todo_read_record, structured_todo_item ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "tests" / "control_plane")) CLI = ROOT / "loopx" / "cli.py" MODULE = ROOT / "loopx" / "cli_commands" / "project_lifecycle.py" PROJECT_SKILL = ROOT / "skills" / "loopx-project" / "SKILL.md" @@ -59,6 +67,8 @@ def require_json_success(result: subprocess.CompletedProcess[str]) -> dict[str, def write_fixture(project: Path) -> tuple[Path, Path, Path]: + from canonical_authority_fixture import initialize_canonical_authority + runtime_root = project / "runtime" registry_path = project / ".loopx" / "registry.json" state_file = project / ".codex" / "goals" / GOAL_ID / "ACTIVE_GOAL_STATE.md" @@ -160,10 +170,17 @@ def write_fixture(project: Path) -> tuple[Path, Path, Path]: run_json.write_text(json.dumps(run_record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") run_md.write_text("# Fixture Run\n", encoding="utf-8") index_path.write_text(json.dumps(run_record, ensure_ascii=False) + "\n", encoding="utf-8") + groups, _, _ = parse_todo_source(state_file.read_text(encoding="utf-8")) + records = [canonical_todo_read_record(structured_todo_item(item, role=role, + source_section=item["source_section"])) for role, rows in groups.items() for item in rows] + initialize_canonical_authority(runtime_root, GOAL_ID, build_todo_runtime_shadow_projection( + goal_id=GOAL_ID, todos=records, handoff_mode="soft_claim"), state_path=state_file, provider="sqlite") return registry_path, runtime_root, index_path def main() -> None: + from canonical_authority_fixture import isolate_sqlite_runtime + cli_source = CLI.read_text(encoding="utf-8") module_source = MODULE.read_text(encoding="utf-8") init_source = INIT.read_text(encoding="utf-8") @@ -242,8 +259,12 @@ def main() -> None: for option in options: require(option in help_text, f"{command} help omitted {option}") - with tempfile.TemporaryDirectory(prefix="loopx-project-lifecycle-cli-") as tmp: + with (tempfile.TemporaryDirectory(prefix="loopx-project-lifecycle-cli-") as tmp, + MonkeyPatch.context() as patch, ExitStack() as cleanup): project = Path(tmp) + isolate_sqlite_runtime(project, patch) + patch.setenv("LOOPX_USAGE_PING", "0") + cleanup.callback(restart_effect_runtime, wait_for_exit=True) registry_path, runtime_root, index_path = write_fixture(project) before_index = index_path.read_text(encoding="utf-8") command_prefix = ( diff --git a/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-desktop.png b/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-desktop.png new file mode 100644 index 0000000000..19770bab01 Binary files /dev/null and b/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-desktop.png differ diff --git a/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-evidence.png b/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-evidence.png new file mode 100644 index 0000000000..4cd725bcf2 Binary files /dev/null and b/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-evidence.png differ diff --git a/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-help.png b/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-help.png new file mode 100644 index 0000000000..3420cc820a Binary files /dev/null and b/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-help.png differ diff --git a/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-lower.png b/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-lower.png new file mode 100644 index 0000000000..ad712a7ba9 Binary files /dev/null and b/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-lower.png differ diff --git a/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-narrow-detail.png b/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-narrow-detail.png new file mode 100644 index 0000000000..0376dd18b1 Binary files /dev/null and b/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-narrow-detail.png differ diff --git a/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-narrow.png b/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-narrow.png new file mode 100644 index 0000000000..4a26706f08 Binary files /dev/null and b/examples/personal-workspace-browser/assets/goal-outcome-disclosure/after-narrow.png differ diff --git a/examples/personal-workspace-browser/assets/goal-outcome-disclosure/before-desktop.png b/examples/personal-workspace-browser/assets/goal-outcome-disclosure/before-desktop.png new file mode 100644 index 0000000000..d4dbc7c5c6 Binary files /dev/null and b/examples/personal-workspace-browser/assets/goal-outcome-disclosure/before-desktop.png differ diff --git a/examples/personal-workspace-browser/assets/goal-outcome-disclosure/before-narrow.png b/examples/personal-workspace-browser/assets/goal-outcome-disclosure/before-narrow.png new file mode 100644 index 0000000000..696955ca9f Binary files /dev/null and b/examples/personal-workspace-browser/assets/goal-outcome-disclosure/before-narrow.png differ diff --git a/examples/personal-workspace-browser/goal-outcome-backend.py b/examples/personal-workspace-browser/goal-outcome-backend.py index cac0a9df66..26c2b6c2f6 100644 --- a/examples/personal-workspace-browser/goal-outcome-backend.py +++ b/examples/personal-workspace-browser/goal-outcome-backend.py @@ -114,6 +114,14 @@ def cli(*args): path.write_text(json.dumps(patch, ensure_ascii=False)) cli("refine", "--agent-id", "author", "--document", str(path), "--operation-id", f"experience-review-{index}", "--expected-provider-revision", cli("inspect")["provider_revision"], "--execute") + # A real declaration change invalidates the earlier review basis. + # Reading it again must never make that historical review current. + path = root / "updated-recovery.json" + path.write_text(json.dumps({"outcome_id": "experience-0", "requirements": [{ + "id": "recovery-later-requirement", "description": "后续补充的恢复要求也应保留,不能把旧版自查当作新版核验。"}], + "source": {"ref": "fixture:updated-recovery", "summary": "A later requirement changes the review basis"}}, ensure_ascii=False)) + cli("refine", "--agent-id", "author", "--document", str(path), "--operation-id", "updated-recovery", + "--expected-provider-revision", cli("inspect")["provider_revision"], "--execute") path = root / "checks.json" path.write_text(json.dumps({"outcome_id": "product-tour", "criteria": [ {"id": "tour-opening", "validation_argv": ["python3", "check-tour.py", "opening"]}, diff --git a/examples/personal-workspace-browser/goal-outcome-recovery.mjs b/examples/personal-workspace-browser/goal-outcome-recovery.mjs index e1ebea7802..93159b6fa2 100644 --- a/examples/personal-workspace-browser/goal-outcome-recovery.mjs +++ b/examples/personal-workspace-browser/goal-outcome-recovery.mjs @@ -20,7 +20,7 @@ const reservation = createServer(); await new Promise(accept => reservation.listen(0, "127.0.0.1", accept)); const port = reservation.address().port; await new Promise(accept => reservation.close(accept)); -const child = spawn(python, ["-u", "examples/personal-workspace-browser/goal-outcome-backend.py", root, String(port), "--without-check-plan", +const child = spawn(python, ["-u", "examples/personal-workspace-browser/goal-outcome-backend.py", root, String(port), "--without-check-plan", "--overview-readability", ...(process.env.LOOPX_GOAL_OUTCOME_ASSETS ? ["--assets-dir", process.env.LOOPX_GOAL_OUTCOME_ASSETS] : [])], {cwd: repoRoot, env: environment, stdio: ["ignore", "pipe", "pipe"]}); let diagnostic = "", browser, page, releaseDelivery; @@ -47,7 +47,8 @@ try { const remoteUrl = new URL("/remote-status.json", url).href; await waitForHttp(url); browser = await launchBrowser(loadPlaywright().chromium); - page = await browser.newPage({viewport: {width: 1440, height: 1000}}); + page = await browser.newPage({viewport: {width: 1440, height: 1000}, hasTouch: true}); + await page.clock.install(); await mkdir(output, {recursive: true}); page.on("pageerror", error => errors.push(error.message)); await page.addInitScript(statusUrl => { @@ -91,15 +92,43 @@ try { await page.locator(".personal-goal-link").first().click(); await page.getByRole("button", {name: "Overview", exact: true}).click(); const outcomes = page.locator(".goal-outcomes"); + await outcomes.waitFor(); + await firstDelivery; + const changedReview = cli("inspect").goal_acceptance_contract.outcome_requirements.outcomes.find(item => item.id === "experience-0"); + assert.ok(changedReview.requirements.some(item => item.agent_review?.basis_current === false), + "The fixture retains a genuinely invalidated native review"); + assert.equal(await outcomes.locator(".goal-outcome-disclosure").count(), await outcomes.locator(".goal-outcome").count(), + "Every declared outcome has its own complete-body disclosure"); + assert.equal(await outcomes.locator(".goal-outcome-disclosure[open]").count(), 0, + "A populated overview starts with whole outcome bodies closed"); + assert.ok(await outcomes.locator(".goal-outcome:visible").count() <= 3, + "Long repeated requirements do not fill the overview before current work"); + await page.screenshot({path: resolve(output, "default-desktop.png"), animations: "disabled"}); + await page.screenshot({path: resolve(output, "default-whole-desktop.png"), animations: "disabled", fullPage: true}); + await page.setViewportSize({width: 390, height: 844}); + await page.screenshot({path: resolve(output, "default-narrow.png"), animations: "disabled"}); + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true); + const touchOutcome = outcomes.locator(".goal-outcome:visible").first().locator(".goal-outcome-disclosure"); + await touchOutcome.locator(":scope > summary").tap(); + assert.notEqual(await touchOutcome.getAttribute("open"), null, "A touch tap opens the complete outcome body"); + await touchOutcome.locator(":scope > summary").tap(); + assert.equal(await touchOutcome.getAttribute("open"), null, "The same touch target closes the outcome without a hover dependency"); + await page.setViewportSize({width: 1440, height: 1000}); const expandAll = async () => { const more = outcomes.locator(":scope > .goal-outcomes-more"); - if (await more.getAttribute("aria-expanded") === "false") await more.click(); + if (await more.count() && await more.getAttribute("aria-expanded") === "false") await more.click(); + }; + const openOutcome = async region => { + const details = region.locator(":scope > .goal-outcome-disclosure"); + if (await details.getAttribute("open") === null) { + await details.locator(":scope > summary").focus(); + await page.keyboard.press("Enter"); + } }; await expandAll(); - const openingGroup = outcomes.locator(".goal-outcome-status-group").filter({has: page.getByText("Retain the approved introduction", {exact: true})}).first(); - if (await openingGroup.getAttribute("open") === null) await openingGroup.locator(":scope > summary").click(); + const original = outcomes.locator(".goal-outcome").filter({has: page.getByRole("heading", {name: "Deliver a clear product tour", exact: true})}); + await openOutcome(original); await outcomes.locator(".goal-overview-criteria").getByText("Retain the approved introduction", {exact: true}).waitFor(); - await firstDelivery; assert.ok(deliverySnapshot?.acceptance, "Retain the real initial delivery snapshot before advancing the native owner"); const evidence = page.locator(".goal-overview-evidence"); assert.equal(await evidence.getAttribute("open"), null, "Secondary evidence starts collapsed without losing its reader"); @@ -119,11 +148,11 @@ try { "--expected-provider-revision", cli("inspect").provider_revision, "--execute"); const initialCount = outcome().requirements.length; assert.equal(deliverySnapshot.acceptance.goal_acceptance_contract.outcome_requirements.outcomes.some(item => item.id === outcomeId), false); - const original = outcomes.locator(".goal-outcome").filter({has: page.getByRole("heading", {name: "Deliver a clear product tour", exact: true})}); await original.getByText("Add a requirement", {exact: true}).click(); await original.getByRole("button", {name: "Refresh result", exact: true}).click(); const region = outcomes.locator(".goal-outcome").filter({has: page.getByRole("heading", {name: newOutcome.objective, exact: true})}); await region.waitFor(); + await openOutcome(region); const form = region.locator("form"); await region.getByText("Add a requirement", {exact: true}).click(); const text = form.locator("textarea"); @@ -158,6 +187,30 @@ try { }; const draft = "Keep the conclusion understandable without prior context"; await text.fill(draft); + await outcomes.locator(":scope > .goal-outcomes-more").click(); + assert.equal(await outcomes.locator(":scope > .goal-outcomes-more").getAttribute("aria-expanded"), "false"); + assert.equal(await text.isVisible(), true, + "An open outcome and its draft stay visible when the full collection collapses to the attention preview"); + const scrollBefore = await text.evaluate(node => { + const positions = []; + for (let parent = node.parentElement; parent; parent = parent.parentElement) positions.push(parent.scrollTop); + return positions; + }); + const writesBeforePoll = requests.length; + const poll = page.waitForResponse(response => response.url().includes("/api/chat/goal-acceptance?") && response.request().method() === "GET"); + await page.clock.fastForward(15_001); + await poll; + assert.equal(await text.inputValue(), draft, "Quiet updates preserve the unsent draft"); + assert.notEqual(await region.locator(".goal-outcome-disclosure").getAttribute("open"), null, + "Quiet updates preserve the selected outcome"); + assert.deepEqual(await text.evaluate(node => { + const positions = []; + for (let parent = node.parentElement; parent; parent = parent.parentElement) positions.push(parent.scrollTop); + return positions; + }), scrollBefore, "Quiet same-source updates preserve the reading position"); + assert.equal(requests.length, writesBeforePoll, "A background read does not check, refine or run a model"); + assert.ok(cli("inspect").goal_acceptance_contract.outcome_requirements.outcomes.find(item => item.id === "experience-0") + .requirements.some(item => item.agent_review?.basis_current === false), "A fresh read does not validate the changed requirement basis"); await interruptDelivery(); assert.equal(await text.inputValue(), draft, "Unsubmitted draft survives delivery loading and error"); assert.equal(requests.length, 0); @@ -231,6 +284,7 @@ try { await refresh.click(); await text.waitFor({state: "hidden"}); await expandAll(); + await openOutcome(region); await region.getByText("Add a requirement", {exact: true}).click(); await text.fill("This pending requirement belongs to the original lifetime"); rejectRefinement = true; @@ -284,6 +338,7 @@ try { await refresh.click(); await text.waitFor({state: "hidden"}); await expandAll(); + await openOutcome(region); await region.getByText("Add a requirement", {exact: true}).click(); assert.equal(await text.inputValue(), "", "A reused Goal/outcome id cannot inherit another lifetime's pending request"); assert.equal(await rebase.count(), 0, "The old request cannot be rebased into the successor lifetime"); @@ -311,8 +366,7 @@ try { await page.getByRole("option", {name: "Remote snapshot", exact: true}).click(); await page.getByRole("heading", {name: "Remote handoff workspace", exact: true}).waitFor(); await expandAll(); - const remoteGroup = outcomes.locator(".goal-outcome-status-group").first(); - if (await remoteGroup.getAttribute("open") === null) await remoteGroup.locator(":scope > summary").click(); + await openOutcome(outcomes.locator(".goal-outcome").first()); await outcomes.getByText("Only this remote source declaration", {exact: true}).waitFor(); assert.equal(await outcomes.getByText(newOutcome.objective, {exact: true}).count(), 0, "The same Goal id in a remote source cannot consume the local acceptance cache"); -