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
2 changes: 1 addition & 1 deletion apps/presentation/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"smoke:delegation-preflight-browser": "node --experimental-strip-types smoke/delegation-preflight-browser-smoke.mjs",
"smoke:personal-workspace": "npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:workspace-locale": "LOOPX_PERSONAL_WORKSPACE_SCENARIO=workspace-locale node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:personal-workspace-packaged": "npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser/server-startup.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs && LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-evidence-return-smoke.mjs",
"smoke:personal-workspace-packaged": "npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser/server-startup.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs && LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-evidence-return-smoke.mjs && LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-feedback-recovery-smoke.mjs",
"smoke:todo-resume-condition": "rm -rf /tmp/loopx-todo-resume-condition-smoke && tsc --ignoreConfig --target ES2022 --module commonjs --moduleResolution node --ignoreDeprecations 6.0 --skipLibCheck --strict --outDir /tmp/loopx-todo-resume-condition-smoke smoke/todo-resume-condition-smoke.ts src/features/personal-workspace/todo-resume-condition.ts && node /tmp/loopx-todo-resume-condition-smoke/smoke/todo-resume-condition-smoke.js",
"smoke:presentation-surface-schema": "rm -rf /tmp/loopx-presentation-surface-schema-smoke && tsc --ignoreConfig --target ES2022 --module CommonJS --moduleResolution Node --ignoreDeprecations 6.0 --skipLibCheck --strict --resolveJsonModule --esModuleInterop --outDir /tmp/loopx-presentation-surface-schema-smoke smoke/presentation-surface-schema-smoke.ts src/data/status.ts src/data/decision-research.ts src/data/goal-channel-frontstage.ts && NODE_PATH=\"$PWD/node_modules\" node /tmp/loopx-presentation-surface-schema-smoke/apps/presentation/dashboard/smoke/presentation-surface-schema-smoke.js",
"smoke:projection-localization": "rm -rf /tmp/loopx-projection-localization-smoke && tsc --ignoreConfig --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck --strict --outDir /tmp/loopx-projection-localization-smoke smoke/projection-localization-smoke.ts src/features/personal-workspace/projection-localization.ts && node /tmp/loopx-projection-localization-smoke/smoke/projection-localization-smoke.js",
Expand Down
62 changes: 62 additions & 0 deletions apps/presentation/dashboard/smoke/team-feedback-http-fixture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Disposable production inbox/store; the UI fixture supplies team evidence only."""
from __future__ import annotations

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

sys.path.insert(0, str(Path(__file__).resolve().parents[4]))

from loopx.chat_runtime import ChatRuntimeController
from loopx.chat_server import ChatHTTPServer, ChatRequestHandler
from loopx.chat_store import ChatSessionStore


def main() -> None:
with tempfile.TemporaryDirectory(prefix="loopx-team-feedback-") as directory:
root = Path(directory)
registry = root / "registry.json"
registry.write_text(json.dumps({"schema_version": "0.1", "goals": [
{"id": "research", "repo": str(root), "status": "active"},
]}), encoding="utf-8")
store = ChatSessionStore(root / "runtime")
runtime = ChatRuntimeController(store=store, codex_bin="missing-codex", registry_path=registry)
store.create_session(goal_id="research", agent_id="codex", channel_id="goal.research",
upstream_thread_id="fixture", session_id="feedback",
adapter_kind="codex_app_server", upstream_mode="chat")
# Establish local admission facts without dispatching a model or worker.
accepted = store.accept_managed_turn("feedback", client_turn_id="fixture-turn",
message="Review the report", loopx_execution=True)
store.update_session("feedback", active_turn_id=accepted.turn["turn_id"],
loopx_mode={"enabled": True, "paused": False})
server = ChatHTTPServer(("127.0.0.1", 0), ChatRequestHandler)
server.verbose = False
server.registry_path = registry
server.runtime_root = root / "runtime"
server.chat_store = store
server.runtime_controller = runtime
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)
try:
for command in sys.stdin:
if command.strip() == "pause":
store.update_session("feedback", loopx_mode={"enabled": True, "paused": True})
print(json.dumps({"paused": True}), flush=True)
elif command.strip() == "inspect":
print(json.dumps({"ingress": store.loopx_ingress("feedback"),
"messages": store.messages("feedback"),
"turn_count": sum(1 for _ in store.sessions_root.glob("*/turns/*.json"))}), flush=True)
else:
raise ValueError("Unknown fixture command")
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
runtime.close()


if __name__ == "__main__":
main()
141 changes: 141 additions & 0 deletions apps/presentation/dashboard/smoke/team-feedback-recovery-smoke.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Packaged evidence UI + production inbox HTTP/store; no model or active Goal writes.
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-feedback-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);
Object.assign(mode, {enabled: true, paused: false, active_turn_id: "fixture-loopx-turn",
native: {status: "active", tokenBudget: 100000}, fixtureCorrectionEpisode: true});
await page.getByText("LoopX · 正在推进", {exact: true}).waitFor();
const posts = [];
let loseAcknowledgement = true;
await page.route("**/api/chat/sessions/*/loopx", async route => {
const body = route.request().method() === "POST" ? route.request().postDataJSON() : {};
if (body.operation !== "message") return route.fallback();
posts.push(body);
const response = await route.fetch({url: `${origin}/api/chat/sessions/feedback/loopx`});
if (loseAcknowledgement) {
assert.equal(response.status(), 200, await response.text());
loseAcknowledgement = false;
return route.fulfill({status: 503, json: {error: "Synthetic acknowledgement loss after commit"}});
}
return route.fulfill({response});
});
const open = () => page.getByRole("button", {name: "团队执行情况", exact: true}).click();
const dialog = page.getByRole("dialog", {name: "团队执行情况"});
const evidence = page.getByRole("region", {name: "执行证据", exact: true});
const feedback = evidence.getByLabel("向协调员反馈此执行", {exact: true});
const select = () => dialog.getByRole("button", {name: "查看证据与反馈", exact: true}).first().click();
await open(); await select();
await feedback.fill("Use the revised disclosure, and keep the source comparison.");
await evidence.getByRole("button", {name: "发送反馈", exact: true}).click();
await evidence.getByRole("alert").filter({hasText: "未确认投递"}).waitFor();
const first = posts[0];
assert.match(first.message, /accepted-analysis/);
assert.match(first.message, /sha256:/);
const committed = await command("inspect");
assert.equal(committed.ingress.length, 1, "The failed response follows an actual durable inbox commit");
await evidence.getByRole("button", {name: "核验关联执行", exact: true}).click();
await evidence.getByRole("button", {name: "阅读原始产物", exact: true}).click();
await evidence.getByLabel("向协调员反馈此执行", {exact: true}).waitFor();
assert.equal(await feedback.inputValue(), "", "Feedback is scoped to its originating operation");
await dialog.getByRole("button", {name: "返回上一份证据", exact: true}).click();
await feedback.waitFor();
assert.equal(await feedback.inputValue(), "Use the revised disclosure, and keep the source comparison.",
"Returning must retain an uncertain submission rather than mint another feedback identity");
await page.reload();
await page.getByText("LoopX · 正在推进", {exact: true}).waitFor();
const unavailableEvidence = route => route.request().method() === "POST"
&& route.request().postDataJSON()?.operation === "read"
&& route.request().postDataJSON()?.operation_id === "accepted-analysis"
? route.fulfill({status: 409, json: {error: "Synthetic output version withdrawn"}}) : route.fallback();
await page.route("**/api/chat/sessions/*/loopx", unavailableEvidence);
await open(); await select();
await evidence.getByRole("alert").filter({hasText: "已清除上次证据"}).waitFor();
await evidence.getByRole("button", {name: "重试同一条反馈", exact: true}).waitFor();
assert.equal(posts.length, 1, "Reload and evidence navigation cannot send feedback");
await mkdir(outputDir, {recursive: true});
await evidence.getByRole("button", {name: "重试同一条反馈", exact: true}).scrollIntoViewIfNeeded();
await page.screenshot({path: resolve(outputDir, "team-feedback-unconfirmed-desktop.png"), animations: "disabled"});
await evidence.getByRole("button", {name: "重试同一条反馈", exact: true}).focus();
await page.keyboard.press("Enter");
await evidence.getByText("已进入协调员收件箱,等待读取", {exact: true}).waitFor();
assert.deepEqual(posts[1], first, "Retry preserves the exact id, body and observed artifact versions");
const recovered = await command("inspect");
assert.equal(recovered.ingress.length, 1);
assert.equal(recovered.messages.filter(row => row.origin === "loopx_inbox").length, 1);
assert.equal(recovered.turn_count, committed.turn_count, "Feedback never dispatches another model Turn");
assert.equal(await evidence.getByText("已进入协调员收件箱,等待读取", {exact: true}).count(), 1,
"An inbox receipt is presented as awaiting read, never as applied feedback");
await page.unroute("**/api/chat/sessions/*/loopx", unavailableEvidence);
await evidence.getByRole("button", {name: "重新读取证据", exact: true}).click();
await evidence.getByLabel("证据内容: report.json").waitFor();
await mkdir(outputDir, {recursive: true});
await feedback.scrollIntoViewIfNeeded();
await page.screenshot({path: resolve(outputDir, "team-feedback-recovered-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 evidence.getByText("已进入协调员收件箱,等待读取", {exact: true}).scrollIntoViewIfNeeded();
await page.screenshot({path: resolve(outputDir, "team-feedback-recovered-mobile.png"), animations: "disabled"});
await evidence.getByRole("button", {name: "撰写新的反馈", exact: true}).click();
await feedback.fill("A separate explicitly requested correction.");
await command("pause");
await evidence.getByRole("button", {name: "发送反馈", exact: true}).click();
await evidence.getByRole("alert").filter({hasText: "未确认投递"}).waitFor();
assert.match(posts.at(-1).message, /A separate explicitly requested correction/);
assert.notEqual(posts.at(-1).operation_id, first.operation_id);
const refused = await command("inspect");
assert.equal(refused.ingress.length, 1, "Real typed admission rejects feedback to a paused coordinator");
assert.equal(refused.turn_count, committed.turn_count);
await page.evaluate(sessionId => sessionStorage.setItem(`loopx-team-feedback:${JSON.stringify([sessionId, "original-analysis"])}`, "malformed"), configured.sessionId);
await evidence.getByRole("button", {name: "核验关联执行", exact: true}).click();
await evidence.getByRole("button", {name: "阅读原始产物", exact: true}).click();
await evidence.getByRole("alert").filter({hasText: "无法恢复上次反馈"}).waitFor();
assert.ok(await evidence.getByRole("button", {name: "发送反馈", exact: true}).isDisabled());
assert.equal(posts.length, 3, "Malformed local recovery cannot silently create a replacement submission");
console.log("team-feedback-recovery: passed (packaged UI, production inbox/store, lost receipt, navigation/reload, exact retry, scoped input, paused admission, 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();
}
Loading
Loading