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
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.
18 changes: 16 additions & 2 deletions apps/presentation/dashboard/smoke/team-adoption-http-fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ def main() -> None:
"ref": "accepted-input.json", "description": "Accepted input", "sha256": artifact["sha256"],
"delegation": {"operation_id": "analysis-1", "ref": artifact["ref"], "relation": "uses"}}]})
assert wait(runner, "synthesis-1")["status"] == "accepted"
source_chain = "--source-chain" in sys.argv
if source_chain:
downstream = runner.read("synthesis-1")["artifacts"][0]
config["bindings"].append({**config["bindings"][0], "id": "further", "agent_id": "analyst",
"todo_id": "todo_analyst-corrected", "workspace": str(root / "analyst/corrected"),
"output_refs": ["output.json", "report.md"]})
runner.config.write_text(json.dumps(config))
(root / "analyst/corrected/report.md").write_text("# Further result\n\nUses the exact accepted synthesis.\n")
(root / "analyst/corrected/accepted-input.json").write_text(downstream["text"])
runner.start("further", "further-1", {**brief(), "inputs": [{
"ref": "accepted-input.json", "description": "Accepted synthesis", "sha256": downstream["sha256"],
"delegation": {"operation_id": "synthesis-1", "ref": downstream["ref"], "relation": "uses"}}]})
assert wait(runner, "further-1")["status"] == "accepted"
repo = root / "project"
executor = repo / ".loopx/config/delegations.json"
executor.parent.mkdir(parents=True)
Expand Down Expand Up @@ -79,12 +92,13 @@ def main() -> None:
input_file.write_text(artifact["text"])
print(json.dumps({"restored": True}), flush=True)
elif operation == "inspect":
observed = runner.read("analysis-1")
observed = runner.read("synthesis-1" if source_chain else "analysis-1")
session = store.load_session("adoption")
print(json.dumps({"result": observed, "session_unchanged": session == initial_session,
"coordinator_paused": session["loopx_mode"]["paused"] is True,
"host_invocations": [(root / workspace / "host-invocations").read_text()
for workspace in ("analyst/initial", "reviewer/corrected")]}), flush=True)
for workspace in (("analyst/initial", "reviewer/corrected", "analyst/corrected")
if source_chain else ("analyst/initial", "reviewer/corrected"))]}), flush=True)
else:
raise ValueError("Unknown fixture command")
finally:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import {launchBrowser, loadPlaywright, waitForHttp} from "../../../../examples/d
process.env.LOOPX_PERSONAL_WORKSPACE_PACKAGED = "1";
const {repoRoot, outputDir, port, startServer} = await import("../../../../examples/personal-workspace-browser/fixture.mjs");
const {openWorkspacePage} = await import("../../../../examples/personal-workspace-browser/scenario-context.mjs");
const fixture = spawn(resolveTestPython({repoRoot}), ["-u", "apps/presentation/dashboard/smoke/team-adoption-http-fixture.py"],
const sourceChain = process.argv.includes("--source-chain");
const fixture = spawn(resolveTestPython({repoRoot}), ["-u", "apps/presentation/dashboard/smoke/team-adoption-http-fixture.py",
...(sourceChain ? ["--source-chain"] : [])],
{cwd: repoRoot, stdio: ["pipe", "pipe", "pipe"]});
const exited = once(fixture, "exit");
const lines = createInterface({input: fixture.stdout});
Expand Down Expand Up @@ -57,6 +59,48 @@ try {
});
Object.assign(mode, {enabled: true, paused: true, active_turn_id: null, native: {status: "paused", tokenBudget: 100000}});
const results = page.getByRole("region", {name: "团队成果", exact: true});
if (sourceChain) {
// The displayed source is itself a consumer. Its domain output remains
// accepted when its input is withdrawn; current_use must still fence UI use.
await results.getByRole("button", {name: "reviewer · report.md", exact: true}).click();
await results.locator(".goal-team-result-reader > details > summary").click();
const adoption = results.locator(".goal-team-adoption");
await adoption.locator("summary").click();
await adoption.getByRole("button", {name: "查找使用此版本的后续结果", exact: true}).click();
await adoption.getByLabel("后续结果", {exact: true}).waitFor();
await results.getByLabel("当前报告", {exact: true}).waitFor();
const before = await command("inspect");
assert.equal(before.result.current_use.state, "current");
await command("withdraw");
const unavailable = await command("inspect");
assert.equal(unavailable.result.status, "accepted", "Historical acceptance stays intact");
assert.equal(unavailable.result.current_use.state, "unavailable");
assert.deepEqual(unavailable.result.artifacts, before.result.artifacts, "Same output bytes, different use eligibility");
await adoption.getByRole("button", {name: "阅读后续结果", exact: true}).click();
await page.waitForFunction(() => !document.querySelector(".goal-team-adoption")
|| document.querySelector(".goal-team-adoption").getAttribute("aria-busy") === "false");
await mkdir(outputDir, {recursive: true});
await page.screenshot({path: resolve(outputDir, "team-source-chain-real-observation.png"), animations: "disabled"});
await results.getByRole("alert").filter({hasText: "产物或验收已变化"}).waitFor();
assert.equal(await results.getByLabel("当前报告", {exact: true}).count(), 0);
assert.equal(writes.length, 0, "A refused source read cannot offer or submit adoption");
await mkdir(outputDir, {recursive: true});
await page.screenshot({path: resolve(outputDir, "team-source-chain-real-desktop.png"), animations: "disabled"});
await page.setViewportSize({width: 390, height: 844}); await page.emulateMedia({reducedMotion: "reduce"});
assert.ok(await results.evaluate(el => el.scrollWidth <= el.clientWidth));
await page.screenshot({path: resolve(outputDir, "team-source-chain-real-mobile.png"), animations: "disabled"});
await command("restore");
await results.getByRole("button", {name: "刷新成果", exact: true}).focus();
await page.keyboard.press("Enter");
await results.getByLabel("当前报告", {exact: true}).waitFor();
const recovered = await command("inspect");
assert.equal(recovered.result.current_use.state, "current");
assert.ok(recovered.session_unchanged);
assert.deepEqual(recovered.host_invocations, ["1", "1", "1"]);
assert.equal(writes.length, 0);
assert.equal(api.turnRequests.length, 0);
console.log("team-source-chain: passed (packaged real HTTP/SQLite, historical output retained, source input withdrawal/refusal, keyboard recovery, zero adoption/dispatch)");
} else {
await results.getByRole("button", {name: "analyst · output.json", exact: true}).click();
await results.locator(".goal-team-result-reader > details > summary").click();
const adoption = results.locator(".goal-team-adoption");
Expand Down Expand Up @@ -104,6 +148,7 @@ try {
assert.equal(writes.length, 1, "Readback recovery never records a new adoption");
assert.equal(api.turnRequests.length, 0);
console.log("team-adoption-recovery: passed (packaged report, real HTTP/SQLite/checks/receipts, explicit decision, lost ACK, exact use withdrawal/recovery, no dispatch, mobile/keyboard)");
}
} catch (error) {
if (workspace) console.error({body: (await workspace.page.locator("body").innerText()).slice(-3000), errors: workspace.errors});
throw error;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,53 @@ try {
await verificationGap.scrollIntoViewIfNeeded();
await page.screenshot({path: resolve(outputDir, "team-verifier-gap-mobile.png"), animations: "disabled"});
await page.setViewportSize({width: 1512, height: 980});
// A completed source can retain its output bytes while its dependency
// chain becomes unavailable. Reading an adoption candidate must withdraw
// the earlier correction trace and must never offer a confirmation.
const adoption = evidence.locator(".goal-team-adoption");
await evidence.getByText("版本与采用关系详情", {exact: true}).click();
await adoption.getByText("确认采用于后续结果", {exact: true}).click();
const consumerInventory = route => route.request().postDataJSON()?.operation === "operations"
? route.fulfill({json: {items: [{record_id: "b".repeat(64), operation_id: "accepted-synthesis",
agent_id: "synthesizer", status: "accepted", recovery_required: false}],
has_more: false, next_cursor: null, page_readback_complete: true}}) : route.fallback();
await page.route("**/api/chat/sessions/*/loopx", consumerInventory);
await adoption.getByRole("button", {name: "查找使用此版本的后续结果", exact: true}).click();
await adoption.getByLabel("后续结果", {exact: true}).waitFor();
const invalidSource = route => route.request().postDataJSON()?.operation === "read"
&& route.request().postDataJSON()?.operation_id === "accepted-analysis"
? route.fulfill({json: {...legacyReadback, current_use: {state: "unavailable", reason: "input_unavailable"}}})
: route.fallback();
await page.route("**/api/chat/sessions/*/loopx", invalidSource);
const adoptionWritesBefore = api.loopxModeRequests.filter(row => row.operation === "adopt").length;
await adoption.getByRole("button", {name: "阅读后续结果", exact: true}).click();
await page.waitForFunction(() => !document.querySelector(".goal-team-adoption")
|| document.querySelector(".goal-team-adoption").getAttribute("aria-busy") === "false");
await page.screenshot({path: resolve(outputDir, "team-source-chain-observation.png"), animations: "disabled"});
await evidence.getByRole("alert").filter({hasText: "关联执行或版本已变化"}).waitFor({timeout: 3000});
assert.equal(await evidence.getByRole("button", {name: "阅读原始产物", exact: true}).count(), 0,
"An unavailable source chain withdraws the earlier correction witness");
assert.equal(await adoption.getByRole("button", {name: "确认采用于此结果", exact: true}).count(), 0);
assert.equal(await original().count(), 0);
assert.equal(api.loopxModeRequests.filter(row => row.operation === "adopt").length, adoptionWritesBefore);
await page.screenshot({path: resolve(outputDir, "team-source-chain-unavailable-desktop.png"), animations: "disabled"});
await page.setViewportSize({width: 390, height: 844});
await page.emulateMedia({reducedMotion: "reduce"});
assert.ok(await dialog.evaluate(el => el.scrollWidth <= el.clientWidth));
await page.screenshot({path: resolve(outputDir, "team-source-chain-unavailable-mobile.png"), animations: "disabled"});
await page.setViewportSize({width: 1512, height: 980});
await page.unroute("**/api/chat/sessions/*/loopx", invalidSource);
await page.unroute("**/api/chat/sessions/*/loopx", consumerInventory);
await recheck.focus();
await page.keyboard.press("Enter");
await original().waitFor();
await evidence.getByRole("button", {name: "核验关联执行", exact: true}).click();
await verificationGap.waitFor();
// Restore the same comparison explicitly after withdrawal.
await evidence.getByRole("button", {name: /^修订依据/}).click();
await evidence.getByLabel("对照产物", {exact: false}).selectOption({label: "report.md"});
await evidence.getByRole("button", {name: "查看原文差异", exact: true}).click();
await selectedOutput().waitFor();
// Losing an optional adoption cannot erase freshly verified correction evidence.
const unavailableDownstream = route => route.request().postDataJSON()?.operation === "read"
&& route.request().postDataJSON()?.operation_id === "accepted-synthesis"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {adoptLoopXTeamWork, fetchLoopXTeamWork, readLoopXTeamWork, type Delegati
import {isMarkdownArtifact, TeamArtifactReport} from "./team-artifact-content";

const accepted = (row: DelegationReadback) => row.status === "accepted"
&& !row.error && !row.recovery_required && Boolean(row.artifacts?.length);
&& row.current_use?.state !== "unavailable" && !row.error && !row.recovery_required && Boolean(row.artifacts?.length);
const sameResult = (expected: DelegationReadback, observed: DelegationReadback) => accepted(observed)
&& expected.operation_id === observed.operation_id && expected.request_id === observed.request_id
&& expected.agent_id === observed.agent_id && expected.todo_id === observed.todo_id
Expand Down
Loading