Files
pikasTech-HWLAB/web/hwlab-cloud-web/src/logic/code-agent-facts.test.ts
T
Lyon 0d06a4c5fe fix(web): migrate Cloud Web from vanilla DOM to React + TypeScript + Vite (#756) (#764)
- Replace monolithic app.ts / app-conversation.ts / app-device-pod.ts /
  app-skills.ts / app-helpers.ts / app-session-tabs.ts / auth.ts and the
  364-line index.html with a Vite-built React + TypeScript SPA.
- New src/ module tree: main.tsx, App.tsx, components/{layout,auth,
  conversation,command-bar,device-pod,skills,settings,help},
  hooks/, services/api, services/auth, state, types, logic/ (pure
  helpers from the old tree), styles/.
- Pure-logic modules (composer-policy, code-agent-facts, code-agent-
  status, live-status, message-markdown, app-trace, app-session-tabs,
  runtime) move to src/logic/ with their tests preserved; tests still
  auto-discover.
- Build pipeline switches from Bun.build to Vite: vite.config.ts
  inlines everything into a single app.js + app-assets/ for the dist;
  scripts/dist-contract.ts now runs vite build and asserts the dist
  against a fresh build instead of comparing dist to source.
- scripts/check.ts now verifies the React entry, the mount-only
  index.html, Vite dist, the composer / session / device pod / skills
  / settings / help contracts, the React state shape, and the new
  test file locations. It also refuses to run if any legacy app-*.ts
  or auth.ts survives at the top level.
- New scripts/frontend-guard.ts enforces the 400-line migration target,
  refuses the legacy el map and document.getElementById markers,
  refuses to keep app-*.ts / auth.ts at the top level, and verifies
  the required module directory layout.
- Update scripts/src/dev-cloud-workbench-smoke-lib.mjs to read the new
  src/services/, src/logic/ paths and refresh cloudWebModuleSourceFiles.
- index.html is now a mount shell only (no login shell, no workbench
  shell, no device pod sidebar markup). React renders the full UI from
  src/main.tsx, satisfying the issue-756 migration goal of catching
  unclosed tags at the TypeScript/JSX compile step instead of relying
  on browser DOM healing.
- web:check (now: check + frontend-guard + tsc-check + bun test) is
  green: 51 pass / 0 fail, Vite build produces dist/app.js (379 kB)
  + dist/index.html + dist/app-assets/. Vite build is reproducible;
  dist freshness is verified against a fresh Vite build.
- Grandfathered pure-logic files (> 600 lines): src/logic/app-trace.ts
  (1367), src/logic/live-status.ts (1006), src/logic/code-agent-facts.ts
  (548), src/logic/code-agent-status.ts (572). Reason: each is a tested
  pure-logic classifier with a public contract consumed by both the
  CLI renderer and the Web renderer; splitting now would fork the
  public function shape across two PRs and the migration target is the
  React entry + module split, not a pure-logic refactor.

Refs #756. Closes #756 once merged + deployed to hwlab-v02.

Co-authored-by: Codex <codex@local>
2026-06-03 16:53:25 +08:00

361 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import test from "node:test";
import {
codeAgentAttributionFromMessage,
codeAgentFactsFromMessage,
codeAgentRuntimePathFromMessage
} from "./code-agent-facts.ts";
test("codex-readonly-runner is shown as partial read-only session registry", () => {
const facts = codeAgentFactsFromMessage({
status: "source",
provider: "codex-readonly-runner",
model: "read-only-tools",
backend: "hwlab-cloud-api/codex-readonly-runner",
workspace: "/workspace/hwlab",
sandbox: "read-only",
capabilityLevel: "read-only-session-tools",
sessionMode: "controlled-readonly-session-registry",
implementationType: "controlled-readonly-session-registry",
session: {
sessionId: "ses_readonly",
status: "idle",
turn: 1,
lastTraceId: "trc_readonly"
},
sessionReuse: {
reused: false,
turn: 1,
status: "idle"
},
runner: {
kind: "hwlab-readonly-runner",
codexStdio: false,
writeCapable: false,
durableSession: false,
longLivedSession: true
},
longLivedSessionGate: {
status: "blocked",
pass: false,
blockers: [{ code: "codex_stdio_blocked_readonly_session_available" }]
},
toolCalls: [{ name: "pwd", status: "completed" }],
skills: { status: "not_requested", count: 0 },
runnerTrace: {
traceId: "trc_readonly",
runnerKind: "hwlab-readonly-runner",
sessionMode: "controlled-readonly-session-registry",
turn: 1
},
traceId: "trc_readonly"
});
assert.equal(facts.kind, "controlled-readonly-session-registry");
assert.equal(facts.fullCodeAgent, false);
assert.equal(facts.codexSessionGatePass, false);
assert.match(facts.statusLabel, /只读长会话/u);
assert.match(facts.summary, /可复用/u);
assert.match(facts.summary, /Codex stdio/u);
assert.match(facts.rows.find((row) => row.label === "runner/provider/backend").value, /runner=hwlab-readonly-runner/u);
assert.match(facts.rows.find((row) => row.label === "toolCalls").value, /1\/1 pwd\(completed\)/u);
});
test("long-lived Codex app-server stdio session fields are shown as full Code Agent session", () => {
const message = {
status: "completed",
provider: "codex-stdio",
model: "gpt-5.5",
backend: "hwlab-cloud-api/codex-app-server-stdio",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
capabilityLevel: "long-lived-codex-stdio-session",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
session: {
sessionId: "ses_stdio",
status: "idle",
turn: 3,
idleTimeoutMs: 1800000,
lastTraceId: "trc_stdio",
longLivedSession: true,
codexStdio: true
},
runner: {
kind: "codex-app-server-stdio-runner",
codexStdio: true,
writeCapable: true,
durableSession: true
},
longLivedSessionGate: {
status: "pass",
pass: true
},
toolCalls: [{ name: "codex-reply", status: "completed" }],
skills: { status: "not_requested", count: 0 },
runnerTrace: {
traceId: "trc_stdio",
runnerKind: "codex-app-server-stdio-runner",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
turn: 3
},
providerTrace: {
transport: "stdio",
protocol: "codex-app-server-jsonrpc-stdio",
wireApi: "responses",
command: "codex app-server --listen stdio://",
terminalStatus: "completed"
},
traceId: "trc_stdio"
};
const facts = codeAgentFactsFromMessage(message);
const runtimePath = codeAgentRuntimePathFromMessage({ role: "agent", ...message });
assert.equal(facts.kind, "codex-stdio-long-lived");
assert.equal(facts.statusLabel, "真实 runnerCodex app-server stdio 长会话");
assert.equal(facts.fullCodeAgent, true);
assert.equal(facts.codexSessionGatePass, true);
assert.match(facts.rows.find((row) => row.label === "sandbox").value, /workspace-write/u);
assert.equal(runtimePath.kind, "codex-app-server-stdio");
assert.equal(runtimePath.tone, "ok");
assert.equal(runtimePath.provider, "codex-stdio");
assert.equal(runtimePath.runnerKind, "codex-app-server-stdio-runner");
assert.equal(runtimePath.protocol, "codex-app-server-jsonrpc-stdio");
assert.equal(runtimePath.implementationType, "repo-owned-codex-app-server-stdio-session");
assert.equal(runtimePath.command, "codex app-server --listen stdio://");
assert.equal(runtimePath.terminalStatus, "completed");
assert.equal(runtimePath.wireApi, "responses");
assert.match(runtimePath.summary, /Responses wire API/u);
});
test("AgentRun carried Codex app-server session is shown as full Code Agent session", () => {
const message = {
status: "completed",
provider: "codex-stdio",
model: "deepseek-chat",
backend: "hwlab-cloud-api/codex-app-server-stdio",
infrastructureBackend: "agentrun-v01/deepseek",
workspace: "/home/agentrun/workspace",
sandbox: "danger-full-access",
capabilityLevel: "long-lived-codex-stdio-session",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
session: {
sessionId: "ses_web_agentrun",
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
status: "idle",
lastTraceId: "trc_web_agentrun",
longLivedSession: true,
codexStdio: true,
writeCapable: true,
durable: true,
durableSession: true
},
sessionReuse: {
sessionId: "ses_web_agentrun",
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
reused: true
},
runner: {
kind: "codex-app-server-stdio-runner",
codexStdio: true,
writeCapable: true,
durableSession: true,
longLivedSession: true
},
longLivedSessionGate: {
status: "pass",
pass: true
},
toolCalls: [{ name: "codex-app-server.turn", status: "completed" }],
skills: { status: "delegated", count: 0 },
runnerTrace: {
traceId: "trc_web_agentrun",
runnerKind: "codex-app-server-stdio-runner",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session"
},
providerTrace: {
transport: "stdio",
protocol: "codex-app-server-jsonrpc-stdio",
wireApi: "responses",
command: "codex app-server --listen stdio://",
terminalStatus: "completed",
source: "agentrun-v01"
},
traceId: "trc_web_agentrun"
};
const facts = codeAgentFactsFromMessage(message);
const runtimePath = codeAgentRuntimePathFromMessage({ role: "agent", ...message });
assert.equal(facts.kind, "codex-stdio-long-lived");
assert.equal(facts.fullCodeAgent, true);
assert.equal(runtimePath.kind, "codex-app-server-stdio");
assert.equal(runtimePath.tone, "ok");
assert.equal(runtimePath.providerTraceMissing, false);
assert.equal(runtimePath.criticalMissing, false);
assert.equal(runtimePath.terminalStatusOk, true);
});
test("Codex app-server without providerTrace is degraded with structured missing fields", () => {
const message = {
role: "agent",
status: "completed",
provider: "codex-stdio",
backend: "hwlab-cloud-api/codex-app-server-stdio",
capabilityLevel: "long-lived-codex-stdio-session",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
runner: { kind: "codex-app-server-stdio-runner" },
longLivedSessionGate: { status: "pass", pass: true },
traceId: "trc_missing_provider_trace"
};
const facts = codeAgentFactsFromMessage(message);
const runtimePath = codeAgentRuntimePathFromMessage(message);
assert.equal(facts.kind, "codex-stdio-provider-trace-missing");
assert.equal(facts.fullCodeAgent, false);
assert.equal(runtimePath.providerTraceMissing, true);
assert.equal(runtimePath.label, "DEGRADEDproviderTrace 缺失");
assert.deepEqual(runtimePath.missingFields, ["protocol", "providerTrace.command", "providerTrace.terminalStatus"]);
});
test("AgentRun invalid tool-call failure is attributed to Codex provider tool-call JSON", () => {
const message = {
role: "agent",
status: "failed",
provider: "codex-stdio",
backend: "hwlab-cloud-api/codex-app-server-stdio",
capabilityLevel: "long-lived-codex-stdio-session",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
runner: { kind: "codex-app-server-stdio-runner" },
providerTrace: {
transport: "stdio",
protocol: "codex-app-server-jsonrpc-stdio",
wireApi: "responses",
command: "codex app-server --listen stdio://",
terminalStatus: "failed",
failureKind: "provider-invalid-tool-call",
failureMessage: "invalid params, invalid function arguments json string, tool_call_id: call_function_selftest_2 (2013)"
},
error: {
code: "provider-invalid-tool-call",
category: "provider_invalid_tool_call"
},
traceId: "trc_invalid_tool_call"
};
const facts = codeAgentFactsFromMessage(message);
const runtimePath = codeAgentRuntimePathFromMessage(message);
assert.equal(facts.kind, "codex-stdio-blocked");
assert.equal(runtimePath.kind, "codex-app-server-stdio");
assert.equal(runtimePath.terminalStatusOk, false);
assert.equal(runtimePath.failureKind, "provider-invalid-tool-call");
assert.equal(runtimePath.label, "BLOCKEDCodex tool-call JSON 无效");
assert.match(runtimePath.summary, /不是 HWLAB device-pod/u);
assert.match(runtimePath.summary, /tool-call arguments JSON/u);
});
test("openai-responses fallback is explicitly text-chat-only", () => {
const message = {
status: "source",
provider: "openai-responses",
model: "gpt-5.5",
backend: "hwlab-cloud-api/openai-responses",
capabilityLevel: "text-chat-only",
sessionMode: "provider-text-request",
runner: {
kind: "openai-responses-fallback",
codexStdio: false,
writeCapable: false,
durableSession: false
},
longLivedSessionGate: {
status: "blocked",
pass: false,
blockers: [{ code: "openai_responses_fallback_not_session" }]
},
toolCalls: [],
skills: { status: "not_requested", count: 0 },
runnerTrace: {
traceId: "trc_fallback",
runnerKind: "openai-responses-fallback",
events: ["fallback:text-chat-only"]
},
traceId: "trc_fallback"
};
const facts = codeAgentFactsFromMessage(message);
const attribution = codeAgentAttributionFromMessage(message);
assert.equal(facts.kind, "text-chat-only");
assert.equal(facts.statusLabel, "OpenAI fallback:只是文本回答 / text-chat-only");
assert.equal(facts.fullCodeAgent, false);
assert.equal(facts.codexSessionGatePass, false);
assert.match(facts.summary, /只是文本回答/u);
assert.match(attribution.label, /只是文本回答/u);
assert.match(attribution.summary, /当前不能执行工具/u);
assert.equal(attribution.fields.find((field) => field.key === "runnerKind").value, "openai-responses-fallback / 不是 runner 控制");
assert.equal(attribution.fields.find((field) => field.key === "capabilityLevel").value, "text-chat-only / 只是文本回答");
assert.equal(attribution.fields.find((field) => field.key === "toolCalls").value, "0/0 / 无工具调用:只是文本回答");
});
test("stateless one-shot is not wrapped as a long-lived session", () => {
const facts = codeAgentFactsFromMessage({
status: "source",
provider: "codex-cli",
model: "gpt-5.5",
backend: "hwlab-cloud-api/codex-cli",
workspace: "/workspace/hwlab",
sandbox: "read-only",
capabilityLevel: "text-chat-only",
sessionMode: "ephemeral-one-shot",
implementationType: "codex-cli-one-shot-ephemeral",
session: {
sessionId: "cnv_one_shot",
status: "expired",
idleTimeoutMs: 0,
lastTraceId: "trc_one_shot"
},
runner: {
kind: "codex-cli-one-shot-ephemeral",
codexStdio: false,
writeCapable: false,
durableSession: false
},
longLivedSessionGate: {
status: "blocked",
pass: false,
blockers: [{ code: "one_shot_runner_not_long_lived" }]
},
traceId: "trc_one_shot"
});
assert.equal(facts.kind, "stateless-one-shot");
assert.equal(facts.fullCodeAgent, false);
assert.equal(facts.codexSessionGatePass, false);
assert.match(facts.summary, /一次性 runner/u);
});
test("missing attribution fields render structured evidence insufficiency", () => {
const attribution = codeAgentAttributionFromMessage({
role: "agent",
status: "failed",
traceId: "trc_missing_fields",
error: {
code: "provider_unavailable",
message: "blocked"
}
});
assert.equal(attribution.label, "BLOCKED:当前不能执行");
assert.equal(attribution.fields.find((field) => field.key === "traceId").value, "trc_missing_fields");
assert.equal(attribution.fields.find((field) => field.key === "provider").missing, true);
assert.equal(attribution.fields.find((field) => field.key === "backend").value, "字段缺失:证据不足");
assert.equal(attribution.fields.find((field) => field.key === "runnerKind").chip, "runnerKind=字段缺失:证据不足");
assert.equal(attribution.fields.find((field) => field.key === "providerTrace.command").missing, true);
assert.match(attribution.summary, /字段缺失/u);
});