255 lines
9.4 KiB
JavaScript
255 lines
9.4 KiB
JavaScript
import assert from "node:assert/strict";
|
||
import test from "node:test";
|
||
|
||
import {
|
||
codeAgentAttributionFromMessage,
|
||
codeAgentFactsFromMessage,
|
||
codeAgentRuntimePathFromMessage
|
||
} from "./code-agent-facts.mjs";
|
||
|
||
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, "真实 runner:Codex 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("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, "DEGRADED:providerTrace 缺失");
|
||
assert.deepEqual(runtimePath.missingFields, ["protocol", "providerTrace.command", "providerTrace.terminalStatus"]);
|
||
});
|
||
|
||
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);
|
||
});
|