Files
pikasTech-HWLAB/scripts/src/rpt004-mvp-e2e-harness.test.mjs
T

585 lines
17 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import {
buildRpt004Report,
classifyCodeAgentSessionTurns,
classifyCodeAgentSkillPath,
classifySkillCliM3Route,
classifyM3DurableEvidence,
classifyRpt004Conclusion,
parseArgs,
validateLiveBoundary
} from "./rpt004-mvp-e2e-harness.mjs";
const fixtureCommit = "667349554b1f";
test("RPT-004 CLI accepts the required live URL/API URL shape", () => {
const args = parseArgs([
"--live",
"--url",
"http://74.48.78.17:16666/",
"--api-url",
"http://74.48.78.17:16667/"
]);
assert.equal(args.live, true);
assert.equal(args.url, "http://74.48.78.17:16666/");
assert.equal(args.apiUrl, "http://74.48.78.17:16667/");
assert.equal(args.writeReport, true);
assert.equal(args.allowM3Write, false);
assert.doesNotThrow(() => validateLiveBoundary(args));
});
test("RPT-004 CLI requires explicit flag before live M3 writes", () => {
const defaultLive = parseArgs([
"--live",
"--url",
"http://74.48.78.17:16666/",
"--api-url",
"http://74.48.78.17:16667/"
]);
const writeLive = parseArgs([
"--live",
"--allow-m3-write",
"--url",
"http://74.48.78.17:16666/",
"--api-url",
"http://74.48.78.17:16667/"
]);
assert.equal(defaultLive.allowM3Write, false);
assert.equal(writeLive.allowM3Write, true);
});
test("RPT-004 live boundary rejects deprecated or swapped active DEV ports", () => {
assert.throws(
() => validateLiveBoundary({ url: "http://74.48.78.17:6666/", apiUrl: "http://74.48.78.17:16667/" }),
/16666/u
);
assert.throws(
() => validateLiveBoundary({ url: "http://74.48.78.17:16667/", apiUrl: "http://74.48.78.17:16666/" }),
/16666/u
);
});
test("RPT-004 classifies deployed revision drift as NOT_CURRENT, never MVP PASS", () => {
const conclusion = classifyRpt004Conclusion([
check("expected-artifact-current", "pass"),
check("live-revision-current", "not_current"),
check("live-health-ready", "pass"),
check("16666-default-chinese-workbench", "pass")
]);
assert.equal(conclusion.status, "not_current");
assert.equal(conclusion.classification, "NOT_CURRENT");
assert.equal(conclusion.mvpPass, false);
});
test("RPT-004 classifies degraded live health as BLOCKED, never MVP PASS", () => {
const conclusion = classifyRpt004Conclusion([
check("expected-artifact-current", "pass"),
check("live-revision-current", "pass"),
check("live-health-ready", "blocked"),
check("16666-default-chinese-workbench", "pass"),
check("m3-v1-io-trusted-loop", "not_run")
]);
assert.equal(conclusion.status, "blocked");
assert.equal(conclusion.classification, "BLOCKED");
assert.equal(conclusion.mvpPass, false);
});
test("RPT-004 reports MVP_PASS only when every required live dimension passes", () => {
const conclusion = classifyRpt004Conclusion([
check("expected-artifact-current", "pass"),
check("live-revision-current", "pass"),
check("live-health-ready", "pass"),
check("16666-default-chinese-workbench", "pass"),
check("code-agent-skill-path", "pass"),
check("code-agent-session-thread", "pass"),
check("skill-cli-hwlab-api-m3-route", "pass"),
check("m3-v1-io-trusted-loop", "pass"),
check("operation-audit-evidence-durable-green", "pass")
]);
assert.equal(conclusion.status, "pass");
assert.equal(conclusion.classification, "MVP_PASS");
assert.equal(conclusion.mvpPass, true);
});
test("Code Agent OpenAI fallback cannot satisfy skill path/toolCalls readiness", () => {
const result = classifyCodeAgentSkillPath({
status: "completed",
provider: "openai-responses",
model: "gpt-5.5",
backend: "hwlab-cloud-api/openai-responses",
implementationType: "openai-responses-fallback",
runner: {
kind: "openai-responses-fallback"
},
reply: {
content: "text answer"
},
toolCalls: [],
skills: {
status: "not_requested",
items: []
}
}, { httpStatus: 200, httpOk: true });
assert.equal(result.status, "blocked");
assert.match(result.summary, /openai-fallback/u);
});
test("Code Agent Codex stdio skill discovery passes with completed toolCalls and repo skill", () => {
const result = classifyCodeAgentSkillPath({
status: "completed",
conversationId: "cnv_test",
sessionId: "ses_test",
messageId: "msg_test",
traceId: "trc_test",
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_test",
status: "idle",
idleTimeoutMs: 1800000,
lastTraceId: "trc_test",
longLivedSession: true,
codexStdio: true
},
sessionReuse: {
reused: false,
turn: 1
},
runner: {
kind: "codex-app-server-stdio-runner",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
codexStdio: true,
writeCapable: true,
durableSession: true
},
runnerTrace: {
runnerKind: "codex-app-server-stdio-runner"
},
longLivedSessionGate: {
status: "pass",
pass: true
},
toolCalls: [{
name: "skills.discover",
status: "completed",
type: "file-read"
}],
skills: {
status: "ready",
items: [{
name: "hwlab-agent-runtime",
source: "/workspace/hwlab/skills/hwlab-agent-runtime/SKILL.md"
}],
count: 1
}
}, { httpStatus: 200, httpOk: true });
assert.equal(result.status, "pass");
});
test("Code Agent controlled read-only session skill discovery passes the harness skill path gate", () => {
const result = classifyCodeAgentSkillPath({
...codexSessionPayload({
toolName: "skills.discover",
skills: {
status: "ready",
items: [{
name: "hwlab-agent-runtime",
source: "/workspace/hwlab/skills/hwlab-agent-runtime/SKILL.md"
}],
count: 1
}
}),
provider: "codex-readonly-runner",
backend: "hwlab-cloud-api/codex-readonly-runner",
sandbox: "read-only",
capabilityLevel: "read-only-session-tools",
sessionMode: "controlled-readonly-session-registry",
implementationType: "controlled-readonly-session-registry",
runner: {
kind: "hwlab-readonly-runner",
workspace: "/workspace/hwlab",
sandbox: "read-only",
sessionMode: "controlled-readonly-session-registry",
implementationType: "controlled-readonly-session-registry",
codexStdio: false,
writeCapable: false,
durableSession: false
},
runnerTrace: {
runnerKind: "hwlab-readonly-runner",
sessionMode: "controlled-readonly-session-registry"
}
}, { httpStatus: 200, httpOk: true });
assert.equal(result.status, "pass");
assert.equal(result.observations.controlledSessionRunner, true);
});
test("Code Agent three-turn same session classifier passes with pwd, skills, and context reference", () => {
const turns = [
sessionTurn("pwd", codexSessionPayload({
toolName: "pwd",
turn: 1,
reused: false,
reply: "当前工作目录是 /workspace/hwlab。"
})),
sessionTurn("skills", codexSessionPayload({
toolName: "skills.discover",
turn: 2,
reused: true,
skills: {
status: "ready",
items: [{
name: "hwlab-agent-runtime",
source: "/workspace/hwlab/skills/hwlab-agent-runtime/SKILL.md"
}],
count: 1
},
reply: "可用 skill 包括 hwlab-agent-runtime。"
})),
sessionTurn("context-pwd", codexSessionPayload({
toolName: "pwd",
turn: 3,
reused: true,
reply: "前文第一轮你问的是 pwd;当前工作目录仍是 /workspace/hwlab。"
}))
];
const result = classifyCodeAgentSessionTurns(turns, { expectedConversationId: "cnv_rpt004_fixture" });
assert.equal(result.status, "pass");
assert.deepEqual(result.observations.conversationIds, ["cnv_rpt004_fixture"]);
assert.deepEqual(result.observations.sessionIds, ["ses_rpt004_fixture"]);
assert.equal(result.observations.turns[2].referencesPrevious, true);
});
test("Code Agent fallback-only turns cannot satisfy session checks", () => {
const turns = [
sessionTurn("pwd", openAiFallbackPayload()),
sessionTurn("skills", openAiFallbackPayload()),
sessionTurn("context-pwd", openAiFallbackPayload({ reply: "第一轮是 pwd。" }))
];
const result = classifyCodeAgentSessionTurns(turns, { expectedConversationId: "cnv_rpt004_fixture" });
assert.equal(result.status, "blocked");
assert.equal(result.observations.fallbackOnly, true);
assert.ok(result.observations.blockers.includes("fallback-only"));
});
test("RPT-004 live report emits NOT_CURRENT and skips downstream checks", async () => {
const report = await buildRpt004Report({
...parseArgs([
"--live",
"--url",
"http://74.48.78.17:16666/",
"--api-url",
"http://74.48.78.17:16667/",
"--expected-commit",
fixtureCommit
]),
writeReport: false
}, {
repoRoot: "/fixture-rpt004",
fsJson: fixtureArtifactJson(),
now: () => "2026-05-23T00:00:00.000Z",
httpGetJson: async (url) => ({ ok: true, status: 200, json: healthPayload(url, { revision: "1111111" }) }),
httpGetText: async () => ({
ok: true,
status: 200,
contentType: "text/html; charset=utf-8",
body: workbenchHtml()
}),
layoutRunner: async () => {
throw new Error("layout must not run when NOT_CURRENT");
}
});
assert.equal(report.status, "not_current");
assert.equal(report.conclusion.classification, "NOT_CURRENT");
assert.equal(report.dimensions.codeAgentSession.status, "not_run");
assert.equal(report.dimensions.skillCliM3Route.status, "not_run");
assert.equal(report.liveMutationUsed, false);
});
test("M3 durable evidence requires operation, audit, evidence, DEV-LIVE green, and persisted writes", () => {
const green = classifyM3DurableEvidence(greenOperations());
assert.equal(green.status, "pass");
const blockedOps = greenOperations();
blockedOps[2].evidenceState.writeStatus = "written_non_durable";
const blocked = classifyM3DurableEvidence(blockedOps);
assert.equal(blocked.status, "blocked");
assert.equal(blocked.observations.sequence[2].persisted, false);
});
function check(id, status) {
return {
id,
status,
summary: `${id} ${status}`
};
}
function greenOperations() {
return [
operation("write-do1-true", "do.write", true),
operation("read-di1-true", "di.read", true),
operation("write-do1-false", "do.write", false),
operation("read-di1-false", "di.read", false)
];
}
function operation(id, action, value) {
return {
id,
action,
status: "succeeded",
operationId: `op_${id}`,
traceId: `trc_${id}`,
auditId: `aud_${id}`,
evidenceId: `evd_${id}`,
resultValue: value,
evidenceState: {
status: "green",
sourceKind: "DEV-LIVE",
durable: true,
writeStatus: "persisted"
}
};
}
function sessionTurn(kind, payload, response = { ok: true, status: 200 }) {
return {
kind,
response,
payload
};
}
function codexSessionPayload({
conversationId = "cnv_rpt004_fixture",
sessionId = "ses_rpt004_fixture",
toolName = "pwd",
turn = 1,
reused = false,
reply = "当前工作目录是 /workspace/hwlab。",
skills = {
status: "not_requested",
items: [],
count: 0
}
} = {}) {
return {
status: "completed",
conversationId,
sessionId,
messageId: `msg_rpt004_${turn}`,
traceId: `trc_rpt004_${turn}`,
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,
status: "idle",
idleTimeoutMs: 1800000,
lastTraceId: `trc_rpt004_${turn}`,
turn,
longLivedSession: true,
codexStdio: true,
durable: true,
writeCapable: true
},
sessionReuse: {
conversationId,
sessionId,
reused,
turn,
status: "idle"
},
runner: {
kind: "codex-app-server-stdio-runner",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
session: "codex-app-server-stdio-long-lived",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
codexStdio: true,
writeCapable: true,
durableSession: true
},
runnerTrace: {
traceId: `trc_rpt004_${turn}`,
runnerKind: "codex-app-server-stdio-runner",
sessionMode: "codex-app-server-stdio-long-lived",
sessionId,
turn,
sessionReused: reused,
events: [`turn:${turn}`, `tool:${toolName}:completed`]
},
longLivedSessionGate: {
status: "pass",
pass: true
},
toolCalls: [{
name: toolName,
status: "completed",
type: toolName === "skills.discover" ? "file-read" : "shell",
traceId: `trc_rpt004_${turn}`
}],
skills,
reply: {
content: reply
}
};
}
function openAiFallbackPayload({ reply = "text fallback" } = {}) {
return {
status: "completed",
conversationId: "cnv_rpt004_fixture",
sessionId: "ses_rpt004_fixture",
messageId: "msg_fallback",
traceId: "trc_fallback",
provider: "openai-responses",
model: "gpt-5.5",
backend: "hwlab-cloud-api/openai-responses",
implementationType: "openai-responses-fallback",
capabilityLevel: "text-chat-only",
sessionMode: "provider-text-request",
runner: {
kind: "openai-responses-fallback"
},
runnerTrace: {
runnerKind: "openai-responses-fallback"
},
toolCalls: [],
skills: {
status: "not_requested",
items: []
},
reply: {
content: reply
}
};
}
function healthPayload(url, { revision = fixtureCommit } = {}) {
const isWeb = String(url).includes(":16666");
return {
serviceId: isWeb ? "hwlab-cloud-web" : "hwlab-cloud-api",
environment: "dev",
status: "ready",
ready: true,
commit: {
id: revision
},
image: {
tag: revision.slice(0, 7),
digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
runtime: {
durable: true,
ready: true,
status: "ready",
blocker: null,
liveRuntimeEvidence: true
},
readiness: {
ready: true,
status: "ready",
durability: {
ready: true,
status: "ready",
blocker: null
}
}
};
}
function workbenchHtml() {
return `
<!doctype html>
<html lang="zh-CN">
<body data-app-shell>
<main class="workbench-shell">
<section data-view="workspace">
<h1>HWLAB 云工作台</h1>
<aside class="resource-tree">硬件资源</aside>
<section class="center-workspace">Agent 对话 使用说明</section>
<aside class="right-sidebar">可信记录 内部复核</aside>
<form id="command-form" class="command-form"></form>
</section>
<section data-view="gate" hidden>gate</section>
</main>
</body>
</html>`;
}
function fixtureArtifactJson() {
const serviceIds = [
"hwlab-cloud-api",
"hwlab-cloud-web",
"hwlab-gateway",
"hwlab-gateway-simu",
"hwlab-box-simu",
"hwlab-patch-panel",
"hwlab-router",
"hwlab-tunnel-client",
"hwlab-edge-proxy",
"hwlab-agent-skills"
];
const deploy = {
commitId: fixtureCommit
};
const catalog = {
commitId: fixtureCommit,
artifactState: "published",
publish: {
ciPublished: true,
registryVerified: true
},
services: serviceIds.map((serviceId) => ({
serviceId,
commitId: fixtureCommit,
imageTag: fixtureCommit.slice(0, 7),
digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
publishState: "published"
}))
};
const artifactReport = {
commitId: fixtureCommit,
artifactPublish: {
sourceCommitId: fixtureCommit
}
};
return async (relativePath) => {
if (relativePath === "deploy/deploy.yaml") return deploy;
if (relativePath === "deploy/artifact-catalog.dev.json") return catalog;
if (relativePath === "/tmp/hwlab-dev-gate/dev-artifacts.json") return artifactReport;
return null;
};
}