fix: 接入真实 Codex stdio trace
This commit is contained in:
+111
-529
@@ -37,52 +37,30 @@ function logOk(name) {
|
||||
process.stdout.write(`[code-agent-chat-smoke] ok ${name}\n`);
|
||||
}
|
||||
|
||||
function runnerTraceLabels(payload) {
|
||||
return Array.isArray(payload?.runnerTrace?.events)
|
||||
? payload.runnerTrace.events.map((event) => event?.label ?? event).filter(Boolean)
|
||||
: [];
|
||||
}
|
||||
|
||||
async function runLocalContractSmoke() {
|
||||
const completed = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: "cnv_code-agent-chat-smoke",
|
||||
traceId: "trc_code-agent-chat-smoke",
|
||||
projectId: "prj_code-agent-chat-smoke",
|
||||
message: "请用一句话说明当前 HWLAB 工作台可以做什么。"
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-22T00:00:00.000Z",
|
||||
callProvider: async ({ providerPlan }) => ({
|
||||
provider: providerPlan.provider,
|
||||
model: providerPlan.model,
|
||||
backend: providerPlan.backend,
|
||||
content: "HWLAB 工作台可以用 Code Agent 对话来整理任务、查看资源并保持硬件变更受控。",
|
||||
usage: null,
|
||||
providerTrace: {
|
||||
source: "schema-smoke-provider"
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
validateCodeAgentChatSchema(completed);
|
||||
assert.equal(completed.conversationId, "cnv_code-agent-chat-smoke");
|
||||
assert.equal(completed.sessionId, "cnv_code-agent-chat-smoke");
|
||||
assert.match(completed.messageId, /^msg_/);
|
||||
assert.equal(completed.status, "completed");
|
||||
assert.equal(completed.createdAt, "2026-05-22T00:00:00.000Z");
|
||||
assert.equal(completed.updatedAt, "2026-05-22T00:00:00.000Z");
|
||||
assert.equal(completed.traceId, "trc_code-agent-chat-smoke");
|
||||
assert.equal(completed.error, undefined);
|
||||
assert.match(completed.reply.content, /Code Agent/);
|
||||
logOk("completed schema");
|
||||
|
||||
const sourceReadiness = classifyCodeAgentChatReadiness(completed, { realDevLive: false });
|
||||
assert.equal(sourceReadiness.status, "blocked");
|
||||
assert.equal(sourceReadiness.level, "SOURCE");
|
||||
assert.equal(sourceReadiness.devLiveReplyPass, false);
|
||||
logOk("local stub completion is not #143 DEV-LIVE pass");
|
||||
|
||||
const echoReadiness = classifyCodeAgentChatReadiness({
|
||||
...completed,
|
||||
conversationId: "cnv_code-agent-chat-smoke-echo",
|
||||
sessionId: "cnv_code-agent-chat-smoke-echo",
|
||||
messageId: "msg_code-agent-chat-smoke-echo",
|
||||
status: "completed",
|
||||
createdAt: "2026-05-22T00:00:00.000Z",
|
||||
updatedAt: "2026-05-22T00:00:00.000Z",
|
||||
traceId: "trc_code-agent-chat-smoke-echo",
|
||||
provider: "echo-mock",
|
||||
model: "mock-model",
|
||||
backend: "hwlab-cloud-api/echo-mock"
|
||||
backend: "hwlab-cloud-api/echo-mock",
|
||||
reply: {
|
||||
messageId: "msg_code-agent-chat-smoke-echo",
|
||||
role: "assistant",
|
||||
content: "mock reply",
|
||||
createdAt: "2026-05-22T00:00:00.000Z"
|
||||
}
|
||||
}, { realDevLive: true, httpStatus: 200 });
|
||||
assert.equal(echoReadiness.status, "blocked");
|
||||
assert.equal(echoReadiness.level, "BLOCKED/untrusted-completion");
|
||||
@@ -91,138 +69,48 @@ async function runLocalContractSmoke() {
|
||||
assert.match(echoReadiness.reason, /echo\/mock\/stub/u);
|
||||
logOk("echo/mock completion is not real DEV-LIVE pass");
|
||||
|
||||
const failed = await handleCodeAgentChat(
|
||||
{
|
||||
traceId: "trc_code-agent-chat-smoke-failed",
|
||||
message: "你好"
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-22T00:01:00.000Z",
|
||||
env: {
|
||||
PATH: "",
|
||||
HWLAB_CODE_AGENT_PROVIDER: "codex-cli",
|
||||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||||
HWLAB_CODE_AGENT_CODEX_COMMAND: "/tmp/hwlab-missing-codex"
|
||||
for (const [index, message] of [
|
||||
"列出可以使用的skill",
|
||||
"pwd",
|
||||
"ls .",
|
||||
"rg --files .",
|
||||
"请用cat读取 package.json"
|
||||
].entries()) {
|
||||
const blocked = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: `cnv_code-agent-chat-blocked-${index}`,
|
||||
traceId: `trc_code-agent-chat-blocked-${index}`,
|
||||
message
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-22T00:01:00.000Z",
|
||||
env: {
|
||||
PATH: "",
|
||||
HWLAB_CODE_AGENT_PROVIDER: "codex-cli",
|
||||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||||
HWLAB_CODE_AGENT_CODEX_COMMAND: "/tmp/hwlab-missing-codex"
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
validateCodeAgentChatSchema(failed);
|
||||
assert.match(failed.conversationId, /^cnv_/);
|
||||
assert.match(failed.sessionId, /^cnv_/);
|
||||
assert.match(failed.messageId, /^msg_/);
|
||||
assert.equal(failed.status, "failed");
|
||||
assert.equal(failed.createdAt, "2026-05-22T00:01:00.000Z");
|
||||
assert.equal(failed.updatedAt, "2026-05-22T00:01:00.000Z");
|
||||
assert.equal(failed.traceId, "trc_code-agent-chat-smoke-failed");
|
||||
assert.equal(failed.provider, "codex-cli");
|
||||
assert.equal(failed.model, "gpt-test");
|
||||
assert.equal(failed.backend, "hwlab-cloud-api/codex-cli");
|
||||
assert.equal(failed.error.code, "codex_cli_binary_missing");
|
||||
assert.equal(failed.error.blocker.code, "codex_cli_binary_missing");
|
||||
assert.match(failed.error.nextEvidence, /HWLAB_CODE_AGENT_CODEX_COMMAND|PATH/u);
|
||||
assert.match(failed.error.message, /Codex CLI command is not available/);
|
||||
assert.deepEqual(failed.error.missingCommands, ["/tmp/hwlab-missing-codex"]);
|
||||
assert.ok(failed.error.missingEnv.includes("OPENAI_API_KEY"));
|
||||
assert.equal(failed.availability.status, "partial");
|
||||
assert.match(failed.availability.blocker, /Codex CLI command/u);
|
||||
assert.equal(failed.availability.reason, "codex_cli_binary_missing");
|
||||
assert.equal(failed.availability.runner.ready, true);
|
||||
assert.equal(failed.availability.codexStdio.runtimeContract.binary.status, "missing");
|
||||
assert.equal(failed.availability.codexStdio.runtimeContract.lifecycleSupervisor.status, "blocked");
|
||||
assert.equal(failed.availability.codexStdio.runtimeContract.stdioProtocol.status, "blocked");
|
||||
assert.ok(failed.availability.codexStdio.blockerCodes.includes("codex_cli_binary_missing"));
|
||||
assert.ok(failed.availability.codexStdio.blockerCodes.includes("runner_lifecycle_missing"));
|
||||
assert.ok(failed.availability.codexStdio.blockerCodes.includes("stdio_protocol_not_wired"));
|
||||
assert.match(failed.availability.summary, /受控只读 runner/u);
|
||||
assert.match(failed.availability.summary, /hwlab-code-agent-provider\/openai-api-key/u);
|
||||
assert.equal(JSON.stringify(failed).includes("sk-"), false);
|
||||
assert.equal(Object.hasOwn(failed, "reply"), false);
|
||||
logOk("failed provider-gap schema");
|
||||
|
||||
const credentialReadiness = classifyCodeAgentChatReadiness(failed, { realDevLive: true });
|
||||
assert.equal(credentialReadiness.status, "blocked");
|
||||
assert.equal(credentialReadiness.level, "BLOCKED");
|
||||
assert.equal(credentialReadiness.blocker, "runtime");
|
||||
assert.equal(credentialReadiness.devLiveReplyPass, false);
|
||||
logOk("codex binary missing blocker readiness");
|
||||
|
||||
const upstreamBlocked = classifyCodeAgentChatReadiness({
|
||||
...failed,
|
||||
error: {
|
||||
code: "provider_unavailable",
|
||||
message: "OpenAI Responses returned HTTP 502: request rejected",
|
||||
providerStatus: 502,
|
||||
missingEnv: []
|
||||
},
|
||||
availability: {
|
||||
status: "available"
|
||||
}
|
||||
}, { realDevLive: true, httpStatus: 200 });
|
||||
assert.equal(upstreamBlocked.status, "blocked");
|
||||
assert.equal(upstreamBlocked.level, "BLOCKED/provider");
|
||||
assert.equal(upstreamBlocked.blocker, "provider-upstream");
|
||||
assert.equal(upstreamBlocked.providerStatus, 502);
|
||||
assert.equal(upstreamBlocked.devLiveReplyPass, false);
|
||||
logOk("provider 502 blocker readiness");
|
||||
|
||||
const completedLivePayload = {
|
||||
...completed,
|
||||
provider: "openai-responses",
|
||||
model: "gpt-5.5",
|
||||
backend: "hwlab-cloud-api/openai-responses"
|
||||
};
|
||||
|
||||
const runnerPwd = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: "cnv_code-agent-chat-runner-pwd",
|
||||
traceId: "trc_code-agent-chat-runner-pwd",
|
||||
message: "用pwd列出你当前的工作目录"
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-22T00:02:00.000Z",
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd()
|
||||
}
|
||||
}
|
||||
);
|
||||
validateCodeAgentChatSchema(runnerPwd);
|
||||
assert.equal(runnerPwd.status, "completed");
|
||||
assert.equal(runnerPwd.provider, "codex-readonly-runner");
|
||||
assert.equal(runnerPwd.workspace, process.cwd());
|
||||
assert.equal(runnerPwd.sandbox, "read-only");
|
||||
assert.equal(runnerPwd.capabilityLevel, "read-only-session-tools");
|
||||
assert.equal(runnerPwd.session.status, "idle");
|
||||
assert.equal(runnerPwd.session.workspace, process.cwd());
|
||||
assert.equal(runnerPwd.session.sandbox, "read-only");
|
||||
assert.equal(runnerPwd.session.idleTimeoutMs, 1800000);
|
||||
assert.equal(runnerPwd.session.lastTraceId, "trc_code-agent-chat-runner-pwd");
|
||||
assert.equal(runnerPwd.sessionMode, "controlled-readonly-session-registry");
|
||||
assert.equal(runnerPwd.implementationType, "controlled-readonly-session-registry");
|
||||
assert.equal(runnerPwd.runner.session, "controlled-readonly-session-registry");
|
||||
assert.equal(runnerPwd.runner.codexStdio, false);
|
||||
assert.equal(runnerPwd.runner.longLivedSession, true);
|
||||
assert.equal(runnerPwd.runner.writeCapable, false);
|
||||
assert.equal(runnerPwd.runner.durableSession, false);
|
||||
assert.equal(runnerPwd.sessionReuse.reused, false);
|
||||
assert.equal(runnerPwd.sessionReuse.turn, 1);
|
||||
assert.equal(runnerPwd.sessionReuse.status, "idle");
|
||||
assert.ok(runnerPwd.runnerLimitations.includes("not-codex-stdio"));
|
||||
assert.equal(runnerPwd.codexStdioFeasibility.status, "blocked");
|
||||
assert.ok(runnerPwd.codexStdioFeasibility.blockerCodes.includes("stdio_protocol_not_wired"));
|
||||
assert.equal(runnerPwd.codexStdioFeasibility.runtimeContract.stdioProtocol.status, "blocked");
|
||||
assert.equal(runnerPwd.codexStdioFeasibility.runtimeContract.lifecycleSupervisor.status, "blocked");
|
||||
assert.equal(runnerPwd.longLivedSessionGate.status, "blocked");
|
||||
assert.equal(runnerPwd.longLivedSessionGate.pass, false);
|
||||
assert.ok(runnerPwd.longLivedSessionGate.blockers.some((blocker) => blocker.code === "codex_stdio_blocked_readonly_session_available"));
|
||||
assert.equal(runnerPwd.toolCalls[0].name, "pwd");
|
||||
assert.equal(runnerPwd.toolCalls[0].status, "completed");
|
||||
const runnerCapability = classifyCodexRunnerCapability(runnerPwd, { httpStatus: 200 });
|
||||
assert.equal(runnerCapability.status, "blocked");
|
||||
assert.equal(runnerCapability.capabilityPass, false);
|
||||
assert.equal(runnerCapability.blocker, "controlled-readonly-not-long-lived");
|
||||
logOk("read-only runner pwd uses a reusable session but remains partial capability");
|
||||
);
|
||||
validateCodeAgentChatSchema(blocked);
|
||||
assert.equal(blocked.status, "failed");
|
||||
assert.equal(blocked.provider, "codex-stdio");
|
||||
assert.equal(blocked.backend, "hwlab-cloud-api/codex-mcp-stdio");
|
||||
assert.equal(blocked.error.code, "codex_cli_binary_missing");
|
||||
assert.equal(Object.hasOwn(blocked, "reply"), false);
|
||||
assert.deepEqual(blocked.toolCalls, []);
|
||||
assert.ok(blocked.runnerLimitations.includes("no-controlled-readonly-fallback"));
|
||||
assert.ok(blocked.runnerLimitations.includes("no-text-fallback"));
|
||||
assert.equal(blocked.availability.ready, false);
|
||||
assert.equal(blocked.availability.partialReady, false);
|
||||
assert.equal(blocked.runnerTrace.traceId, `trc_code-agent-chat-blocked-${index}`);
|
||||
assert.ok(runnerTraceLabels(blocked).includes("request:accepted"));
|
||||
assert.ok(runnerTraceLabels(blocked).includes("codex-stdio:blocked"));
|
||||
assert.equal(blocked.runnerTrace.lastEvent.errorCode, "codex_cli_binary_missing");
|
||||
assert.equal(blocked.runnerTrace.lastEvent.waitingFor, "codex-stdio-readiness");
|
||||
assert.equal(JSON.stringify(blocked).includes("sk-"), false);
|
||||
}
|
||||
logOk("skills/pwd/ls/rg/cat prompts fail with Codex stdio blocker instead of local fallback");
|
||||
|
||||
const stdioWorkspaceRoot = await mkdtemp(path.join(os.tmpdir(), `hwlab-code-agent-stdio-smoke-${process.pid}-`));
|
||||
const stdioWorkspace = path.join(stdioWorkspaceRoot, "workspace");
|
||||
@@ -243,6 +131,7 @@ async function runLocalContractSmoke() {
|
||||
let fakeCodex = null;
|
||||
try {
|
||||
fakeCodex = await createFakeCodexCommand();
|
||||
const rpcCalls = [];
|
||||
const stdioManager = createCodexStdioSessionManager({
|
||||
idFactory: () => "ses_code_agent_chat_smoke_stdio",
|
||||
createRpcClient: async () => ({
|
||||
@@ -253,6 +142,7 @@ async function runLocalContractSmoke() {
|
||||
return ["codex", "codex-reply"];
|
||||
},
|
||||
async callTool(name, toolArgs) {
|
||||
rpcCalls.push({ name, toolArgs });
|
||||
return {
|
||||
structuredContent: {
|
||||
threadId: "thread_code_agent_chat_smoke_stdio",
|
||||
@@ -281,12 +171,14 @@ async function runLocalContractSmoke() {
|
||||
{
|
||||
conversationId: "cnv_code-agent-chat-stdio",
|
||||
traceId: "trc_code-agent-chat-stdio-pwd",
|
||||
message: "请 pwd、ls .、列出可用 skills,并做一次 workspace 写入读取清理 smoke"
|
||||
message: "列出可以使用的skill"
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-22T00:02:10.000Z",
|
||||
codexStdioManager: stdioManager,
|
||||
env: stdioEnv
|
||||
env: stdioEnv,
|
||||
skillsDirs: [stdioSkillsDir],
|
||||
skillsDirsExact: true
|
||||
}
|
||||
);
|
||||
validateCodeAgentChatSchema(stdioPwd);
|
||||
@@ -313,20 +205,24 @@ async function runLocalContractSmoke() {
|
||||
assert.equal(stdioPwd.codexStdioFeasibility.runtimeContract.codexHome.status, "ready");
|
||||
assert.equal(stdioPwd.longLivedSessionGate.status, "pass");
|
||||
assert.equal(stdioPwd.longLivedSessionGate.pass, true);
|
||||
assert.equal(stdioPwd.providerTrace.sidecarOnly, true);
|
||||
assert.equal(stdioPwd.providerTrace.toolName, "workspace-sidecar");
|
||||
assert.ok(stdioPwd.toolCalls.some((call) => call.name === "pwd" && call.status === "completed"));
|
||||
assert.ok(stdioPwd.toolCalls.some((call) => call.name === "ls" && call.status === "completed"));
|
||||
assert.equal(stdioPwd.providerTrace.toolName, "codex");
|
||||
assert.equal(stdioPwd.providerTrace.threadId, "thread_code_agent_chat_smoke_stdio");
|
||||
assert.equal(Object.hasOwn(stdioPwd.providerTrace, "sidecarOnly"), false);
|
||||
assert.equal(rpcCalls[0].name, "codex");
|
||||
assert.match(rpcCalls[0].toolArgs.prompt, /列出可以使用的skill/u);
|
||||
assert.ok(stdioPwd.toolCalls.some((call) => call.name === "codex" && call.status === "completed"));
|
||||
assert.ok(stdioPwd.toolCalls.some((call) => call.name === "skills.discover" && call.status === "completed"));
|
||||
assert.ok(stdioPwd.toolCalls.some((call) => call.name === "workspace.smoke.write" && call.status === "completed"));
|
||||
assert.ok(stdioPwd.toolCalls.some((call) => call.name === "workspace.smoke.read" && call.status === "completed"));
|
||||
assert.ok(stdioPwd.toolCalls.some((call) => call.name === "workspace.smoke.cleanup" && call.status === "completed"));
|
||||
assert.equal(stdioPwd.skills.status, "ready");
|
||||
assert.ok(stdioPwd.skills.items.some((skill) => skill.name === "aaa-stdio-smoke-skill"));
|
||||
assert.equal(stdioPwd.runnerTrace.runnerKind, "codex-mcp-stdio-runner");
|
||||
assert.equal(stdioPwd.runnerTrace.sessionMode, "codex-mcp-stdio-long-lived");
|
||||
assert.ok(stdioPwd.runnerTrace.events.includes("tool:workspace.pwd:completed"));
|
||||
assert.ok(stdioPwd.runnerTrace.events.includes("tool:workspace.smoke.cleanup:completed"));
|
||||
assert.ok(runnerTraceLabels(stdioPwd).includes("session:created"));
|
||||
assert.ok(runnerTraceLabels(stdioPwd).includes("prompt:sent"));
|
||||
assert.ok(runnerTraceLabels(stdioPwd).includes("tool:codex:started"));
|
||||
assert.ok(runnerTraceLabels(stdioPwd).includes("tool:codex:output_chunk"));
|
||||
assert.ok(runnerTraceLabels(stdioPwd).includes("tool:codex:completed"));
|
||||
assert.ok(runnerTraceLabels(stdioPwd).includes("assistant:chunk"));
|
||||
assert.ok(runnerTraceLabels(stdioPwd).includes("assistant:completed"));
|
||||
assert.equal(classifyCodexRunnerCapability(stdioPwd, { httpStatus: 200 }).capabilityPass, true);
|
||||
|
||||
const stdioSecondTurn = await handleCodeAgentChat(
|
||||
@@ -338,7 +234,9 @@ async function runLocalContractSmoke() {
|
||||
{
|
||||
now: () => "2026-05-22T00:02:11.000Z",
|
||||
codexStdioManager: stdioManager,
|
||||
env: stdioEnv
|
||||
env: stdioEnv,
|
||||
skillsDirs: [stdioSkillsDir],
|
||||
skillsDirsExact: true
|
||||
}
|
||||
);
|
||||
validateCodeAgentChatSchema(stdioSecondTurn);
|
||||
@@ -346,12 +244,15 @@ async function runLocalContractSmoke() {
|
||||
assert.equal(stdioSecondTurn.provider, "codex-stdio");
|
||||
assert.equal(stdioSecondTurn.sessionId, stdioPwd.sessionId);
|
||||
assert.equal(stdioSecondTurn.session.sessionId, stdioPwd.session.sessionId);
|
||||
assert.equal(stdioSecondTurn.session.threadId, null);
|
||||
assert.equal(stdioSecondTurn.session.threadId, "thread_code_agent_chat_smoke_stdio");
|
||||
assert.equal(stdioSecondTurn.sessionReuse.reused, true);
|
||||
assert.equal(stdioSecondTurn.sessionReuse.turn, 2);
|
||||
assert.equal(stdioSecondTurn.providerTrace.sidecarOnly, true);
|
||||
assert.equal(stdioSecondTurn.providerTrace.toolName, "workspace-sidecar");
|
||||
assert.ok(stdioSecondTurn.toolCalls.some((call) => call.name === "ls" && call.status === "completed"));
|
||||
assert.equal(stdioSecondTurn.providerTrace.toolName, "codex-reply");
|
||||
assert.equal(Object.hasOwn(stdioSecondTurn.providerTrace, "sidecarOnly"), false);
|
||||
assert.ok(stdioSecondTurn.toolCalls.some((call) => call.name === "codex-reply" && call.status === "completed"));
|
||||
assert.ok(runnerTraceLabels(stdioSecondTurn).includes("session:reused"));
|
||||
assert.ok(runnerTraceLabels(stdioSecondTurn).includes("prompt:sent"));
|
||||
assert.ok(runnerTraceLabels(stdioSecondTurn).includes("tool:codex-reply:completed"));
|
||||
assert.equal(classifyCodexRunnerCapability(stdioSecondTurn, { httpStatus: 200 }).capabilityPass, true);
|
||||
assert.equal(JSON.stringify(stdioSecondTurn).includes("test-openai-key-material"), false);
|
||||
logOk("codex stdio runner passes long-lived workspace-write session capability");
|
||||
@@ -361,349 +262,30 @@ async function runLocalContractSmoke() {
|
||||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const runnerSecondTurn = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: "cnv_code-agent-chat-runner-pwd",
|
||||
traceId: "trc_code-agent-chat-runner-session-reuse",
|
||||
message: "ls ."
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-22T00:02:01.000Z",
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd()
|
||||
}
|
||||
const completedCodexPayload = {
|
||||
conversationId: "cnv_code-agent-chat-stdio-http",
|
||||
sessionId: "ses_code-agent-chat-stdio-http",
|
||||
messageId: "msg_code-agent-chat-stdio-http",
|
||||
status: "completed",
|
||||
createdAt: "2026-05-22T00:05:00.000Z",
|
||||
updatedAt: "2026-05-22T00:05:00.000Z",
|
||||
traceId: "trc_code-agent-chat-stdio-http",
|
||||
provider: "codex-stdio",
|
||||
model: "gpt-5.5",
|
||||
backend: "hwlab-cloud-api/codex-mcp-stdio",
|
||||
runner: { kind: "codex-mcp-stdio-runner", codexStdio: true, writeCapable: true, durableSession: true },
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
implementationType: "repo-owned-codex-mcp-stdio-session",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
reply: {
|
||||
messageId: "msg_code-agent-chat-stdio-http",
|
||||
role: "assistant",
|
||||
content: "real stdio reply",
|
||||
createdAt: "2026-05-22T00:05:00.000Z"
|
||||
}
|
||||
);
|
||||
validateCodeAgentChatSchema(runnerSecondTurn);
|
||||
assert.equal(runnerSecondTurn.status, "completed");
|
||||
assert.equal(runnerSecondTurn.sessionId, runnerPwd.sessionId);
|
||||
assert.equal(runnerSecondTurn.session.sessionId, runnerPwd.session.sessionId);
|
||||
assert.equal(runnerSecondTurn.sessionReuse.reused, true);
|
||||
assert.equal(runnerSecondTurn.sessionReuse.turn, 2);
|
||||
assert.equal(runnerSecondTurn.session.lastTraceId, "trc_code-agent-chat-runner-session-reuse");
|
||||
assert.equal(runnerSecondTurn.toolCalls[0].name, "ls");
|
||||
assert.equal(runnerSecondTurn.toolCalls[0].status, "completed");
|
||||
assert.match(runnerSecondTurn.reply.content, /package\.json/u);
|
||||
logOk("same conversation reuses controlled read-only session registry");
|
||||
|
||||
const fallbackCapability = classifyCodexRunnerCapability(completedLivePayload, { httpStatus: 200 });
|
||||
assert.equal(fallbackCapability.status, "blocked");
|
||||
assert.equal(fallbackCapability.blocker, "openai-fallback-not-runner");
|
||||
assert.equal(fallbackCapability.capabilityPass, false);
|
||||
logOk("OpenAI fallback does not satisfy Codex runner capability");
|
||||
|
||||
const missingSkills = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: "cnv_code-agent-chat-missing-skills",
|
||||
traceId: "trc_code-agent-chat-missing-skills",
|
||||
message: "列出你可用的skills"
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-22T00:03:00.000Z",
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
|
||||
HWLAB_CODE_AGENT_SKILLS_DIRS: "/tmp/hwlab-code-agent-smoke-missing-skills"
|
||||
},
|
||||
skillsDirs: ["/tmp/hwlab-code-agent-smoke-missing-skills"],
|
||||
skillsDirsExact: true
|
||||
}
|
||||
);
|
||||
validateCodeAgentChatSchema(missingSkills);
|
||||
assert.equal(missingSkills.status, "failed");
|
||||
assert.equal(missingSkills.error.code, "skills_unavailable");
|
||||
assert.equal(missingSkills.skills.status, "blocked");
|
||||
assert.equal(missingSkills.skills.blockers[0].sourceIssue, "pikasTech/HWLAB#136");
|
||||
assert.equal(classifyCodexRunnerCapability(missingSkills, { httpStatus: 200 }).capabilityPass, false);
|
||||
logOk("missing skills is structured blocker");
|
||||
|
||||
const boundedCat = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: "cnv_code-agent-chat-cat",
|
||||
traceId: "trc_code-agent-chat-cat",
|
||||
message: "请用cat读取 package.json"
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-22T00:04:00.000Z",
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd()
|
||||
}
|
||||
}
|
||||
);
|
||||
validateCodeAgentChatSchema(boundedCat);
|
||||
assert.equal(boundedCat.status, "completed");
|
||||
assert.equal(boundedCat.toolCalls[0].name, "cat");
|
||||
assert.equal(boundedCat.toolCalls[0].status, "completed");
|
||||
assert.ok(boundedCat.toolCalls[0].stdout.length <= 4100);
|
||||
assert.match(boundedCat.reply.content, /"name": "hwlab"/u);
|
||||
logOk("bounded cat read-only tool capability");
|
||||
|
||||
const boundedRgFiles = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: "cnv_code-agent-chat-rg-files",
|
||||
traceId: "trc_code-agent-chat-rg-files",
|
||||
message: "rg --files ."
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-22T00:04:30.000Z",
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd()
|
||||
}
|
||||
}
|
||||
);
|
||||
validateCodeAgentChatSchema(boundedRgFiles);
|
||||
assert.equal(boundedRgFiles.status, "completed");
|
||||
assert.equal(boundedRgFiles.toolCalls[0].name, "rg --files");
|
||||
assert.equal(boundedRgFiles.toolCalls[0].status, "completed");
|
||||
assert.ok(boundedRgFiles.toolCalls[0].stdout.length <= 4100);
|
||||
assert.match(boundedRgFiles.reply.content, /受控只读 rg --files 结果/u);
|
||||
logOk("bounded rg --files read-only tool capability");
|
||||
|
||||
const toolUnavailable = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: "cnv_code-agent-chat-tool-unavailable",
|
||||
traceId: "trc_code-agent-chat-tool-unavailable",
|
||||
message: "请用grep搜索 package.json"
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-22T00:04:40.000Z",
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd()
|
||||
}
|
||||
}
|
||||
);
|
||||
validateCodeAgentChatSchema(toolUnavailable);
|
||||
assert.equal(toolUnavailable.status, "failed");
|
||||
assert.equal(toolUnavailable.error.code, "tool_unavailable");
|
||||
assert.equal(toolUnavailable.toolCalls[0].status, "blocked");
|
||||
assert.equal(classifyCodexRunnerCapability(toolUnavailable, { httpStatus: 200 }).capabilityPass, false);
|
||||
logOk("tool unavailable is blocked, not completed runner capability");
|
||||
|
||||
const securityBlocked = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: "cnv_code-agent-chat-security-blocked",
|
||||
traceId: "trc_code-agent-chat-security-blocked",
|
||||
message: "请cat ../secret"
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-22T00:04:50.000Z",
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd()
|
||||
}
|
||||
}
|
||||
);
|
||||
validateCodeAgentChatSchema(securityBlocked);
|
||||
assert.equal(securityBlocked.status, "failed");
|
||||
assert.equal(securityBlocked.error.code, "security_blocked");
|
||||
assert.equal(securityBlocked.toolCalls[0].status, "blocked");
|
||||
assert.equal(classifyCodexRunnerCapability(securityBlocked, { httpStatus: 200 }).capabilityPass, false);
|
||||
logOk("security blocked path is structured");
|
||||
|
||||
const m3SkillCalls = [];
|
||||
const m3SkillWrite = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: "cnv_code-agent-chat-m3-skill",
|
||||
traceId: "trc_code-agent-chat-m3-skill",
|
||||
message: "通过 HWLAB API 把 res_boxsimu_1 的 DO1 写成 true,然后读取 res_boxsimu_2 的 DI1。"
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-22T00:04:55.000Z",
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
|
||||
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||||
OPENAI_API_KEY: "must-not-be-used"
|
||||
},
|
||||
callProvider: async () => {
|
||||
throw new Error("OpenAI fallback must not be used for M3 IO");
|
||||
},
|
||||
m3IoSkillRequestJson: async (url, request) => {
|
||||
m3SkillCalls.push({ url, request });
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: {
|
||||
status: "succeeded",
|
||||
accepted: true,
|
||||
action: "do.write",
|
||||
traceId: "trc_code-agent-chat-m3-skill",
|
||||
operationId: "op_m3_do_write_smoke",
|
||||
auditId: "aud_m3_do_write_smoke_succeeded",
|
||||
evidenceId: "evd_m3_do_write_smoke_succeeded",
|
||||
auditState: {
|
||||
status: "written_non_durable"
|
||||
},
|
||||
evidenceState: {
|
||||
status: "blocked",
|
||||
sourceKind: "BLOCKED",
|
||||
blocker: "runtime_durable_not_green",
|
||||
writeStatus: "written_non_durable"
|
||||
},
|
||||
durableStatus: {
|
||||
status: "degraded",
|
||||
durable: false,
|
||||
blocker: "runtime_durable_not_green"
|
||||
},
|
||||
result: {
|
||||
value: true,
|
||||
targetReadback: {
|
||||
status: "succeeded",
|
||||
value: true
|
||||
}
|
||||
},
|
||||
controlPath: {
|
||||
cloudApi: true,
|
||||
gatewaySimu: true,
|
||||
boxSimu: true,
|
||||
patchPanel: true,
|
||||
frontendBypass: false
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
validateCodeAgentChatSchema(m3SkillWrite);
|
||||
assert.equal(m3SkillWrite.status, "completed");
|
||||
assert.equal(m3SkillWrite.provider, "hwlab-skill-cli");
|
||||
assert.equal(m3SkillWrite.toolCalls[0].name, "hwlab-agent-runtime.m3-io");
|
||||
assert.equal(m3SkillWrite.toolCalls[0].route, "/v1/m3/io");
|
||||
assert.equal(m3SkillWrite.toolCalls[0].method, "POST");
|
||||
assert.equal(m3SkillWrite.toolCalls[0].operationId, "op_m3_do_write_smoke");
|
||||
assert.equal(m3SkillWrite.toolCalls[0].auditId, "aud_m3_do_write_smoke_succeeded");
|
||||
assert.equal(m3SkillWrite.toolCalls[0].evidenceId, "evd_m3_do_write_smoke_succeeded");
|
||||
assert.equal(m3SkillWrite.toolCalls[0].readback.value, true);
|
||||
assert.equal(m3SkillWrite.providerTrace.fallbackUsed, false);
|
||||
assert.equal(m3SkillWrite.providerTrace.method, "POST");
|
||||
assert.equal(m3SkillWrite.providerTrace.readback.value, true);
|
||||
assert.equal(m3SkillWrite.runner.safety.directGatewayCallsAllowed, false);
|
||||
assert.match(m3SkillWrite.reply.content, /HWLAB API \/v1\/m3\/io/u);
|
||||
assert.match(m3SkillWrite.reply.content, /res_boxsimu_1:DO1=true/u);
|
||||
assert.match(m3SkillWrite.reply.content, /res_boxsimu_2:DI1=true/u);
|
||||
assert.equal(m3SkillCalls.length, 1);
|
||||
assert.equal(m3SkillCalls[0].url, "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667/v1/m3/io");
|
||||
assert.equal(m3SkillCalls[0].request.body.action, "do.write");
|
||||
assert.equal(m3SkillCalls[0].request.body.value, true);
|
||||
assert.equal(m3SkillCalls[0].url.includes("gateway-simu"), false);
|
||||
assert.equal(m3SkillCalls[0].url.includes("box-simu"), false);
|
||||
assert.equal(m3SkillCalls[0].url.includes("patch-panel"), false);
|
||||
logOk("M3 Skill CLI routes through HWLAB API only");
|
||||
|
||||
const m3SkillFalseCalls = [];
|
||||
const m3SkillWriteFalse = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: "cnv_code-agent-chat-m3-skill-false",
|
||||
traceId: "trc_code-agent-chat-m3-skill-false",
|
||||
message: "通过 HWLAB API 把 res_boxsimu_1 的 DO1 写成 false,然后读取 res_boxsimu_2 的 DI1。"
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-22T00:04:56.000Z",
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
|
||||
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||||
OPENAI_API_KEY: "must-not-be-used"
|
||||
},
|
||||
callProvider: async () => {
|
||||
throw new Error("OpenAI fallback must not be used for M3 IO");
|
||||
},
|
||||
m3IoSkillRequestJson: async (url, request) => {
|
||||
m3SkillFalseCalls.push({ url, request });
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: {
|
||||
status: "succeeded",
|
||||
accepted: true,
|
||||
action: "do.write",
|
||||
traceId: "trc_code-agent-chat-m3-skill-false",
|
||||
operationId: "op_m3_do_write_false_smoke",
|
||||
auditId: "aud_m3_do_write_false_smoke_succeeded",
|
||||
evidenceId: "evd_m3_do_write_false_smoke_succeeded",
|
||||
auditState: {
|
||||
status: "written_non_durable"
|
||||
},
|
||||
evidenceState: {
|
||||
status: "blocked",
|
||||
sourceKind: "BLOCKED",
|
||||
blocker: "runtime_durable_not_green",
|
||||
writeStatus: "written_non_durable"
|
||||
},
|
||||
durableStatus: {
|
||||
status: "degraded",
|
||||
durable: false,
|
||||
blocker: "runtime_durable_not_green"
|
||||
},
|
||||
result: {
|
||||
value: false,
|
||||
targetReadback: {
|
||||
status: "succeeded",
|
||||
value: false
|
||||
}
|
||||
},
|
||||
controlPath: {
|
||||
cloudApi: true,
|
||||
gatewaySimu: true,
|
||||
boxSimu: true,
|
||||
patchPanel: true,
|
||||
frontendBypass: false
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
validateCodeAgentChatSchema(m3SkillWriteFalse);
|
||||
assert.equal(m3SkillWriteFalse.status, "completed");
|
||||
assert.equal(m3SkillWriteFalse.provider, "hwlab-skill-cli");
|
||||
assert.equal(m3SkillWriteFalse.toolCalls[0].route, "/v1/m3/io");
|
||||
assert.equal(m3SkillWriteFalse.toolCalls[0].method, "POST");
|
||||
assert.equal(m3SkillWriteFalse.toolCalls[0].operationId, "op_m3_do_write_false_smoke");
|
||||
assert.equal(m3SkillWriteFalse.toolCalls[0].readback.value, false);
|
||||
assert.equal(m3SkillWriteFalse.providerTrace.fallbackUsed, false);
|
||||
assert.match(m3SkillWriteFalse.reply.content, /HWLAB API \/v1\/m3\/io/u);
|
||||
assert.match(m3SkillWriteFalse.reply.content, /res_boxsimu_1:DO1=false/u);
|
||||
assert.match(m3SkillWriteFalse.reply.content, /res_boxsimu_2:DI1=false/u);
|
||||
assert.equal(m3SkillFalseCalls.length, 1);
|
||||
assert.equal(m3SkillFalseCalls[0].url, "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667/v1/m3/io");
|
||||
assert.equal(m3SkillFalseCalls[0].request.body.action, "do.write");
|
||||
assert.equal(m3SkillFalseCalls[0].request.body.value, false);
|
||||
logOk("M3 Skill CLI handles user-direct false request through HWLAB API only");
|
||||
|
||||
const directToolCallPayload = JSON.parse(JSON.stringify(m3SkillWrite));
|
||||
directToolCallPayload.toolCalls[0].hwlabApi = {
|
||||
route: "/v1/m3/io",
|
||||
redactedUrl: "http://hwlab-gateway-simu-1.hwlab-dev.svc.cluster.local:7101/v1/m3/io"
|
||||
};
|
||||
assert.throws(
|
||||
() => validateCodeAgentChatSchema(directToolCallPayload),
|
||||
/direct gateway\/box\/patch-panel target/u
|
||||
);
|
||||
|
||||
const missingRouteToolCallPayload = JSON.parse(JSON.stringify(m3SkillWrite));
|
||||
delete missingRouteToolCallPayload.toolCalls[0].route;
|
||||
missingRouteToolCallPayload.toolCalls[0].blocker = null;
|
||||
missingRouteToolCallPayload.toolCalls[0].status = "completed";
|
||||
assert.throws(
|
||||
() => validateCodeAgentChatSchema(missingRouteToolCallPayload),
|
||||
/must show route \/v1\/m3\/io/u
|
||||
);
|
||||
logOk("M3 toolCall schema rejects direct hardware URLs and missing route evidence");
|
||||
|
||||
const completedHttp200Readiness = classifyCodeAgentChatReadiness(completedLivePayload, { realDevLive: true, httpStatus: 200 });
|
||||
assert.equal(completedHttp200Readiness.status, "blocked");
|
||||
assert.equal(completedHttp200Readiness.devLiveReplyPass, false);
|
||||
assert.equal(completedHttp200Readiness.blocker, "untrusted-completion");
|
||||
logOk("HTTP 200 OpenAI fallback completion is not full Code Agent pass");
|
||||
|
||||
for (const status of [400, 401, 403, 429, 500, 502, 503, 504]) {
|
||||
const readiness = classifyCodeAgentChatReadiness(completedLivePayload, { realDevLive: true, httpStatus: status });
|
||||
const readiness = classifyCodeAgentChatReadiness(completedCodexPayload, { realDevLive: true, httpStatus: status });
|
||||
assert.equal(readiness.status, "blocked", `HTTP ${status} must block`);
|
||||
assert.equal(readiness.blocker, "provider-upstream", `HTTP ${status} blocker`);
|
||||
assert.equal(readiness.providerStatus, status, `HTTP ${status} providerStatus`);
|
||||
|
||||
@@ -281,22 +281,9 @@ const workbenchMarkers = Object.freeze([
|
||||
]);
|
||||
|
||||
const requiredCodeAgentEvidenceTerms = Object.freeze([
|
||||
"消息归因",
|
||||
"字段缺失:证据不足",
|
||||
"只是文本回答",
|
||||
"当前不能执行",
|
||||
"provider",
|
||||
"model",
|
||||
"backend",
|
||||
"runner",
|
||||
"workspace",
|
||||
"sandbox",
|
||||
"capability",
|
||||
"sessionMode",
|
||||
"sessionReuse",
|
||||
"implementation",
|
||||
"limitations",
|
||||
"codexStdio",
|
||||
"运行 trace",
|
||||
"message-trace",
|
||||
"message-trace-events",
|
||||
"toolCalls",
|
||||
"skills",
|
||||
"runnerTrace",
|
||||
@@ -304,15 +291,9 @@ const requiredCodeAgentEvidenceTerms = Object.freeze([
|
||||
"trace",
|
||||
"message",
|
||||
"providerTrace",
|
||||
"openai-responses",
|
||||
"openai-responses-fallback",
|
||||
"text-chat-only / 只是文本回答",
|
||||
"codex-stdio",
|
||||
"codex-cli",
|
||||
"codex-mcp-stdio-long-lived",
|
||||
"repo-owned-codex-mcp-stdio-session",
|
||||
"controlled-readonly-session-registry",
|
||||
"read-only-session-tools:只读长会话",
|
||||
"not-codex-stdio",
|
||||
"not-write-capable",
|
||||
"process-local-session-registry",
|
||||
@@ -320,8 +301,6 @@ const requiredCodeAgentEvidenceTerms = Object.freeze([
|
||||
"untrusted_completion",
|
||||
"SOURCE fixture",
|
||||
"Code Agent SOURCE 回复",
|
||||
"message-evidence-chip",
|
||||
"message-attribution-chip",
|
||||
"hwlab-skill-cli",
|
||||
"hwlab-m3-io-skill-cli",
|
||||
"message-m3-evidence"
|
||||
@@ -635,22 +614,22 @@ function runStaticSmoke() {
|
||||
|
||||
addCheck(checks, blockers, "route-control-stable-dimensions", hasStableRouteControls(files), "Route controls and status tags have stable dimensions, readable wrapping, and usable labels on small screens.", {
|
||||
blocker: "runtime_blocker",
|
||||
evidence: ["rail-button min-width/min-height", "state-tag wrapping", "side-tab stable labels", "mobile message evidence column"]
|
||||
evidence: ["rail-button min-width/min-height", "state-tag wrapping", "side-tab stable labels", "mobile trace column"]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "code-agent-completed-evidence-visible", hasCodeAgentCompletedEvidenceVisibility(files), "Completed full Code Agent replies expose Codex stdio provider/model/trace/conversation/session evidence and reject echo/mock/stub, OpenAI fallback, and read-only runner completions.", {
|
||||
addCheck(checks, blockers, "code-agent-completed-evidence-visible", hasCodeAgentCompletedEvidenceVisibility(files), "Completed full Code Agent replies expose Codex stdio runnerTrace and reject echo/mock/stub, OpenAI fallback, and local shortcut completions.", {
|
||||
blocker: "observability_blocker",
|
||||
evidence: requiredCodeAgentEvidenceTerms
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "code-agent-conversation-ux-states", hasCodeAgentConversationUxStates(files), "Code Agent thread renders trusted completed, SOURCE fixture, failed, and blocked provider states as distinct conversation replies with bounded evidence.", {
|
||||
addCheck(checks, blockers, "code-agent-conversation-ux-states", hasCodeAgentConversationUxStates(files), "Code Agent thread renders trusted completed, SOURCE fixture, failed, and blocked provider states as distinct conversation replies with bounded trace.", {
|
||||
blocker: "observability_blocker",
|
||||
evidence: [
|
||||
"DEV-LIVE 回复",
|
||||
"SOURCE 回复",
|
||||
"等待超时/API 错误/发送失败",
|
||||
"服务受阻 or legacy BLOCKED 凭证缺口",
|
||||
"message-evidence/details/chips"
|
||||
"message-trace/details/events"
|
||||
]
|
||||
});
|
||||
|
||||
@@ -2347,7 +2326,7 @@ function hasStableRouteControls({ html, styles }) {
|
||||
/\.(?:status-dot|state-tag|badge)[^{]*\{[^}]*max-width:\s*100%;[^}]*line-height:\s*1\.25;[^}]*white-space:\s*normal;[^}]*overflow-wrap:\s*anywhere;/su.test(styles) &&
|
||||
/\.probe-card\s*\{[^}]*min-width:\s*0;/su.test(styles) &&
|
||||
/\.probe-card strong\s*\{[^}]*line-height:\s*1\.2;[^}]*overflow-wrap:\s*anywhere;/su.test(styles) &&
|
||||
/@media\s*\(max-width:\s*860px\)[\s\S]*?\.message-evidence\s*\{[\s\S]*?grid-column:\s*1;/u.test(styles)
|
||||
/@media\s*\(max-width:\s*860px\)[\s\S]*?\.message-trace,\s*\n\s*\.message-m3-evidence\s*\{[\s\S]*?grid-column:\s*1;/u.test(styles)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2489,7 +2468,6 @@ function hasCodeAgentReadinessVisibility({ html, app, liveStatus = "" }) {
|
||||
}
|
||||
|
||||
function hasCodeAgentCompletedEvidenceVisibility({ app, codeAgentM3Evidence }) {
|
||||
const messageEvidenceBody = functionBody(app, "messageEvidenceFields");
|
||||
const realEvidenceBody = functionBody(app, "hasRealCodeAgentEvidence");
|
||||
const runnerEvidenceBody = functionBody(app, "isCodexRunnerCapableEvidence");
|
||||
const codeAgentEvidenceSource = `${app}\n${codeAgentM3Evidence}`;
|
||||
@@ -2497,34 +2475,19 @@ function hasCodeAgentCompletedEvidenceVisibility({ app, codeAgentM3Evidence }) {
|
||||
requiredCodeAgentEvidenceTerms.every((term) => codeAgentEvidenceSource.includes(term)) &&
|
||||
/conversationId:\s*result\.conversationId \|\| result\.sessionId \|\| state\.conversationId/u.test(app) &&
|
||||
/providerTrace:\s*result\.providerTrace/u.test(app) &&
|
||||
/function\s+providerTraceSummary\s*\(/u.test(app) &&
|
||||
/function\s+messageAttributionPanel\s*\(/u.test(app) &&
|
||||
/codeAgentAttributionFromMessage/u.test(app) &&
|
||||
/attribution\.fields\.map\(\(field\) => field\.chip\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("provider",\s*message\.provider\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("model",\s*message\.model\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("backend",\s*message\.backend\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("runner",\s*message\.runner\?\.kind\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("workspace",\s*message\.workspace\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("sandbox",\s*message\.sandbox\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("capability",\s*message\.capabilityLevel\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("session",\s*sessionSummary\(message\.session\)\s*\?\?\s*message\.sessionId\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("sessionMode",\s*message\.sessionMode\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("sessionReuse",\s*sessionReuseSummary\(message\.sessionReuse\)\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("longLivedGate",\s*longLivedSessionGateSummary\(message\.longLivedSessionGate\)\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("implementation",\s*message\.implementationType\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("limitations",\s*limitationsSummary\(message\.runnerLimitations\)\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("codexStdio",\s*codexStdioFeasibilitySummary\(message\.codexStdioFeasibility\)\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("toolCalls",\s*toolCallsSummary\(message\.toolCalls\)\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("skills",\s*skillsSummary\(message\.skills\)\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("runnerTrace",\s*runnerTraceSummary\(message\.runnerTrace\)\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("operationId",\s*operationIdSummary\(message\)\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("audit",\s*auditSummary\(message\)\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("evidence",\s*evidenceSummary\(message\)\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("conversation",\s*message\.conversationId\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("trace",\s*message\.traceId\)/u.test(messageEvidenceBody) &&
|
||||
/recordField\("message",\s*message\.messageId\)/u.test(messageEvidenceBody) &&
|
||||
/providerTraceSummary\(message\.providerTrace\)/u.test(messageEvidenceBody) &&
|
||||
/function\s+messageTracePanel\s*\(/u.test(app) &&
|
||||
/function\s+subscribeRunnerTrace\s*\(/u.test(app) &&
|
||||
/new EventSource\(`\/v1\/agent\/chat\/trace\/\$\{encodeURIComponent\(traceId\)\}\/stream`\)/u.test(app) &&
|
||||
/source\.addEventListener\("runnerTrace"/u.test(app) &&
|
||||
/function\s+pollRunnerTrace\s*\(/u.test(app) &&
|
||||
/function\s+updateMessageTrace\s*\(/u.test(app) &&
|
||||
/function\s+runnerTraceFromSnapshot\s*\(/u.test(app) &&
|
||||
/function\s+runnerTraceHeadline\s*\(/u.test(app) &&
|
||||
/function\s+traceEventMeta\s*\(/u.test(app) &&
|
||||
!/function\s+messageAttributionPanel\s*\(/u.test(app) &&
|
||||
!/function\s+messageEvidencePanel\s*\(/u.test(app) &&
|
||||
!/codeAgentAttributionFromMessage/u.test(app) &&
|
||||
!/codeAgentFactsFromMessage/u.test(app) &&
|
||||
/extractCodeAgentM3Evidence\(result\)/u.test(app) &&
|
||||
/messageM3EvidencePanel/u.test(app) &&
|
||||
/m3EvidenceRows/u.test(app) &&
|
||||
@@ -2540,11 +2503,9 @@ function hasCodeAgentCompletedEvidenceVisibility({ app, codeAgentM3Evidence }) {
|
||||
/isTrustedCodeAgentProvider\(value\?\.provider\)/u.test(realEvidenceBody) &&
|
||||
/isCodexRunnerCapableEvidence\(value\)/u.test(realEvidenceBody) &&
|
||||
/CODEX_RUNNER_CAPABLE_PROVIDERS/u.test(app) &&
|
||||
/CODEX_READONLY_PARTIAL_PROVIDERS/u.test(app) &&
|
||||
/codex-mcp-stdio-long-lived/u.test(runnerEvidenceBody) &&
|
||||
/repo-owned-codex-mcp-stdio-session/u.test(runnerEvidenceBody) &&
|
||||
/long-lived-codex-stdio-session/u.test(runnerEvidenceBody) &&
|
||||
/isReadOnlyRunnerPartialEvidence/u.test(app) &&
|
||||
/isTextFallbackChatResult/u.test(app) &&
|
||||
/value\?\.session\?\.status === "idle"/u.test(runnerEvidenceBody) &&
|
||||
/value\?\.longLivedSessionGate\?\.status === "pass"/u.test(runnerEvidenceBody) &&
|
||||
@@ -2614,14 +2575,13 @@ function hasCodeAgentConversationUxStates({ app, styles }) {
|
||||
/function\s+isSourceFixtureChatResult\s*\(/u.test(app) &&
|
||||
/function\s+isSourceFixtureCompletion\s*\(/u.test(app) &&
|
||||
/function\s+isSourceFixtureCompletedChatMessage\s*\(/u.test(app) &&
|
||||
/function\s+messageEvidencePanel\s*\(/u.test(app) &&
|
||||
/function\s+messageAttributionPanel\s*\(/u.test(app) &&
|
||||
/function\s+messageTracePanel\s*\(/u.test(app) &&
|
||||
/function\s+subscribeRunnerTrace\s*\(/u.test(app) &&
|
||||
/function\s+updateMessageTrace\s*\(/u.test(app) &&
|
||||
/function\s+messagePendingContextPanel\s*\(/u.test(app) &&
|
||||
/function\s+messageEvidenceSummary\s*\(/u.test(app) &&
|
||||
/function\s+boundedEvidenceField\s*\(/u.test(app) &&
|
||||
/codeAgentAttributionFromMessage/u.test(app) &&
|
||||
/字段缺失:证据不足/u.test(app) &&
|
||||
/当前不能执行/u.test(app) &&
|
||||
!/function\s+messageEvidencePanel\s*\(/u.test(app) &&
|
||||
!/function\s+messageAttributionPanel\s*\(/u.test(app) &&
|
||||
!/codeAgentAttributionFromMessage/u.test(app) &&
|
||||
/DEFAULT_API_TIMEOUT_MS\s*=\s*4500/u.test(app) &&
|
||||
/DEFAULT_CODE_AGENT_TIMEOUT_MS\s*=\s*180000/u.test(app) &&
|
||||
/timeoutMs:\s*CODE_AGENT_TIMEOUT_MS/u.test(app) &&
|
||||
@@ -2640,12 +2600,12 @@ function hasCodeAgentConversationUxStates({ app, styles }) {
|
||||
/message-agent/u.test(styles) &&
|
||||
/message-pending-context\s*\{/u.test(styles) &&
|
||||
/message-m3-evidence\s*\{/u.test(styles) &&
|
||||
/message-attribution\s*\{/u.test(styles) &&
|
||||
/message-attribution-chip/u.test(styles) &&
|
||||
/message-m3-rows\s*\{/u.test(styles) &&
|
||||
/copy-chip\s*\{/u.test(styles) &&
|
||||
/message-evidence\s*\{/u.test(styles) &&
|
||||
/message-evidence-chip/u.test(styles) &&
|
||||
/message-trace\s*\{/u.test(styles) &&
|
||||
/message-trace-events\s*\{/u.test(styles) &&
|
||||
!/message-attribution\s*\{/u.test(styles) &&
|
||||
!/message-evidence\s*\{/u.test(styles) &&
|
||||
/width:\s*min\(100%,\s*780px\)/u.test(styles) &&
|
||||
/status === "completed" \? "dev-live" : status === "failed" \|\| status === "blocked" \? "blocked" : status === "running" \? "pending" : "source"/u.test(app) &&
|
||||
!/sourceKind:\s*"SOURCE"[\s\S]{0,160}status:\s*"completed"/u.test(app)
|
||||
@@ -4237,29 +4197,27 @@ async function inspectJourneyUi(page) {
|
||||
messageCount: document.querySelectorAll(".message-card").length,
|
||||
completedMessageHasNonSensitiveMeta: Boolean(
|
||||
[...document.querySelectorAll(".message-card.status-completed .message-meta")]
|
||||
.some((element) => /openai-responses|codex-readonly-runner|gpt-5\.5|runner=|workspace=|toolCalls=|trc_/u.test(element.textContent ?? ""))
|
||||
.some((element) => /codex-stdio|gpt-5\.5|runner=|workspace=|toolCalls=|trc_|trace\s+trc_/u.test(element.textContent ?? ""))
|
||||
),
|
||||
sourceMessageHasNonSensitiveMeta: Boolean(
|
||||
[...document.querySelectorAll(".message-card.status-source .message-meta")]
|
||||
.some((element) => /SOURCE|source-fixture|trc_/u.test(element.textContent ?? ""))
|
||||
),
|
||||
attributionText: [...document.querySelectorAll(".message-card.message-agent .message-attribution")]
|
||||
traceText: [...document.querySelectorAll(".message-card.message-agent .message-trace")]
|
||||
.map((element) => element.textContent ?? "")
|
||||
.join("\n"),
|
||||
attributionHasTraceId: [...document.querySelectorAll(".message-card.message-agent .message-attribution [data-field='traceId']")]
|
||||
traceHasTraceId: [...document.querySelectorAll(".message-card.message-agent .message-trace")]
|
||||
.some((element) => /trc_/u.test(element.textContent ?? "")),
|
||||
attributionShowsToolCalls: [...document.querySelectorAll(".message-card.message-agent .message-attribution [data-field='toolCalls']")]
|
||||
.some((element) => /toolCalls=/u.test(element.textContent ?? "")),
|
||||
attributionMissingFieldsStructured: [...document.querySelectorAll(".message-card.message-agent .message-attribution-chip.is-missing")]
|
||||
.some((element) => /字段缺失:证据不足/u.test(element.textContent ?? "")),
|
||||
traceShowsEvents: [...document.querySelectorAll(".message-card.message-agent .message-trace-events li")]
|
||||
.some((element) => /#\d+|session|prompt|tool|assistant|timeout|blocked/u.test(element.textContent ?? "")),
|
||||
noMainAttributionNoise: document.querySelectorAll(".message-card.message-agent .message-attribution, .message-card.message-agent .message-evidence, .message-card.message-agent .code-agent-facts").length === 0,
|
||||
attributionFallbackNotRunnerControl: !/openai-responses-fallback[\s\S]{0,80}(真实 runner|Codex stdio 长会话|workspace-write)/u.test(
|
||||
[...document.querySelectorAll(".message-card.message-agent .message-attribution")]
|
||||
[...document.querySelectorAll(".message-card.message-agent .message-trace")]
|
||||
.map((element) => element.textContent ?? "")
|
||||
.join("\n")
|
||||
),
|
||||
conversationFactsVisible: /conversationFacts|fact traces|fact skills|fact tools|turns=|sessionId=|workspace=/u.test(
|
||||
[
|
||||
...document.querySelectorAll(".message-card.message-agent .code-agent-facts"),
|
||||
document.querySelector("#code-agent-summary")
|
||||
]
|
||||
.map((element) => element?.textContent ?? "")
|
||||
@@ -4703,9 +4661,9 @@ async function runLocalAgentFixtureSmoke({
|
||||
ui.agentChatStatus === "SOURCE 回复" &&
|
||||
ui.sourceMessageVisible &&
|
||||
ui.sourceMessageHasNonSensitiveMeta &&
|
||||
ui.attributionHasTraceId &&
|
||||
ui.attributionShowsToolCalls &&
|
||||
ui.attributionMissingFieldsStructured &&
|
||||
ui.traceHasTraceId &&
|
||||
ui.traceShowsEvents &&
|
||||
ui.noMainAttributionNoise &&
|
||||
ui.attributionFallbackNotRunnerControl &&
|
||||
ui.conversationFactsVisible &&
|
||||
mobilePending?.pass === true &&
|
||||
@@ -6102,21 +6060,21 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {}
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
traceId,
|
||||
provider: "codex-readonly-runner",
|
||||
model: "read-only-tools",
|
||||
backend: "local-source-fixture/slow-structured-blocker",
|
||||
provider: "codex-stdio",
|
||||
model: "codex-stdio",
|
||||
backend: "local-source-fixture/codex-stdio-slow-structured-blocker",
|
||||
projectId: stringOrFallback(body?.projectId, gateSummary.topology.projectId),
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "read-only",
|
||||
runner: {
|
||||
kind: "hwlab-readonly-runner",
|
||||
writeCapable: false,
|
||||
codexStdio: false,
|
||||
kind: "codex-mcp-stdio-runner",
|
||||
writeCapable: true,
|
||||
codexStdio: true,
|
||||
longLivedSession: true,
|
||||
durableSession: false
|
||||
durableSession: true
|
||||
},
|
||||
capabilityLevel: "blocked",
|
||||
sessionMode: "controlled-readonly-session-registry",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
session: {
|
||||
sessionId: conversationId,
|
||||
conversationId,
|
||||
@@ -6191,16 +6149,21 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {}
|
||||
},
|
||||
runnerTrace: {
|
||||
traceId,
|
||||
runnerKind: "hwlab-readonly-runner",
|
||||
sessionMode: "controlled-readonly-session-registry",
|
||||
runnerKind: "codex-mcp-stdio-runner",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
status: "blocked",
|
||||
events: ["tool:skills.discover:blocked"]
|
||||
events: [
|
||||
{ seq: 1, traceId, type: "session", stage: "created", status: "completed", label: "session:created" },
|
||||
{ seq: 2, traceId, type: "prompt", stage: "sent", status: "completed", label: "prompt:sent" },
|
||||
{ seq: 3, traceId, type: "error", stage: "blocked", status: "blocked", label: "codex-stdio:blocked", errorCode: "skills_unavailable", waitingFor: "codex-stdio" }
|
||||
],
|
||||
lastEvent: { seq: 3, traceId, type: "error", stage: "blocked", status: "blocked", label: "codex-stdio:blocked", errorCode: "skills_unavailable", waitingFor: "codex-stdio" }
|
||||
},
|
||||
conversationFacts: {
|
||||
conversationId,
|
||||
sessionId: conversationId,
|
||||
sessionStatus: "idle",
|
||||
sessionMode: "controlled-readonly-session-registry",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
turnCount: 1,
|
||||
latestTraceId: traceId,
|
||||
traceIds: [traceId],
|
||||
@@ -6270,7 +6233,11 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {}
|
||||
runnerTrace: {
|
||||
traceId,
|
||||
runnerKind: "openai-responses-fallback",
|
||||
events: ["fixture:text-chat-only"]
|
||||
events: [
|
||||
{ seq: 1, traceId, type: "session", stage: "fixture", status: "completed", label: "source-fixture:session" },
|
||||
{ seq: 2, traceId, type: "assistant", stage: "completed", status: "completed", label: "assistant:completed" }
|
||||
],
|
||||
lastEvent: { seq: 2, traceId, type: "assistant", stage: "completed", status: "completed", label: "assistant:completed" }
|
||||
},
|
||||
conversationFacts: {
|
||||
conversationId,
|
||||
|
||||
Reference in New Issue
Block a user