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 @@ -21,11 +21,34 @@ if (writeAuthorityOnly && packaged) throw new Error("--write-authority-only requ
const payload = writeAuthorityOnly ? null : JSON.parse(execFileSync(python, ["-c", `
import runpy, tempfile, json
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
from loopx.status import collect_status
from tests.control_plane.canonical_authority_fixture import initialize_canonical_authority, isolate_sqlite_runtime
fixture = runpy.run_path('tests/test_delivery_review.py')
refresh = fixture['make_project'].__globals__['refresh_state_run']
def initialize_and_refresh(**kwargs):
# The standalone caller has no pytest setup: initialize the same real
# canonical provider before the shared fixture's first operational read.
groups, _, _ = parse_todo_source(kwargs['state_file'].read_text())
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(Path(kwargs['runtime_root_override']), kwargs['goal_id'],
build_todo_runtime_shadow_projection(goal_id=kwargs['goal_id'], todos=records,
handoff_mode='soft_claim'), state_path=kwargs['state_file'], provider='sqlite')
return refresh(**kwargs)
fixture['make_project'].__globals__['refresh_state_run'] = initialize_and_refresh
with tempfile.TemporaryDirectory() as directory:
registry, runtime = fixture['make_project'](Path(directory))
print(json.dumps(collect_status(registry_path=registry, runtime_root_override=str(runtime), limit=20, goal_id='release-demo')))
with MonkeyPatch.context() as patch:
isolate_sqlite_runtime(Path(directory), patch)
try:
registry, runtime = fixture['make_project'](Path(directory))
print(json.dumps(collect_status(registry_path=registry, runtime_root_override=str(runtime), limit=20, goal_id='release-demo')))
finally:
restart_effect_runtime(wait_for_exit=True)
`], { cwd: root, encoding: "utf8" }));
if (payload) payload.run_history.goals[0].acceptance_observation = acceptance;
const server = packaged
Expand All @@ -34,7 +57,8 @@ const server = packaged
async function checkLiveWriteAuthority(browser, origin) {
const page = await browser.newPage({viewport: {width: 1024, height: 768}});
const writes = [], errors = [];
let readsFail = false;
let readsFail = false, nativeReads = 0;
await page.clock.install();
const native = {ok: true, goal_id: "release-demo", provider_revision: "fixture:current",
goal_acceptance_contract: {...contract, lifecycle_state: "active",
goal_ref: {goal_id: "release-demo", goal_instance_id: "write-authority-instance"}}};
Expand All @@ -43,10 +67,13 @@ async function checkLiveWriteAuthority(browser, origin) {
await page.route("**/api/**", route => {
const request = route.request(), path = new URL(request.url()).pathname;
if (request.method() !== "GET") writes.push({path, body: request.postDataJSON()});
if (path === "/api/chat/goal-acceptance") return route.fulfill(
if (path === "/api/chat/goal-acceptance") {
if (request.method() === "GET") nativeReads++;
return route.fulfill(
request.method() === "GET" && readsFail
? {status: 503, json: {error: "Synthetic native read failure"}}
: {json: native});
}
if (path === "/api/actions") return route.fulfill({json: {
ok: true, schema_version: "loopx_chat_action_list_v1", proposals: [],
}});
Expand All @@ -71,6 +98,28 @@ async function checkLiveWriteAuthority(browser, origin) {
await phase("released"); await idle();
assert.deepEqual(writes, [], "Native read failure during cancellation must prevent the stale effect POST");

const failedReads = nativeReads;
await page.clock.fastForward(15_001);
assert.equal(nativeReads, failedReads, "Failed reads back off instead of repeating the healthy poll cadence");
const retry = page.waitForResponse(response => response.url().includes("/api/chat/goal-acceptance?"));
await page.clock.fastForward(45_000);
await retry;
assert.equal(nativeReads, failedReads + 1, "The bounded automatic retry only reads");
assert.deepEqual(writes, []);
await page.evaluate(() => {
Object.defineProperty(document, "visibilityState", {configurable: true, value: "hidden"});
window.dispatchEvent(new Event("visibilitychange"));
});
await page.clock.fastForward(60_000);
assert.equal(nativeReads, failedReads + 1, "A hidden workspace does not poll its acceptance reader");
readsFail = false;
await page.evaluate(() => {
Object.defineProperty(document, "visibilityState", {configurable: true, value: "visible"});
window.dispatchEvent(new Event("visibilitychange"));
});
await check.waitFor();
assert.deepEqual(writes, [], "Returning to the workspace recovers the read without running checks");

// A fresh retry remains usable; the negative case must not pass by blocking every write.
readsFail = false;
await page.getByRole("button", {name: "Refresh native read", exact: true}).click();
Expand Down Expand Up @@ -199,10 +248,18 @@ try {
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");
assert.equal(await outcomes.locator(".goal-outcome-disclosure[open]").count(), 0, "Whole outcomes start collapsed");
const first = outcomes.locator(".goal-outcome").filter({has: page.getByRole("heading", {name: "First colliding outcome", exact: true})});
const second = outcomes.locator(".goal-outcome").filter({has: page.getByRole("heading", {name: "Second colliding outcome", exact: true})});
await first.locator(".goal-outcome-disclosure > summary").focus();
await page.keyboard.press("Enter");
assert.equal(await first.locator(".goal-outcome-criteria > li").count(), 3);
assert.equal(await second.locator(".goal-outcome-disclosure").getAttribute("open"), null,
"Colon-bearing identities do not open another outcome's requirements");
await second.locator(".goal-outcome-disclosure > summary").click();
await second.getByText("Unrelated met requirement", {exact: true}).waitFor();
assert.equal(await outcomes.locator(".goal-outcome-criteria > li:visible").count(), 4,
"Each selected outcome exposes every requirement without identity collisions");
response = structuredClone(snapshot);
payload.run_history.goals[0].acceptance_observation = acceptance;
await page.reload({waitUntil: "networkidle"});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,16 @@ export function GoalAcceptanceContractSection({goalId, contract: retained, copy,
const client = useQueryClient();
const queryKey = ["goal-acceptance-readback", goalId];
const read = useQuery({queryKey, queryFn: ({signal}) => acceptanceRequest(goalId, undefined, signal),
enabled: localSource && canManage, retry: false, refetchOnWindowFocus: "always",
refetchInterval: 15_000, refetchIntervalInBackground: false});
enabled: localSource, retry: false, staleTime: 15_000, refetchOnWindowFocus: true,
refetchInterval: query => query.state.error ? 60_000 : 15_000, refetchIntervalInBackground: false});
const actions = useTypedActionReadback(!localSource || !canManage, goalId);
const plan = !localSource || actions.isError ? null : preparedAcceptancePlan(actions.data ?? [], goalId);
// A failed or loading delivery read revokes writes, not a newer same-source
// declaration. Remote snapshots never inherit the local reader's cache.
const contract = localSource && read.data?.goal_id === goalId ? read.data.goal_acceptance_contract : retained;
const fresh = localSource && canManage && !read.isError && read.data?.goal_id === goalId
const fresh = localSource && !read.isError && read.data?.goal_id === goalId
&& (!contract?.lifecycle_state || contract.lifecycle_state === "active");
const writeCurrent = canManage && fresh;
const sourceKey = JSON.stringify([localSource, goalId, contract?.goal_ref ?? null,
contract?.outcome_requirements?.current_lifetime ?? null]);
const checksConfigured = hasConfiguredGoalChecks(contract);
Expand All @@ -50,7 +51,7 @@ export function GoalAcceptanceContractSection({goalId, contract: retained, copy,
const [notice, setNotice] = useState("");
const generation = useRef(0), flight = useRef(false);
const source = useRef(sourceKey); source.current = sourceKey;
const writable = useRef(!!fresh); writable.current = !!fresh;
const writable = useRef(!!writeCurrent); writable.current = !!writeCurrent;
useEffect(() => {
generation.current++; flight.current = false;
setDocument(null); setFilename(""); setError(""); setNotice(""); setBusy(false);
Expand Down Expand Up @@ -102,7 +103,7 @@ export function GoalAcceptanceContractSection({goalId, contract: retained, copy,
<p>{copy.guidance}</p>
<div className="delivery-acceptance-controls">
<button type="button" disabled={busy || readOnly} onClick={() => {setError(""); void read.refetch(); void actions.refetch();}}>{zh ? "刷新结果" : "Refresh result"}</button>
<label>{zh ? "导入现有 JSON 合同(可选)" : "Import an existing JSON contract (optional)"}<input type="file" accept="application/json,.json" disabled={busy || !fresh}
<label>{zh ? "导入现有 JSON 合同(可选)" : "Import an existing JSON contract (optional)"}<input type="file" accept="application/json,.json" disabled={busy || !writeCurrent}
onChange={event => {const file = event.target.files?.[0]; const token = ++generation.current; setDocument(null); setFilename("");
if (!file) return; if (file.size > 262144) {setError(zh ? "文件超过 256 KB。" : "File exceeds 256 KB."); return;}
void file.text().then(text => {if (token !== generation.current) return; const parsed: unknown = JSON.parse(text);
Expand All @@ -112,8 +113,8 @@ export function GoalAcceptanceContractSection({goalId, contract: retained, copy,
}}/></label>
{document ? <details open><summary>{zh ? "待保存配置" : "Configuration to save"} · {filename}</summary><pre>{JSON.stringify(document, null, 2)}</pre>
<p>{zh ? "保存会更新所列任务的关联,不会运行检查。" : "Saving updates the listed task associations and does not run checks."}</p></details> : null}
<button type="button" disabled={busy || !fresh || !document || !!error} onClick={() => void operate("configure")}>{zh ? "保存配置" : "Save configuration"}</button>
<button type="button" disabled={busy || !fresh || !checksConfigured} onClick={() => void operate("clear-checks")}>{zh ? "移除检查配置" : "Remove check configuration"}</button>
<button type="button" disabled={busy || !writeCurrent || !document || !!error} onClick={() => void operate("configure")}>{zh ? "保存配置" : "Save configuration"}</button>
<button type="button" disabled={busy || !writeCurrent || !checksConfigured} onClick={() => void operate("clear-checks")}>{zh ? "移除检查配置" : "Remove check configuration"}</button>
</div>
<dl className="delivery-acceptance-source"><div><dt>{copy.source}</dt><dd><code>{goalId}</code></dd></div>
{checksConfigured ? <><div><dt>{copy.scope}</dt><dd>{contract.scope?.kind === "selected_work" ? copy.selectedWork : copy.allWork}</dd></div>
Expand Down
Loading
Loading