844 lines
25 KiB
JavaScript
844 lines
25 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("layout-smoke-hit-target", "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-mcp-stdio-runner",
|
|
workspace: "/workspace/hwlab",
|
|
sandbox: "workspace-write",
|
|
capabilityLevel: "long-lived-codex-stdio-session",
|
|
sessionMode: "codex-mcp-stdio-long-lived",
|
|
implementationType: "repo-owned-codex-mcp-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-mcp-stdio-runner",
|
|
workspace: "/workspace/hwlab",
|
|
sandbox: "workspace-write",
|
|
sessionMode: "codex-mcp-stdio-long-lived",
|
|
implementationType: "repo-owned-codex-mcp-stdio-session",
|
|
codexStdio: true,
|
|
writeCapable: true,
|
|
durableSession: true
|
|
},
|
|
runnerTrace: {
|
|
runnerKind: "codex-mcp-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("Skill CLI M3 route passes with operation/audit/evidence ids and no direct path", () => {
|
|
const result = classifySkillCliM3Route(skillCliPayload(), { httpStatus: 200, httpOk: true });
|
|
|
|
assert.equal(result.status, "pass");
|
|
assert.equal(result.observations.route, "/v1/m3/io");
|
|
assert.equal(result.observations.method, "POST");
|
|
assert.equal(result.observations.idsPresent, true);
|
|
assert.equal(result.observations.accepted, true);
|
|
assert.equal(result.observations.controlReady, true);
|
|
assert.equal(result.observations.readback.value, true);
|
|
assert.equal(result.observations.safety.directGatewayCalls, false);
|
|
assert.equal(result.observations.safety.fallbackUsed, false);
|
|
});
|
|
|
|
test("Skill CLI M3 route passes with structured blocker and no direct path", () => {
|
|
const payload = skillCliPayload({
|
|
accepted: false,
|
|
status: "blocked",
|
|
operationId: null,
|
|
auditId: null,
|
|
evidenceId: null,
|
|
blocker: {
|
|
code: "hwlab_api_unavailable",
|
|
message: "HWLAB API unavailable"
|
|
}
|
|
});
|
|
const result = classifySkillCliM3Route(payload, { httpStatus: 200, httpOk: true });
|
|
|
|
assert.equal(result.status, "pass");
|
|
assert.equal(result.observations.structuredBlocker, true);
|
|
assert.equal(result.observations.idsPresent, false);
|
|
});
|
|
|
|
test("Skill CLI M3 route blocks missing route and direct gateway evidence", () => {
|
|
const payload = skillCliPayload({
|
|
route: "http://hwlab-gateway-simu-1.hwlab-dev.svc.cluster.local:7101/invoke",
|
|
directGatewayCalls: true
|
|
});
|
|
const result = classifySkillCliM3Route(payload, { httpStatus: 200, httpOk: true });
|
|
|
|
assert.equal(result.status, "blocked");
|
|
assert.match(result.summary, /route=\/v1\/m3\/io/u);
|
|
assert.equal(result.observations.safety.directGatewayCalls, true);
|
|
});
|
|
|
|
test("Skill CLI M3 route requires explicit POST method evidence", () => {
|
|
const result = classifySkillCliM3Route(skillCliPayload({ method: null }), { httpStatus: 200, httpOk: true });
|
|
|
|
assert.equal(result.status, "blocked");
|
|
assert.match(result.summary, /method=POST/u);
|
|
assert.ok(result.observations.missing.includes("method=POST"));
|
|
});
|
|
|
|
test("RPT-004 live report defaults to read-only M3 route checks and records no live mutation", 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) }),
|
|
httpGetText: async () => ({
|
|
ok: true,
|
|
status: 200,
|
|
contentType: "text/html; charset=utf-8",
|
|
body: workbenchHtml()
|
|
}),
|
|
layoutRunner: async () => ({ status: "pass", summary: "layout ok", artifacts: { screenshots: [] } }),
|
|
codeAgentRunner: async ({ turn, conversationId, sessionId }) => ({
|
|
ok: true,
|
|
status: 200,
|
|
json: codexSessionPayload({
|
|
conversationId,
|
|
sessionId,
|
|
toolName: turn.kind === "skills" ? "skills.discover" : "pwd",
|
|
turn: turn.kind === "pwd" ? 1 : turn.kind === "skills" ? 2 : 3,
|
|
reused: turn.kind !== "pwd",
|
|
skills: turn.kind === "skills" ? {
|
|
status: "ready",
|
|
items: [{
|
|
name: "hwlab-agent-runtime",
|
|
source: "/workspace/hwlab/skills/hwlab-agent-runtime/SKILL.md"
|
|
}],
|
|
count: 1
|
|
} : undefined,
|
|
reply: turn.kind === "context-pwd"
|
|
? "前文第一轮你问的是 pwd;当前工作目录仍是 /workspace/hwlab。"
|
|
: "当前工作目录是 /workspace/hwlab。"
|
|
})
|
|
}),
|
|
skillCliRunner: async () => ({
|
|
ok: true,
|
|
status: 200,
|
|
json: skillCliPayload()
|
|
})
|
|
});
|
|
|
|
assert.equal(report.status, "pass");
|
|
assert.equal(report.liveMutationUsed, false);
|
|
assert.equal(report.safety.liveMutationUsed, false);
|
|
assert.equal(report.commanderSummary.allowedLiveMutationUsed, false);
|
|
assert.equal(report.commanderSummary.sessionResult.status, "pass");
|
|
assert.equal(report.commanderSummary.skillCliResult.status, "pass");
|
|
assert.equal(report.commanderSummary.m3RouteResult.route, "/v1/m3/io");
|
|
assert.equal(report.commanderSummary.m3RouteResult.method, "POST");
|
|
assert.equal(report.commanderSummary.m3RouteResult.accepted, true);
|
|
assert.equal(report.commanderSummary.m3RouteResult.controlReady, true);
|
|
assert.equal(report.commanderSummary.m3RouteResult.readback.value, true);
|
|
assert.equal(report.commanderSummary.exactRouteEvidenceContract.visible, true);
|
|
assert.equal(report.commanderSummary.exactRouteEvidenceContract.route, "/v1/m3/io");
|
|
assert.equal(report.commanderSummary.exactRouteEvidenceContract.method, "POST");
|
|
assert.equal(report.commanderSummary.exactRouteEvidenceContract.operationId, "op_rpt004_skill_m3");
|
|
assert.equal(report.commanderSummary.exactRouteEvidenceContract.traceId, "trc_rpt004_skill");
|
|
assert.equal(report.commanderSummary.exactRouteEvidenceContract.auditId, "aud_rpt004_skill_m3");
|
|
assert.equal(report.commanderSummary.exactRouteEvidenceContract.evidenceId, "evd_rpt004_skill_m3");
|
|
assert.equal(report.commanderSummary.exactRouteEvidenceContract.accepted, true);
|
|
assert.equal(report.commanderSummary.exactRouteEvidenceContract.status, "completed");
|
|
assert.equal(report.commanderSummary.exactRouteEvidenceContract.readback.value, true);
|
|
assert.equal(report.commanderSummary.evidenceIds.evidenceId, "evd_rpt004_skill_m3");
|
|
assert.equal(report.dimensions.skillCliM3Route.status, "pass");
|
|
assert.equal(report.dimensions.m3TrustedLoop.observations.skippedWriteLoop, true);
|
|
assert.equal(report.dimensions.durableEvidence.observations.notApplicableInDefaultReadOnlyMode, true);
|
|
});
|
|
|
|
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-mcp-stdio-runner",
|
|
workspace: "/workspace/hwlab",
|
|
sandbox: "workspace-write",
|
|
capabilityLevel: "long-lived-codex-stdio-session",
|
|
sessionMode: "codex-mcp-stdio-long-lived",
|
|
implementationType: "repo-owned-codex-mcp-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-mcp-stdio-runner",
|
|
workspace: "/workspace/hwlab",
|
|
sandbox: "workspace-write",
|
|
session: "codex-mcp-stdio-long-lived",
|
|
sessionMode: "codex-mcp-stdio-long-lived",
|
|
implementationType: "repo-owned-codex-mcp-stdio-session",
|
|
codexStdio: true,
|
|
writeCapable: true,
|
|
durableSession: true
|
|
},
|
|
runnerTrace: {
|
|
traceId: `trc_rpt004_${turn}`,
|
|
runnerKind: "codex-mcp-stdio-runner",
|
|
sessionMode: "codex-mcp-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 skillCliPayload({
|
|
route = "/v1/m3/io",
|
|
method = "POST",
|
|
accepted = true,
|
|
status = "succeeded",
|
|
operationId = "op_rpt004_skill_m3",
|
|
auditId = "aud_rpt004_skill_m3",
|
|
evidenceId = "evd_rpt004_skill_m3",
|
|
blocker = null,
|
|
directGatewayCalls = false,
|
|
directBoxCalls = false,
|
|
directPatchPanelCalls = false
|
|
} = {}) {
|
|
return {
|
|
status: "completed",
|
|
conversationId: "cnv_rpt004_skill",
|
|
sessionId: "ses_rpt004_skill",
|
|
messageId: "msg_rpt004_skill",
|
|
traceId: "trc_rpt004_skill",
|
|
provider: "hwlab-skill-cli",
|
|
model: "controlled-m3-io",
|
|
backend: "hwlab-cloud-api/hwlab-agent-runtime-skill-cli",
|
|
workspace: "/workspace/hwlab",
|
|
sandbox: "hwlab-api-route-only",
|
|
capabilityLevel: accepted ? "hwlab-api-control-ready" : "hwlab-api-control-blocked",
|
|
sessionMode: "controlled-m3-io-skill-cli",
|
|
implementationType: "skill-cli-hwlab-api-adapter",
|
|
runner: {
|
|
kind: "hwlab-m3-io-skill-cli",
|
|
safety: {
|
|
directGatewayCallsAllowed: false,
|
|
directBoxSimuCallsAllowed: false,
|
|
directPatchPanelCallsAllowed: false
|
|
}
|
|
},
|
|
runnerTrace: {
|
|
traceId: "trc_rpt004_skill",
|
|
runnerKind: "hwlab-m3-io-skill-cli",
|
|
method,
|
|
route,
|
|
fallbackUsed: false,
|
|
directGatewayCalls,
|
|
directBoxCalls,
|
|
directPatchPanelCalls
|
|
},
|
|
providerTrace: {
|
|
runnerKind: "hwlab-m3-io-skill-cli",
|
|
route,
|
|
method,
|
|
traceId: "trc_rpt004_skill",
|
|
operationId,
|
|
auditId,
|
|
evidenceId,
|
|
accepted,
|
|
controlReady: accepted,
|
|
fallbackUsed: false,
|
|
readback: {
|
|
status: "succeeded",
|
|
value: true,
|
|
resourceId: "res_boxsimu_2",
|
|
port: "DI1"
|
|
}
|
|
},
|
|
toolCalls: [{
|
|
name: "hwlab-agent-runtime.m3-io",
|
|
status: accepted ? "completed" : "blocked",
|
|
type: "skill-cli",
|
|
route,
|
|
method,
|
|
accepted,
|
|
controlReady: accepted,
|
|
capabilityLevel: accepted ? "hwlab-api-control-ready" : "hwlab-api-control-blocked",
|
|
operationId,
|
|
traceId: "trc_rpt004_skill",
|
|
auditId,
|
|
evidenceId,
|
|
audit: {
|
|
auditId,
|
|
status
|
|
},
|
|
evidence: {
|
|
evidenceId,
|
|
status: accepted ? "blocked" : "blocked",
|
|
sourceKind: accepted ? "BLOCKED" : "BLOCKED",
|
|
blocker: blocker?.code ?? "runtime_durable_not_green",
|
|
writeStatus: "written_non_durable"
|
|
},
|
|
durable: {
|
|
status: "degraded",
|
|
blocker: blocker?.code ?? "runtime_durable_not_green"
|
|
},
|
|
readback: {
|
|
status: "succeeded",
|
|
value: true,
|
|
resourceId: "res_boxsimu_2",
|
|
port: "DI1"
|
|
},
|
|
blocker,
|
|
blockers: blocker ? [blocker] : [{
|
|
code: "runtime_durable_not_green",
|
|
message: "durable trust is not green"
|
|
}],
|
|
directGatewayCalls,
|
|
directBoxCalls,
|
|
directPatchPanelCalls,
|
|
fallbackUsed: false
|
|
}],
|
|
skills: {
|
|
status: "used",
|
|
items: [{
|
|
name: "hwlab-agent-runtime.m3-io",
|
|
route
|
|
}],
|
|
blockers: blocker ? [blocker] : []
|
|
},
|
|
reply: {
|
|
content: "M3 IO Skill CLI result"
|
|
}
|
|
};
|
|
}
|
|
|
|
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-agent-mgr",
|
|
"hwlab-agent-worker",
|
|
"hwlab-gateway",
|
|
"hwlab-gateway-simu",
|
|
"hwlab-box-simu",
|
|
"hwlab-patch-panel",
|
|
"hwlab-router",
|
|
"hwlab-tunnel-client",
|
|
"hwlab-edge-proxy",
|
|
"hwlab-cli",
|
|
"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.json") return deploy;
|
|
if (relativePath === "deploy/artifact-catalog.dev.json") return catalog;
|
|
if (relativePath === "reports/dev-gate/dev-artifacts.json") return artifactReport;
|
|
return null;
|
|
};
|
|
}
|