Skip to content
Draft
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
101 changes: 101 additions & 0 deletions apps/presentation/dashboard/smoke/team-adoption-http-fixture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Disposable real delegation/HTTP/SQLite owner; scripted host, no paid model."""
from __future__ import annotations

import json
from pathlib import Path
import sys
import tempfile
import threading
from types import SimpleNamespace

import pytest

REPO = Path(__file__).resolve().parents[4]
sys.path.insert(0, str(REPO))
sys.path.insert(0, str(REPO / "tests"))

def main() -> None:
from loopx.chat_runtime import ChatRuntimeController
from loopx.chat_server import ChatHTTPServer, ChatRequestHandler
from loopx.chat_store import ChatSessionStore
from test_local_delegation import brief, service, wait

with tempfile.TemporaryDirectory(prefix="loopx-team-adoption-") as directory:
patches = pytest.MonkeyPatch()
finalizers = []
server = None
try:
root, runner = service.__wrapped__(Path(directory), SimpleNamespace(
param="sqlite", addfinalizer=finalizers.append), patches)
config = json.loads(runner.config.read_text())
config["bindings"].append({**config["bindings"][0], "id": "synthesis", "agent_id": "reviewer",
"todo_id": "todo_reviewer-corrected", "workspace": str(root / "reviewer/corrected"),
"output_refs": ["output.json", "report.md"]})
runner.config.write_text(json.dumps(config))
runner.start("analysis", "analysis-1", brief())
source = wait(runner)
artifact = source["artifacts"][0]
input_file = root / "reviewer/corrected/accepted-input.json"
input_file.write_text(artifact["text"])
# Authored synthetic companion text; the real acceptance rule still
# checks JSON semantics, while artifact readback checks both versions.
(root / "reviewer/corrected/report.md").write_text(
"# Corrected evidence\n\nThe current-period normalized value is 25. "
"Different periods remain incomparable; the repost adds no independent source.\n")
runner.start("synthesis", "synthesis-1", {**brief(), "inputs": [{
"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"
repo = root / "project"
executor = repo / ".loopx/config/delegations.json"
executor.parent.mkdir(parents=True)
executor.write_bytes(runner.config.read_bytes())
registry = json.loads(runner.registry.read_text())
registry["goals"][0]["spawn_policy"] = {"mode": "multi_subagent", "allowed": True,
"max_children": 2, "execution_config": ".loopx/config/delegations.json"}
runner.registry.write_text(json.dumps(registry))
store = ChatSessionStore(runner.root)
runtime = ChatRuntimeController(store=store, codex_bin="unused", registry_path=runner.registry)
store.create_session(goal_id=runner.goal_id, agent_id="codex", channel_id="goal." + runner.goal_id,
session_id="adoption", upstream_thread_id="fixture", upstream_mode="chat", adapter_kind="codex_app_server")
runtime.loopx_mode.apply("adoption", {"operation": "configure", "settings": {
"agent_id": "lead", "token_budget": 1000}}, work_dir=repo, objective="Read accepted work")
mode = store.load_session("adoption")["loopx_mode"]
store.update_session("adoption", loopx_mode={**mode, "enabled": True, "paused": True})
initial_session = store.load_session("adoption")
server = ChatHTTPServer(("127.0.0.1", 0), ChatRequestHandler)
server.verbose = False
server.registry_path, server.chat_store, server.runtime_controller = runner.registry, store, runtime
server.runtime_root_override, server.scan_roots, server.limit = str(runner.root), [], 20
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
print(json.dumps({"origin": f"http://127.0.0.1:{server.server_port}"}), flush=True)
for command in sys.stdin:
operation = command.strip()
if operation == "withdraw":
input_file.write_text("{}")
print(json.dumps({"withdrawn": True}), flush=True)
elif operation == "restore":
input_file.write_text(artifact["text"])
print(json.dumps({"restored": True}), flush=True)
elif operation == "inspect":
observed = runner.read("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)
else:
raise ValueError("Unknown fixture command")
finally:
if server:
server.shutdown()
server.server_close()
thread.join(timeout=5)
for cleanup in finalizers:
cleanup()
patches.undo()


if __name__ == "__main__":
main()
113 changes: 113 additions & 0 deletions apps/presentation/dashboard/smoke/team-adoption-recovery-smoke.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Packaged report interaction + real scoped HTTP/delegation/SQLite adoption.
// Navigation is synthetic; no acceptance/adoption rule or result is mocked.
import assert from "node:assert/strict";
import {spawn} from "node:child_process";
import {once} from "node:events";
import {createInterface} from "node:readline";
import {mkdir} from "node:fs/promises";
import {resolve} from "node:path";
import {resolveTestPython} from "../../../../scripts/test-python.mjs";
import {launchBrowser, loadPlaywright, waitForHttp} from "../../../../examples/dashboard-browser-smoke-support.mjs";

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"],
{cwd: repoRoot, stdio: ["pipe", "pipe", "pipe"]});
const exited = once(fixture, "exit");
const lines = createInterface({input: fixture.stdout});
const iterator = lines[Symbol.asyncIterator]();
let stderr = "";
fixture.stderr.on("data", chunk => {stderr += String(chunk);});
async function next() {const line = await iterator.next(); assert.equal(line.done, false, stderr); return JSON.parse(line.value);}
async function command(value) {fixture.stdin.write(`${value}\n`); return next();}
let server, browser, workspace;
try {
const {origin} = await next();
server = await startServer();
const url = `http://127.0.0.1:${port}/chat/?statusUrl=/status.json`;
await waitForHttp(url);
browser = await launchBrowser(loadPlaywright().chromium);
workspace = await openWorkspacePage({newPage: options => browser.newPage({locale: "zh-CN", ...options})}, url);
const {page, api} = workspace;
await page.locator(".personal-goal-link", {hasText: "Product Release"}).click();
await page.getByRole("navigation", {name: "Goal 视图"}).getByRole("button", {name: "对话", exact: true}).click();
await page.getByRole("button", {name: "开启 LoopX 模式", exact: true}).click();
await page.getByLabel("已注册的协调身份").selectOption("lead");
await page.getByLabel("协调员总 token 额度").fill("100000");
await page.getByRole("button", {name: "保存设置", exact: true}).click();
const configured = api.loopxModeRequests.findLast(row => row.operation === "configure");
const mode = page.__loopxRuntime.loopxModes.get(configured.sessionId);
const writes = [];
let loseAck = true;
await page.route("**/api/chat/sessions/*/loopx", async route => {
const body = route.request().method() === "POST" ? route.request().postDataJSON() : {};
if (!["read", "operations", "adopt"].includes(body.operation)) return route.fallback();
const response = await route.fetch({url: `${origin}/api/chat/sessions/adoption/loopx`,
headers: {...route.request().headers(), origin}});
if (body.operation === "adopt") {
writes.push(body);
if (loseAck) {
assert.equal(response.status(), 200, await response.text());
loseAck = false;
return route.fulfill({status: 503, json: {error: "Synthetic acknowledgement loss after real commit"}});
}
}
return route.fulfill({response});
});
Object.assign(mode, {enabled: true, paused: true, active_turn_id: null, native: {status: "paused", tokenBudget: 100000}});
const results = page.getByRole("region", {name: "团队成果", exact: true});
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");
await adoption.locator("summary").click();
await adoption.getByRole("button", {name: "查找使用此版本的后续结果", exact: true}).click();
await adoption.getByLabel("后续结果", {exact: true}).waitFor();
assert.equal(writes.length, 0);
assert.equal((await command("inspect")).result.adoptions, undefined);
await adoption.getByRole("button", {name: "阅读后续结果", exact: true}).click();
await adoption.getByLabel("证据内容: report.md").waitFor();
assert.equal(writes.length, 0);
const confirm = adoption.getByRole("button", {name: "确认采用于此结果", exact: true});
await confirm.focus(); await page.keyboard.press("Enter");
await adoption.getByText("采用已记录,指定版本与后续结果当前有效。", {exact: true}).waitFor();
assert.equal(writes.length, 1, "Lost acknowledgement reconciles the actual committed receipt without another write");
assert.deepEqual(writes[0], {operation: "adopt", operation_id: "analysis-1", consumer_operation_id: "synthesis-1"});
const recorded = await command("inspect");
assert.equal(recorded.result.adoptions.length, 1);
assert.equal(recorded.result.adoptions[0].state, "current");
assert.equal(recorded.coordinator_paused, true, "The real session remains paused, as shown in the report");
assert.ok(recorded.session_unchanged, "No native lifecycle, mode, wake or extra coordinator Turn changed");
assert.deepEqual(recorded.host_invocations, ["1", "1"]);
await mkdir(outputDir, {recursive: true});
await adoption.getByText("采用已记录,指定版本与后续结果当前有效。", {exact: true}).scrollIntoViewIfNeeded();
await page.screenshot({path: resolve(outputDir, "team-adoption-owner-desktop.png"), animations: "disabled"});
await page.setViewportSize({width: 390, height: 844}); await page.emulateMedia({reducedMotion: "reduce"});
assert.ok(await adoption.evaluate(el => el.scrollWidth <= el.clientWidth));
await adoption.getByText("采用已记录,指定版本与后续结果当前有效。", {exact: true}).scrollIntoViewIfNeeded();
await page.screenshot({path: resolve(outputDir, "team-adoption-owner-mobile.png"), animations: "disabled"});
// Current source still renders, while a real changed receiver input revokes adoption.
await command("withdraw");
await results.getByRole("button", {name: "刷新成果", exact: true}).click();
await results.locator(".goal-team-result-reader > details > summary").click();
await results.getByText("采用证据已失效或无法核验", {exact: true}).waitFor();
assert.equal(await adoption.getByText("采用已记录,指定版本与后续结果当前有效。", {exact: true}).count(), 0);
await adoption.locator("summary").click();
await adoption.getByRole("button", {name: "查找使用此版本的后续结果", exact: true}).click();
await adoption.getByText("已检查的工作中没有可核验的使用结果。", {exact: true}).waitFor();
const refused = await fetch(`${origin}/api/chat/sessions/adoption/loopx`, {method: "POST", headers: {"Content-Type": "application/json", Origin: origin}, body: JSON.stringify(writes[0])});
assert.equal(refused.status, 409, await refused.text());
await command("restore");
await results.getByRole("button", {name: "刷新成果", exact: true}).click();
await results.locator(".goal-team-result-reader > details > summary").click();
await results.getByText("已记录采用 · 后续结果验收有效", {exact: true}).waitFor();
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;
} finally {
await workspace?.close(); await browser?.close(); server?.kill("SIGTERM");
fixture.stdin.end(); if (fixture.exitCode === null) await exited; lines.close();
}
6 changes: 6 additions & 0 deletions apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,12 @@ export function readLoopXTeamWork(sessionId: string, operationId: string) {
method: "POST", body: JSON.stringify({operation: "read", operation_id: operationId}),
});
}
export function adoptLoopXTeamWork(sessionId: string, operationId: string, consumerOperationId: string) {
return requestJson<DelegationReadback>(`/api/chat/sessions/${sessionId}/loopx`, {
method: "POST", body: JSON.stringify({operation: "adopt", operation_id: operationId,
consumer_operation_id: consumerOperationId}),
});
}
export type ManagedGoalResultRow = {
todo_id: string; title: string; producer_agent_id: string; sha256: string;
content_type: string; size_bytes: number; completed_at?: string | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ li[data-check="blocked"] .goal-team-check-pill { border-color: transparent; back
.goal-team-lineage ul { list-style: none; padding: 0; margin: 12px 0; }
.goal-team-lineage li { display: grid; gap: 8px; padding: 0 0 16px 16px; border-left: 2px solid var(--pw-line, #ebebeb); overflow-wrap: anywhere; }
.goal-team-lineage button { justify-self: start; max-width: 100%; overflow-wrap: anywhere; text-align: start; }
.goal-team-adoption { margin-top: 16px; }
.goal-team-adoption label { display: block; margin-block: 12px; }
.goal-team-adoption select { display: block; max-width: 100%; margin-top: 6px; }
.goal-team-adoption > button { margin-block: 6px; }
.goal-team-episode { margin-block: 20px; border: 1px solid var(--pw-line, #ebebeb); border-radius: 10px; padding: 16px; background: var(--pw-bg, #fafafa); }
.goal-team-episode-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.goal-team-episode > p { color: var(--pw-muted, #666); line-height: 1.5; }
Expand Down
Loading
Loading