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
@@ -0,0 +1,79 @@
"""Real account HTTP/store with synthetic native process IO; no auth or model."""
from __future__ import annotations

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

repo = Path(__file__).resolve().parents[4]
sys.path.insert(0, str(repo))
sys.path.insert(0, str(repo / "packages/loopx-codex-provider-routing/src"))

from loopx.chat_server import ChatHTTPServer, ChatRequestHandler
from loopx.presentation.codex_accounts_api import CodexAccountService


def main() -> None:
native = importlib.import_module("loopx_codex_provider_routing.native")
native.discover = lambda: {"synthetic": True}
clients = [
{"pid": 303, "kind": "unmanaged", "process": "virtual-machine", "restartable": False,
"manager": {"pid": 304, "process": "limactl", "instance": "colima-example", "status": "Running"},
"restart_blocker": "external_workload_unverified"},
{"pid": 404, "kind": "unmanaged", "process": "standalone-client", "restartable": False,
"restart_blocker": "manager_unverified"},
{"pid": 202, "kind": "service", "process": "codex", "restartable": True,
"service": "dev.example.renamed-chat-with-a-long-installed-service-label"},
*[{"pid": 1000 + i, "kind": "app", "process": "codex" if i == 0 else "node",
"restartable": True} for i in range(18)],
]
mode, calls = "many", []

def transport(adapter, operation, **parameters):
calls.append(operation)
if operation == "status":
return {"slots": ["a", "b", "c"], "active_slot": "a", "selection_revision": "synthetic",
"current_login_matches": True, "recovery_required": False}
assert operation == "clients", "Reading settings cannot switch, signal, observe quota or recover"
if mode == "unavailable":
raise native.HostError("ownership_unverified")
rows = [] if mode == "empty" else clients if mode in {"many", "partial"} else clients[:1]
return {"complete": mode != "partial", "clients": rows}

with tempfile.TemporaryDirectory(prefix="loopx-account-clients-") as directory:
runtime = Path(directory).resolve()
provider = native.NativeAccounts(runtime, transport=transport)
provider.configure(expected_revision=0, enabled=False, slots=["a", "b", "c"], restart_clients=True)
before = provider.path.read_bytes()
calls.clear()
service = CodexAccountService(runtime)
service.provider = provider
server = ChatHTTPServer(("127.0.0.1", 0), ChatRequestHandler)
server.verbose = False
server.codex_account_service = service
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 line in sys.stdin:
command = line.strip()
if command == "inspect":
print(json.dumps({"state_unchanged": provider.path.read_bytes() == before,
"calls": calls, "watcher_started": service.watcher.thread is not None}), flush=True)
elif command in {"many", "sparse", "empty", "partial", "unavailable"}:
mode = command
print(json.dumps({"mode": mode}), flush=True)
else:
raise ValueError("Unknown fixture command")
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
service.close()


if __name__ == "__main__":
main()
103 changes: 103 additions & 0 deletions apps/presentation/dashboard/smoke/codex-account-clients-smoke.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Packaged settings + real Chat HTTP/native account store; synthetic process IO.
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 { cleanupBrowserSmoke, 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, fetchWorkspaceBackend } = await import("../../../../examples/personal-workspace-browser/scenario-context.mjs");
const fixture = spawn(resolveTestPython({ repoRoot }), ["-u", "apps/presentation/dashboard/smoke/codex-account-clients-fixture.py"],
{ cwd: repoRoot, stdio: ["pipe", "pipe", "pipe"] });
const exited = once(fixture, "exit");
let stderr = "";
fixture.stderr.on("data", value => { stderr += String(value); });
const iterator = createInterface({ input: fixture.stdout })[Symbol.asyncIterator]();
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;
const requests = [];
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, {
beforeGoto(_api, page) {
return page.route("**/api/chat/codex-accounts**", async route => {
requests.push({method: route.request().method(), path: new URL(route.request().url()).pathname});
assert.equal(route.request().method(), "GET", "Inspection must never start an account operation");
await route.fulfill({response: await fetchWorkspaceBackend(route, origin)});
});
},
});
const { page, api } = workspace;
await page.getByRole("button", {name: "设置", exact: true}).click();
await page.locator(".personal-settings-tabs").getByRole("button", {name: "模型 Provider 配置", exact: true}).click();
const panel = page.getByTestId("codex-account-holders");
const summary = panel.locator("summary");
await summary.filter({hasText: "21 个占用 · 2 个暂不能自动重启"}).waitFor();
assert.equal(await panel.getAttribute("open"), null, "The process table must not displace account status");
await mkdir(outputDir, {recursive: true});
await page.screenshot({path: resolve(outputDir, "auth-holders-default-desktop.png"), animations: "disabled"});
await summary.focus();
await page.keyboard.press("Enter");
await panel.getByText("colima-example", {exact: false}).waitFor();
assert.equal(await panel.locator("li").count(), 4, "Group one App's helpers without repeating restart copy");
for (let pid = 1000; pid < 1018; pid++) assert.ok((await panel.innerText()).includes(String(pid)), "Every observed PID remains readable");
assert.match(await panel.locator("li").first().innerText(), /外部工作负载/);
assert.match(await panel.innerText(), /dev.example.renamed-chat-with-a-long-installed-service-label/);
await page.screenshot({path: resolve(outputDir, "auth-holders-expanded-desktop.png"), animations: "disabled"});
const inventoryReads = () => requests.filter(row => row.path.endsWith("/clients")).length;
const beforePolling = inventoryReads();
await page.waitForTimeout(5500); // Cross one existing status timer tick.
assert.equal(inventoryReads(), beforePolling, "The status timer must not repeatedly scan processes");
await page.setViewportSize({width: 390, height: 844});
await panel.scrollIntoViewIfNeeded();
assert.ok(await page.locator("body").evaluate(body => body.scrollWidth <= innerWidth + 1));
await page.screenshot({path: resolve(outputDir, "auth-holders-expanded-narrow.png"), animations: "disabled"});
for (const mode of ["unavailable", "partial", "empty", "sparse", "many"]) {
await command(mode);
await panel.getByRole("button", {name: "重新检查占用", exact: true}).click();
const expected = mode === "unavailable" || mode === "partial" ? "占用情况暂时无法确认"
: mode === "empty" ? "本次未发现登录占用" : mode === "sparse" ? "1 个占用 · 1 个暂不能自动重启" : "21 个占用 · 2 个暂不能自动重启";
await summary.filter({hasText: expected}).waitFor();
assert.equal(await panel.locator("li").count(), mode === "many" ? 4 : mode === "sparse" ? 1 : 0);
if (mode === "unavailable") await page.screenshot({path: resolve(outputDir, "auth-holders-unavailable-narrow.png"), animations: "disabled"});
}
await page.setViewportSize({width: 1512, height: 982});
await page.locator(".personal-settings-tabs").getByRole("button", {name: "语言", exact: true}).click();
await page.getByRole("radio", {name: "English", exact: true}).click();
await page.locator(".personal-settings-tabs").getByRole("button", {name: "Model provider", exact: true}).click();
await page.getByTestId("codex-account-holders").locator("summary").filter({hasText: "21 holders · 2 cannot restart automatically"}).waitFor();
await page.getByTestId("codex-account-holders").locator("summary").click();
await page.getByText("External workload idle and recovery conditions are unverified", {exact: true}).waitFor();
await page.screenshot({path: resolve(outputDir, "auth-holders-expanded-en.png"), animations: "disabled"});
const state = await command("inspect");
assert.equal(state.state_unchanged, true);
assert.equal(state.watcher_started, false);
assert.ok(state.calls.every(operation => ["status", "clients"].includes(operation)));
assert.equal(api.turnRequests.length, 0);
assert.deepEqual(workspace.errors, []);
console.log(JSON.stringify({ok: true, boundary: "packaged UI / real HTTP and native policy store / synthetic process IO",
inventoryReads: inventoryReads(), effects: 0, fullOwnerCount: 21, recovery: ["unavailable", "partial", "empty", "sparse", "many"], languages: ["zh-CN", "en"]}));
} catch (error) {
if (workspace) console.error((await workspace.page.locator("body").innerText()).slice(0, 2500));
throw error;
} finally {
await workspace?.close();
await browser?.close();
fixture.stdin.end();
if (fixture.exitCode === null) await Promise.race([exited, new Promise(resolve => setTimeout(resolve, 5000))]);
if (fixture.exitCode === null) fixture.kill("SIGTERM");
await cleanupBrowserSmoke({server, fixturePaths: []});
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,17 @@
.personal-codex-accounts details { border-top: 1px solid var(--pw-line); padding-top: 16px; }
.personal-codex-accounts summary { cursor: pointer; padding: 4px 0; }
.personal-codex-accounts details p { margin: 12px 0; }
.codex-account-holders ul { list-style: none; padding: 0; margin: 16px 0 0; }
.codex-account-holders summary { color: var(--pw-text); }
.codex-account-holders li { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; padding: 12px 0; border-top: 1px solid var(--pw-line); overflow-wrap: anywhere; }
.codex-account-holders li > div { display: grid; gap: 4px; }
.codex-account-holders strong { color: var(--pw-text); font-weight: 500; }
.codex-account-holders li > span { color: var(--pw-text); }
.codex-account-holders .codex-account-actions { margin-top: 12px; }
.personal-codex-accounts [role="alert"] { display: flex; gap: 8px; align-items: flex-start; color: var(--pw-red); }
@media (max-width: 640px) {
.codex-account-list { grid-template-columns: 1fr; gap: 20px; }
.codex-account-list dl { grid-template-columns: 1fr 1fr; width: 100%; }
.codex-account-list article > button { width: 100%; }
.codex-account-holders li { grid-template-columns: 1fr; gap: 8px; }
}
Loading
Loading