1813 lines
70 KiB
JavaScript
1813 lines
70 KiB
JavaScript
import { createHash, randomUUID } from "node:crypto";
|
|
import { execFileSync } from "node:child_process";
|
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.mjs";
|
|
import {
|
|
DEV_ENDPOINT,
|
|
DEV_FRONTEND_ENDPOINT,
|
|
ENVIRONMENT_DEV,
|
|
SERVICE_IDS
|
|
} from "../../internal/protocol/index.mjs";
|
|
import {
|
|
M3_IO_CHAIN,
|
|
M3_IO_CONTROL_ROUTE
|
|
} from "../../internal/cloud/m3-io-control.mjs";
|
|
import {
|
|
buildM3IoControlSourceReport,
|
|
runM3IoControlLiveReport
|
|
} from "./m3-io-control-e2e.mjs";
|
|
import { runDevCloudWorkbenchLayoutSmoke } from "./dev-cloud-workbench-smoke-lib.mjs";
|
|
import { classifyCodexRunnerCapability } from "./code-agent-response-contract.mjs";
|
|
import {
|
|
HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
|
|
HWLAB_M3_IO_API_ROUTE,
|
|
HWLAB_M3_IO_SKILL_NAME
|
|
} from "../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs";
|
|
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const defaultJsonReportPath = tempReportPath("rpt-004-mvp-e2e.json");
|
|
const defaultMarkdownReportPath = tempReportPath("rpt-004-mvp-e2e.md");
|
|
const defaultArtifactReportPath = tempReportPath("dev-artifacts.json");
|
|
const issue = "pikasTech/HWLAB#316";
|
|
const taskId = "rpt004-mvp-e2e-harness";
|
|
const reportKind = "rpt-004-mvp-e2e-harness";
|
|
const requiredWorkbenchLabels = Object.freeze([
|
|
"HWLAB 云工作台",
|
|
"硬件资源",
|
|
"Agent 对话",
|
|
"可信记录",
|
|
"内部复核",
|
|
"使用说明"
|
|
]);
|
|
const workbenchMarkers = Object.freeze([
|
|
"data-app-shell",
|
|
"workbench-shell",
|
|
"resource-tree",
|
|
"center-workspace",
|
|
"right-sidebar",
|
|
"command-form"
|
|
]);
|
|
const expectedM3OperationIds = Object.freeze([
|
|
["write-do1-true", "do.write", true],
|
|
["read-di1-true", "di.read", true],
|
|
["write-do1-false", "do.write", false],
|
|
["read-di1-false", "di.read", false]
|
|
]);
|
|
const codeAgentSessionTurns = Object.freeze([
|
|
{
|
|
kind: "pwd",
|
|
message: "请用 pwd 列出你当前的工作目录,并保留本轮 session 证据。"
|
|
},
|
|
{
|
|
kind: "skills",
|
|
message: "请在同一个 conversation/session 中列出你能使用的所有 skills。"
|
|
},
|
|
{
|
|
kind: "context-pwd",
|
|
message: "请引用前文:第一轮我问的是 pwd。再次用 pwd 确认当前工作目录。"
|
|
}
|
|
]);
|
|
const m3ReadOnlySkillMessage = "请通过 M3 DI1 read 读取数字输入;使用 HWLAB API route /v1/m3/io。";
|
|
const forbiddenDirectHardwareTarget = /(?:gateway(?:-simu)?|box(?:-simu)?|patch-panel|hwlab-patch-panel|\/invoke\b|\/sync\/tick\b|:7101\b|:7201\b|:7301\b)/iu;
|
|
const statusRank = Object.freeze({
|
|
pass: 0,
|
|
not_run: 1,
|
|
blocked: 2,
|
|
degraded: 2,
|
|
not_current: 3
|
|
});
|
|
|
|
export async function runRpt004Cli(argv = [], options = {}) {
|
|
const args = parseArgs(argv);
|
|
if (args.help) {
|
|
process.stdout.write(`${usage()}\n`);
|
|
return 0;
|
|
}
|
|
|
|
const report = await buildRpt004Report(args, options);
|
|
if (args.writeReport) {
|
|
await writeReports(report, args, options.repoRoot ?? repoRoot);
|
|
}
|
|
|
|
const printable = args.pretty || args.writeReport ? report : compactReport(report);
|
|
process.stdout.write(`${JSON.stringify(printable, null, 2)}\n`);
|
|
return report.status === "pass" || args.live !== true ? 0 : 2;
|
|
}
|
|
|
|
export async function buildRpt004Report(args = {}, options = {}) {
|
|
const parsed = normalizeArgs(args);
|
|
const now = options.now ?? (() => new Date().toISOString());
|
|
const root = options.repoRoot ?? repoRoot;
|
|
const report = baseReport({ parsed, now, root });
|
|
|
|
const expected = await buildExpectedArtifactState({
|
|
root,
|
|
expectedCommit: parsed.expectedCommit,
|
|
fsJson: options.fsJson
|
|
});
|
|
report.expected = expected;
|
|
report.dimensions.artifact = expected.check;
|
|
addCheck(report, expected.check);
|
|
|
|
if (!parsed.live) {
|
|
addCheck(report, sourceHarnessCheck(root));
|
|
addNotRunDimensions(report, "RPT-004 source check did not touch live DEV endpoints.");
|
|
return finalizeReport(report);
|
|
}
|
|
|
|
validateLiveBoundary(parsed);
|
|
const httpGetJson = options.httpGetJson ?? getJson;
|
|
const httpGetText = options.httpGetText ?? getText;
|
|
const apiHealthUrl = joinBasePath(parsed.apiUrl, "/health/live");
|
|
const webHealthUrl = joinBasePath(parsed.url, "/health/live");
|
|
const [apiHealth, webHealth, html] = await Promise.all([
|
|
httpGetJson(apiHealthUrl, parsed.timeoutMs),
|
|
httpGetJson(webHealthUrl, parsed.timeoutMs),
|
|
httpGetText(parsed.url, parsed.timeoutMs)
|
|
]);
|
|
|
|
report.live.apiHealth = summarizeHealthResponse("hwlab-cloud-api", apiHealthUrl, apiHealth);
|
|
report.live.webHealth = summarizeHealthResponse("hwlab-cloud-web", webHealthUrl, webHealth);
|
|
report.live.indexHtml = summarizeHtmlResponse(parsed.url, html);
|
|
|
|
const revisionCheck = classifyLiveRevision({
|
|
expected,
|
|
apiHealth: report.live.apiHealth,
|
|
webHealth: report.live.webHealth
|
|
});
|
|
const healthCheck = classifyLiveHealth(report.live.apiHealth, report.live.webHealth);
|
|
const workbenchCheck = classifyDefaultChineseWorkbench(report.live.indexHtml);
|
|
report.dimensions.revision = revisionCheck;
|
|
report.dimensions.health = healthCheck;
|
|
report.dimensions.workbench = workbenchCheck;
|
|
addCheck(report, revisionCheck);
|
|
addCheck(report, healthCheck);
|
|
addCheck(report, workbenchCheck);
|
|
|
|
const currentAndHealthy = revisionCheck.status === "pass" && healthCheck.status === "pass";
|
|
if (!currentAndHealthy) {
|
|
const reason = revisionCheck.status !== "pass"
|
|
? "live revision is not current"
|
|
: "live health is degraded or blocked";
|
|
addNotRunDimensions(report, `RPT-004 stopped before downstream smoke because ${reason}.`);
|
|
return finalizeReport(report);
|
|
}
|
|
|
|
const layoutCheck = await runLayoutDimension(parsed, options);
|
|
report.dimensions.layout = layoutCheck;
|
|
addCheck(report, layoutCheck);
|
|
|
|
const codeAgentReport = await runCodeAgentSessionDimension(parsed, options);
|
|
report.live.codeAgent = codeAgentReport.summary;
|
|
report.dimensions.codeAgentSession = codeAgentReport.sessionCheck;
|
|
report.dimensions.codeAgentSkillPath = codeAgentReport.skillPathCheck;
|
|
addCheck(report, codeAgentReport.sessionCheck);
|
|
addCheck(report, codeAgentReport.skillPathCheck);
|
|
|
|
const skillCliRouteCheck = await runSkillCliM3RouteDimension(parsed, options);
|
|
report.live.skillCliM3Route = skillCliRouteCheck.observations;
|
|
report.dimensions.skillCliM3Route = skillCliRouteCheck;
|
|
addCheck(report, skillCliRouteCheck);
|
|
|
|
if (parsed.allowM3Write) {
|
|
const m3Report = await runM3TrustedLoop(parsed, options);
|
|
report.live.m3 = m3Report.summary;
|
|
report.dimensions.m3TrustedLoop = m3Report.loopCheck;
|
|
report.dimensions.durableEvidence = m3Report.durableCheck;
|
|
addCheck(report, m3Report.loopCheck);
|
|
addCheck(report, m3Report.durableCheck);
|
|
} else {
|
|
addReadOnlyM3Dimensions(report);
|
|
}
|
|
|
|
return finalizeReport(report);
|
|
}
|
|
|
|
export function parseArgs(argv = []) {
|
|
const args = {
|
|
live: false,
|
|
url: `${DEV_FRONTEND_ENDPOINT}/`,
|
|
apiUrl: `${DEV_ENDPOINT}/`,
|
|
expectedCommit: null,
|
|
reportPath: defaultJsonReportPath,
|
|
markdownPath: defaultMarkdownReportPath,
|
|
writeReport: false,
|
|
pretty: false,
|
|
timeoutMs: 10000,
|
|
allowM3Write: false,
|
|
help: false
|
|
};
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (arg === "--check") args.live = false;
|
|
else if (arg === "--live") args.live = true;
|
|
else if (arg === "--url") args.url = readArg(argv, ++index, arg);
|
|
else if (arg === "--api-url") args.apiUrl = readArg(argv, ++index, arg);
|
|
else if (arg === "--expected-commit") args.expectedCommit = readArg(argv, ++index, arg);
|
|
else if (arg === "--report") {
|
|
args.reportPath = readArg(argv, ++index, arg);
|
|
args.writeReport = true;
|
|
} else if (arg === "--markdown") {
|
|
args.markdownPath = readArg(argv, ++index, arg);
|
|
args.writeReport = true;
|
|
} else if (arg === "--no-write") {
|
|
args.writeReport = false;
|
|
} else if (arg === "--pretty") {
|
|
args.pretty = true;
|
|
} else if (arg === "--timeout-ms") {
|
|
args.timeoutMs = parseTimeout(readArg(argv, ++index, arg));
|
|
} else if (arg === "--allow-m3-write") {
|
|
args.allowM3Write = true;
|
|
} else if (arg === "--help" || arg === "-h") {
|
|
args.help = true;
|
|
} else {
|
|
throw new Error(`unknown argument: ${arg}`);
|
|
}
|
|
}
|
|
|
|
if (args.live && !argv.includes("--no-write")) {
|
|
args.writeReport = true;
|
|
}
|
|
args.expectedCommit = args.expectedCommit ? sanitizeCommit(args.expectedCommit) : null;
|
|
if (args.expectedCommit === "unknown") {
|
|
throw new Error("--expected-commit must be a 7-40 character Git SHA");
|
|
}
|
|
return args;
|
|
}
|
|
|
|
export function classifyRpt004Conclusion(checks = [], options = {}) {
|
|
const byId = new Map(checks.map((check) => [check.id, check]));
|
|
const revision = byId.get("live-revision-current");
|
|
if (revision?.status === "not_current") {
|
|
return {
|
|
status: "not_current",
|
|
classification: "NOT_CURRENT",
|
|
mvpPass: false,
|
|
summary: "RPT-004 is NOT_CURRENT: live 16666/16667 revision does not match the expected DEV artifact commit."
|
|
};
|
|
}
|
|
|
|
const worst = worstStatus(checks.map((check) => check.status));
|
|
if (worst === "pass") {
|
|
return {
|
|
status: "pass",
|
|
classification: "MVP_PASS",
|
|
mvpPass: true,
|
|
summary: options.allowM3Write === true
|
|
? "RPT-004 MVP PASS: current live revision, default Chinese workbench, layout smoke, Code Agent session, Skill CLI HWLAB API route, M3 trusted IO loop, and durable evidence are green."
|
|
: "RPT-004 MVP PASS: current live revision, default Chinese workbench, layout smoke, Code Agent session, Skill CLI HWLAB API route, and default read-only M3 route verification are green; no M3 write was attempted."
|
|
};
|
|
}
|
|
|
|
const sourceOnly = checks.some((check) => check.id === "rpt004-source-contract") &&
|
|
checks.every((check) => ["pass", "not_run"].includes(check.status));
|
|
if (sourceOnly) {
|
|
return {
|
|
status: "pass",
|
|
classification: "CHECK_ONLY",
|
|
mvpPass: false,
|
|
summary: "RPT-004 harness source check passed; no live DEV smoke ran, so this is not MVP PASS."
|
|
};
|
|
}
|
|
|
|
return {
|
|
status: "blocked",
|
|
classification: "BLOCKED",
|
|
mvpPass: false,
|
|
summary: "RPT-004 is BLOCKED: at least one required live dimension is blocked, degraded, skipped, or not run."
|
|
};
|
|
}
|
|
|
|
export function classifyCodeAgentSkillPath(payload, { httpStatus = null, httpOk = false } = {}) {
|
|
const capability = classifyCodexRunnerCapability(payload, { httpStatus });
|
|
const toolCalls = Array.isArray(payload?.toolCalls) ? payload.toolCalls : [];
|
|
const skillItems = Array.isArray(payload?.skills?.items) ? payload.skills.items : [];
|
|
const openAiFallback =
|
|
payload?.provider === "openai-responses" ||
|
|
payload?.runner?.kind === "openai-responses-fallback" ||
|
|
payload?.implementationType === "openai-responses-fallback";
|
|
const skillsDiscoverCompleted = toolCalls.some((tool) =>
|
|
tool?.name === "skills.discover" && tool?.status === "completed"
|
|
);
|
|
const repoSkillReady = skillItems.some((skill) =>
|
|
skill?.name === "hwlab-agent-runtime" ||
|
|
skill?.name === HWLAB_M3_IO_SKILL_NAME ||
|
|
String(skill?.source ?? "").includes("skills/hwlab-agent-runtime/SKILL.md")
|
|
);
|
|
const runnerKind = payload?.runner?.kind ?? null;
|
|
const controlledSessionRunner =
|
|
runnerKind === "codex-app-server-stdio-runner" ||
|
|
runnerKind === "hwlab-readonly-runner";
|
|
const missing = [];
|
|
if (!httpOk) missing.push(`HTTP ${httpStatus ?? "not_observed"}`);
|
|
if (capability.status !== "pass" && !controlledSessionRunner) {
|
|
missing.push(capability.blocker ?? "runner-capability");
|
|
}
|
|
if (openAiFallback) missing.push("openai-fallback");
|
|
if (!skillsDiscoverCompleted) missing.push("skills.discover toolCall");
|
|
if (payload?.skills?.status !== "ready") missing.push("skills.status=ready");
|
|
if (!repoSkillReady) missing.push("repo-owned hwlab-agent-runtime skill");
|
|
if (!payload?.runnerTrace) missing.push("runnerTrace");
|
|
if (!payload?.sessionMode) missing.push("sessionMode");
|
|
if (!payload?.capabilityLevel) missing.push("capabilityLevel");
|
|
if (!controlledSessionRunner) missing.push("controlled Code Agent runner");
|
|
|
|
return {
|
|
id: "code-agent-skill-path",
|
|
status: missing.length === 0 ? "pass" : "blocked",
|
|
summary: missing.length === 0
|
|
? "Code Agent skill discovery used the repo-owned Codex stdio runner with completed toolCalls and hwlab-agent-runtime skill evidence."
|
|
: `Code Agent skill path is blocked: missing ${missing.join(", ")}.`,
|
|
evidenceLevel: missing.length === 0 ? "DEV-LIVE" : "BLOCKED",
|
|
observations: {
|
|
provider: payload?.provider ?? null,
|
|
model: payload?.model ?? null,
|
|
backend: payload?.backend ?? null,
|
|
runnerKind,
|
|
capability,
|
|
controlledSessionRunner,
|
|
openAiFallback,
|
|
toolCalls: toolCalls.map((tool) => ({
|
|
name: tool?.name ?? null,
|
|
status: tool?.status ?? null,
|
|
type: tool?.type ?? null
|
|
})),
|
|
skills: {
|
|
status: payload?.skills?.status ?? null,
|
|
count: payload?.skills?.count ?? (Array.isArray(payload?.skills?.items) ? payload.skills.items.length : null),
|
|
repoSkillReady
|
|
},
|
|
assistantReplyStored: false
|
|
}
|
|
};
|
|
}
|
|
|
|
export function classifyCodeAgentSessionTurns(turns = [], { expectedConversationId = null } = {}) {
|
|
const normalizedTurns = Array.isArray(turns) ? turns : [];
|
|
const blockers = [];
|
|
if (normalizedTurns.length < 3) blockers.push("three-turn-session");
|
|
const completedTurns = normalizedTurns.filter((turn) => turn?.payload?.status === "completed" && turn?.response?.ok === true);
|
|
if (completedTurns.length !== normalizedTurns.length || normalizedTurns.length === 0) {
|
|
blockers.push("completed-http-turns");
|
|
}
|
|
|
|
const conversationIds = new Set();
|
|
const sessionIds = new Set();
|
|
const turnSummaries = normalizedTurns.map((turn, index) => {
|
|
const payload = turn?.payload ?? {};
|
|
const response = turn?.response ?? {};
|
|
const capability = classifyCodexRunnerCapability(payload, {
|
|
httpStatus: response.status ?? null
|
|
});
|
|
const toolCalls = Array.isArray(payload.toolCalls) ? payload.toolCalls : [];
|
|
const toolNames = toolCalls.map((tool) => tool?.name).filter(Boolean);
|
|
const runnerKind = payload.runner?.kind ?? null;
|
|
const sessionMode = payload.sessionMode ?? payload.runner?.sessionMode ?? null;
|
|
const sessionId = payload.sessionId ?? payload.session?.sessionId ?? null;
|
|
const conversationId = payload.conversationId ?? null;
|
|
const traceId = payload.traceId ?? null;
|
|
const runnerTrace = payload.runnerTrace && typeof payload.runnerTrace === "object" ? payload.runnerTrace : null;
|
|
const fallback = isOpenAiFallbackPayload(payload);
|
|
const partialRunner = payload.provider === "codex-readonly-runner" || runnerKind === "hwlab-readonly-runner";
|
|
const expectedTool = turn?.kind === "skills"
|
|
? "skills.discover"
|
|
: "pwd";
|
|
const hasExpectedTool = toolNames.includes(expectedTool);
|
|
const toolCallsCompleted = toolCalls.length > 0 && toolCalls.every((tool) => tool?.status === "completed");
|
|
const reply = typeof payload.reply?.content === "string" ? payload.reply.content : "";
|
|
const referencesPrevious = turn?.kind === "context-pwd"
|
|
? /pwd|第一轮|前文|same session|同一/u.test(reply)
|
|
: true;
|
|
const ready = response.ok === true &&
|
|
payload.status === "completed" &&
|
|
Boolean(conversationId) &&
|
|
Boolean(sessionId) &&
|
|
Boolean(traceId) &&
|
|
Boolean(runnerKind) &&
|
|
Boolean(sessionMode) &&
|
|
Boolean(payload.capabilityLevel) &&
|
|
Boolean(runnerTrace) &&
|
|
hasExpectedTool &&
|
|
toolCallsCompleted &&
|
|
!fallback;
|
|
|
|
if (conversationId) conversationIds.add(conversationId);
|
|
if (sessionId) sessionIds.add(sessionId);
|
|
return {
|
|
index: index + 1,
|
|
kind: turn?.kind ?? "unknown",
|
|
status: ready && referencesPrevious ? "pass" : "blocked",
|
|
httpStatus: response.status ?? null,
|
|
conversationId,
|
|
sessionId,
|
|
traceId,
|
|
provider: payload.provider ?? null,
|
|
backend: payload.backend ?? null,
|
|
runnerKind,
|
|
sessionMode,
|
|
capabilityLevel: payload.capabilityLevel ?? null,
|
|
capability,
|
|
toolNames,
|
|
toolCallsCompleted,
|
|
hasExpectedTool,
|
|
runnerTracePresent: Boolean(runnerTrace),
|
|
fallback,
|
|
partialRunner,
|
|
turn: payload.sessionReuse?.turn ?? payload.session?.turn ?? payload.runnerTrace?.turn ?? null,
|
|
reused: payload.sessionReuse?.reused ?? payload.session?.reused ?? payload.runnerTrace?.sessionReused ?? null,
|
|
referencesPrevious,
|
|
blockers: [
|
|
...(response.ok === true ? [] : [`HTTP ${response.status ?? "not_observed"}`]),
|
|
...(payload.status === "completed" ? [] : ["payload.status=completed"]),
|
|
...(conversationId ? [] : ["conversationId"]),
|
|
...(sessionId ? [] : ["sessionId"]),
|
|
...(traceId ? [] : ["traceId"]),
|
|
...(runnerKind ? [] : ["runner.kind"]),
|
|
...(sessionMode ? [] : ["sessionMode"]),
|
|
...(payload.capabilityLevel ? [] : ["capabilityLevel"]),
|
|
...(runnerTrace ? [] : ["runnerTrace"]),
|
|
...(hasExpectedTool ? [] : [`toolCall ${expectedTool}`]),
|
|
...(toolCallsCompleted ? [] : ["toolCalls completed"]),
|
|
...(fallback ? ["openai-fallback"] : []),
|
|
...(referencesPrevious ? [] : ["context-reference"])
|
|
]
|
|
};
|
|
});
|
|
|
|
if (expectedConversationId && !conversationIds.has(expectedConversationId)) {
|
|
blockers.push("expected-conversationId");
|
|
}
|
|
if (conversationIds.size !== 1) blockers.push("same-conversationId");
|
|
if (sessionIds.size !== 1) blockers.push("same-sessionId");
|
|
if (turnSummaries.some((turn) => turn.fallback)) blockers.push("fallback-only");
|
|
if (turnSummaries.some((turn) => turn.runnerTracePresent !== true)) blockers.push("runnerTrace");
|
|
if (turnSummaries.some((turn) => turn.toolCallsCompleted !== true)) blockers.push("toolCalls");
|
|
if (turnSummaries.some((turn) => turn.hasExpectedTool !== true)) blockers.push("expected-toolCalls");
|
|
if (turnSummaries.some((turn) => !turn.sessionMode || !turn.capabilityLevel || !turn.runnerKind)) {
|
|
blockers.push("runner/session/capability evidence");
|
|
}
|
|
if (turnSummaries.some((turn) => turn.kind === "context-pwd" && turn.referencesPrevious !== true)) {
|
|
blockers.push("context-reference");
|
|
}
|
|
|
|
const pass = blockers.length === 0 && turnSummaries.every((turn) => turn.status === "pass");
|
|
return {
|
|
id: "code-agent-session-thread",
|
|
status: pass ? "pass" : "blocked",
|
|
summary: pass
|
|
? "Code Agent completed three same-conversation/session turns for pwd, skills, and prior-context reference with runner/tool/session evidence."
|
|
: `Code Agent session thread is blocked: missing ${[...new Set(blockers)].join(", ")}.`,
|
|
evidenceLevel: pass ? "DEV-LIVE" : "BLOCKED",
|
|
observations: {
|
|
expectedConversationId,
|
|
conversationIds: [...conversationIds],
|
|
sessionIds: [...sessionIds],
|
|
turnCount: normalizedTurns.length,
|
|
turns: turnSummaries,
|
|
fallbackOnly: turnSummaries.length > 0 && turnSummaries.every((turn) => turn.fallback),
|
|
partialRunnerOnly: turnSummaries.length > 0 && turnSummaries.every((turn) => turn.partialRunner),
|
|
blockers: [...new Set(blockers)]
|
|
}
|
|
};
|
|
}
|
|
|
|
export function classifySkillCliM3Route(payload, { httpStatus = null, httpOk = false } = {}) {
|
|
const toolCalls = Array.isArray(payload?.toolCalls) ? payload.toolCalls : [];
|
|
const skillTool = toolCalls.find((tool) =>
|
|
tool?.name === HWLAB_M3_IO_SKILL_NAME ||
|
|
tool?.route === HWLAB_M3_IO_API_ROUTE ||
|
|
tool?.route === M3_IO_CONTROL_ROUTE
|
|
) ?? null;
|
|
const providerTrace = payload?.providerTrace && typeof payload.providerTrace === "object" ? payload.providerTrace : null;
|
|
const route = skillTool?.route ?? providerTrace?.route ?? payload?.skills?.items?.find((item) => item?.route)?.route ?? null;
|
|
const method = skillTool?.method ?? providerTrace?.method ?? null;
|
|
const operationId = skillTool?.operationId ?? providerTrace?.operationId ?? null;
|
|
const traceId = skillTool?.traceId ?? providerTrace?.traceId ?? payload?.traceId ?? null;
|
|
const auditId = skillTool?.auditId ?? skillTool?.audit?.auditId ?? providerTrace?.auditId ?? null;
|
|
const evidenceId = skillTool?.evidenceId ?? skillTool?.evidence?.evidenceId ?? providerTrace?.evidenceId ?? null;
|
|
const readback = skillTool?.readback ?? providerTrace?.readback ?? skillTool?.result?.targetReadback ?? null;
|
|
const blockers = normalizeM3RouteBlockers(payload, skillTool);
|
|
const safety = {
|
|
directGatewayCalls: Boolean(skillTool?.directGatewayCalls ?? payload?.runnerTrace?.directGatewayCalls ?? false),
|
|
directBoxCalls: Boolean(skillTool?.directBoxCalls ?? payload?.runnerTrace?.directBoxCalls ?? false),
|
|
directPatchPanelCalls: Boolean(skillTool?.directPatchPanelCalls ?? payload?.runnerTrace?.directPatchPanelCalls ?? false),
|
|
fallbackUsed: Boolean(skillTool?.fallbackUsed ?? payload?.providerTrace?.fallbackUsed ?? payload?.runnerTrace?.fallbackUsed ?? isOpenAiFallbackPayload(payload)),
|
|
routeOnly: route === HWLAB_M3_IO_API_ROUTE || route === M3_IO_CONTROL_ROUTE,
|
|
directTargetMentioned: directHardwareTargetMentioned(payload)
|
|
};
|
|
const structuredBlocker = blockers.length > 0;
|
|
const idsPresent = Boolean(operationId && traceId && auditId && evidenceId);
|
|
const accepted = skillTool?.accepted === true || providerTrace?.accepted === true;
|
|
const controlReady = skillTool?.controlReady === true || providerTrace?.controlReady === true;
|
|
const routeEvidenceReady = route === HWLAB_M3_IO_API_ROUTE || route === M3_IO_CONTROL_ROUTE;
|
|
const routePass = httpOk === true &&
|
|
payload?.status === "completed" &&
|
|
payload?.provider === "legacy-skill-cli" &&
|
|
skillTool &&
|
|
["completed", "blocked"].includes(skillTool.status) &&
|
|
routeEvidenceReady &&
|
|
method === "POST" &&
|
|
safety.directGatewayCalls === false &&
|
|
safety.directBoxCalls === false &&
|
|
safety.directPatchPanelCalls === false &&
|
|
safety.fallbackUsed === false &&
|
|
safety.directTargetMentioned === false &&
|
|
(idsPresent || structuredBlocker);
|
|
|
|
const missing = [];
|
|
if (!httpOk) missing.push(`HTTP ${httpStatus ?? "not_observed"}`);
|
|
if (payload?.status !== "completed") missing.push("payload.status=completed");
|
|
if (payload?.provider !== "legacy-skill-cli") missing.push("provider=legacy-skill-cli");
|
|
if (!skillTool) missing.push(`${HWLAB_M3_IO_SKILL_NAME} toolCall`);
|
|
if (!routeEvidenceReady) missing.push(`route=${HWLAB_M3_IO_API_ROUTE}`);
|
|
if (method !== "POST") missing.push("method=POST");
|
|
if (safety.directGatewayCalls || safety.directBoxCalls || safety.directPatchPanelCalls || safety.directTargetMentioned) {
|
|
missing.push("no direct gateway/box/patch-panel target");
|
|
}
|
|
if (safety.fallbackUsed) missing.push("no fallback");
|
|
if (!idsPresent && !structuredBlocker) missing.push("operationId/traceId/auditId/evidenceId or structured blocker");
|
|
|
|
return {
|
|
id: "skill-cli-hwlab-api-m3-route",
|
|
status: routePass ? "pass" : "blocked",
|
|
summary: routePass
|
|
? structuredBlocker
|
|
? `Skill CLI reached ${HWLAB_M3_IO_API_ROUTE} and returned a structured blocker instead of direct hardware/fallback evidence.`
|
|
: `Skill CLI used HWLAB API ${HWLAB_M3_IO_API_ROUTE} with operation/audit/evidence identifiers and no direct hardware/fallback path.`
|
|
: `Skill CLI HWLAB API route check is blocked: missing ${missing.join(", ")}.`,
|
|
evidenceLevel: routePass ? "DEV-LIVE" : "BLOCKED",
|
|
observations: {
|
|
httpStatus,
|
|
provider: payload?.provider ?? null,
|
|
backend: payload?.backend ?? null,
|
|
route,
|
|
expectedRoute: HWLAB_M3_IO_API_ROUTE,
|
|
method,
|
|
toolCallStatus: skillTool?.status ?? null,
|
|
accepted,
|
|
controlReady,
|
|
capabilityLevel: skillTool?.capabilityLevel ?? payload?.capabilityLevel ?? null,
|
|
operationId,
|
|
traceId,
|
|
auditId,
|
|
evidenceId,
|
|
readback,
|
|
idsPresent,
|
|
structuredBlocker,
|
|
blockers,
|
|
safety,
|
|
missing
|
|
}
|
|
};
|
|
}
|
|
|
|
export function classifyM3DurableEvidence(operations = []) {
|
|
const byId = new Map(operations.map((operation) => [operation.id, operation]));
|
|
const sequence = expectedM3OperationIds.map(([id, action, expectedValue]) => {
|
|
const operation = byId.get(id) ?? {};
|
|
const evidenceState = operation.evidenceState ?? {};
|
|
return {
|
|
id,
|
|
action,
|
|
expectedValue,
|
|
observedValue: Object.hasOwn(operation, "resultValue") ? operation.resultValue : null,
|
|
status: operation.status ?? "missing",
|
|
idsPresent: Boolean(operation.operationId && operation.traceId && operation.auditId && operation.evidenceId),
|
|
valueMatches: operation.resultValue === expectedValue,
|
|
durableGreen: evidenceState.status === "green" &&
|
|
evidenceState.sourceKind === "DEV-LIVE" &&
|
|
evidenceState.durable === true,
|
|
persisted: evidenceState.writeStatus === "persisted"
|
|
};
|
|
});
|
|
const ready = sequence.every((step) =>
|
|
step.status === "succeeded" &&
|
|
step.idsPresent &&
|
|
step.valueMatches &&
|
|
step.durableGreen &&
|
|
step.persisted
|
|
);
|
|
return {
|
|
id: "operation-audit-evidence-durable-green",
|
|
status: ready ? "pass" : "blocked",
|
|
summary: ready
|
|
? "Every M3 DO/DI step produced operationId, traceId, auditId, evidenceId, DEV-LIVE green evidence, and persisted write state."
|
|
: "M3 operation/audit/evidence durable green contract is incomplete.",
|
|
evidenceLevel: ready ? "DEV-LIVE" : "BLOCKED",
|
|
observations: {
|
|
requiredRoute: `${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort} -> ${M3_IO_CHAIN.patchPanelServiceId} -> ${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort}`,
|
|
sequence
|
|
}
|
|
};
|
|
}
|
|
|
|
function baseReport({ parsed, now, root }) {
|
|
const source = observeSource(root);
|
|
const liveMutationUsed = parsed.live === true && parsed.allowM3Write === true;
|
|
return {
|
|
$schema: "https://hwlab.pikastech.local/schemas/rpt-004-mvp-e2e-harness.schema.json",
|
|
$id: "https://hwlab.pikastech.local/dev-gate/rpt-004-mvp-e2e.json",
|
|
reportVersion: "v1",
|
|
reportKind,
|
|
issue,
|
|
supports: [
|
|
"pikasTech/HWLAB#7",
|
|
"pikasTech/HWLAB#78",
|
|
"pikasTech/HWLAB#131",
|
|
"pikasTech/HWLAB#143",
|
|
"pikasTech/HWLAB#227",
|
|
"pikasTech/HWLAB#236",
|
|
"pikasTech/HWLAB#239",
|
|
"pikasTech/HWLAB#275",
|
|
"pikasTech/HWLAB#315",
|
|
"pikasTech/HWLAB#316",
|
|
"pikasTech/HWLAB#317",
|
|
"pikasTech/HWLAB#334",
|
|
"pikasTech/HWLAB#351"
|
|
],
|
|
taskId,
|
|
task: "DC-DCSN-P0-2026-003",
|
|
acceptanceLevel: "rpt004_mvp_e2e",
|
|
devOnly: true,
|
|
prodDisabled: true,
|
|
reportLifecycle: activeReportLifecycle("RPT-004 live MVP E2E harness for active DEV 16666/16667 verification."),
|
|
mode: parsed.live ? "dev-live" : "source-check",
|
|
status: "blocked",
|
|
generatedAt: now(),
|
|
commitId: source.reportCommitId,
|
|
source,
|
|
target: {
|
|
environment: ENVIRONMENT_DEV,
|
|
browserUrl: parsed.url,
|
|
apiUrl: parsed.apiUrl,
|
|
apiHealthUrl: joinBasePath(parsed.apiUrl, "/health/live"),
|
|
webHealthUrl: joinBasePath(parsed.url, "/health/live"),
|
|
m3IoUrl: joinBasePath(parsed.apiUrl, M3_IO_CONTROL_ROUTE)
|
|
},
|
|
runPolicy: {
|
|
defaultLiveMode: "read-only",
|
|
liveMutationUsed,
|
|
allowM3Write: parsed.allowM3Write === true,
|
|
m3WriteRequiresExplicitFlag: "--allow-m3-write",
|
|
liveMutationAllowedOnlyOnDev: true,
|
|
summary: liveMutationUsed
|
|
? "Explicit DEV-only --allow-m3-write was provided; bounded simulator DO/DI write loop may run after current+healthy preconditions."
|
|
: "Default live mode performs only read-only live checks and payload classification; no M3 DO1 true/false write is attempted."
|
|
},
|
|
sourceContract: {
|
|
status: "pass",
|
|
documents: [
|
|
"AGENTS.md",
|
|
"docs/reference/dev-runtime-boundary.md",
|
|
"docs/reference/cloud-workbench.md",
|
|
"docs/reference/code-agent-chat-readiness.md",
|
|
"skills/hwlab-agent-runtime/SKILL.md"
|
|
],
|
|
summary: "RPT-004 keeps layout, live revision, M3 trusted IO, durable evidence, and Code Agent skill-path checks in one repo-owned entrypoint."
|
|
},
|
|
validationCommands: [
|
|
"node --check scripts/rpt004-mvp-e2e-harness.mjs",
|
|
"node --check scripts/src/rpt004-mvp-e2e-harness.mjs",
|
|
"node --test scripts/src/rpt004-mvp-e2e-harness.test.mjs",
|
|
"node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write",
|
|
"node scripts/rpt004-mvp-e2e-harness.mjs --live --url http://74.48.78.17:16666/ --api-url http://74.48.78.17:16667/",
|
|
`node scripts/validate-dev-gate-report.mjs ${defaultJsonReportPath}`
|
|
],
|
|
localSmoke: {
|
|
status: "not_run",
|
|
commands: ["node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write"],
|
|
evidence: ["Source-check mode validates the harness without touching live DEV."],
|
|
summary: "Local/source check is not a substitute for RPT-004 live MVP PASS."
|
|
},
|
|
dryRun: {
|
|
status: "not_applicable",
|
|
commands: ["node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write"],
|
|
evidence: ["RPT-004 is a bounded live smoke harness, not a deployment dry-run."],
|
|
summary: "No deploy apply, rollout, kubectl mutation, or PROD action is performed."
|
|
},
|
|
devPreconditions: {
|
|
status: parsed.live ? "checked-live" : "source-only",
|
|
requirements: [
|
|
"16666 and 16667 must be the active DEV endpoints.",
|
|
"Live API and Cloud Web revisions must match the expected DEV artifact commit.",
|
|
"API /health/live must be ready and durable before the M3 trusted IO loop runs.",
|
|
"OpenAI Responses text fallback cannot satisfy the Code Agent skill-path/toolCalls gate."
|
|
],
|
|
summary: "RPT-004 stops before M3 live writes when revision or health preconditions are not green."
|
|
},
|
|
expected: null,
|
|
live: {},
|
|
dimensions: {
|
|
artifact: null,
|
|
revision: null,
|
|
health: null,
|
|
workbench: null,
|
|
layout: null,
|
|
m3TrustedLoop: null,
|
|
durableEvidence: null,
|
|
codeAgentSkillPath: null,
|
|
codeAgentSession: null,
|
|
skillCliM3Route: null
|
|
},
|
|
checks: [],
|
|
blockers: [],
|
|
safety: {
|
|
devOnly: true,
|
|
prodTouched: false,
|
|
deployApplyAttempted: false,
|
|
rolloutAttempted: false,
|
|
kubectlMutationAttempted: false,
|
|
readsKubernetesSecrets: false,
|
|
secretValuesPrinted: false,
|
|
liveM3WritesRequireCurrentHealthyRevision: true,
|
|
liveM3WritesRequireExplicitFlag: true,
|
|
liveMutationUsed,
|
|
m3WritesLimitedToSimulatorDoDiResetLoop: true,
|
|
openAiFallbackCountsAsPass: false,
|
|
statement: liveMutationUsed
|
|
? "This harness performs public DEV HTTP checks, browser layout smoke, Code Agent session/Skill CLI checks, and an explicit DEV-only bounded M3 simulator DO/DI loop after current+healthy preconditions. It never applies deploys or starts rollouts."
|
|
: "Default live mode performs public DEV HTTP checks, browser layout smoke, Code Agent session/Skill CLI checks, and read-only M3 route payload classification only. It never writes DO1, applies deploys, starts rollouts, reads secrets, or touches PROD."
|
|
}
|
|
};
|
|
}
|
|
|
|
async function buildExpectedArtifactState({ root, expectedCommit, fsJson }) {
|
|
const readJson = typeof fsJson === "function"
|
|
? (relativePath) => fsJson(relativePath)
|
|
: (relativePath) => readJsonOptional(root, relativePath);
|
|
const deploy = await readJson("deploy/deploy.json");
|
|
const catalog = await readJson("deploy/artifact-catalog.dev.json");
|
|
const artifactReport = await readJson(defaultArtifactReportPath);
|
|
const deployCommit = sanitizeCommit(deploy?.commitId);
|
|
const catalogCommit = sanitizeCommit(catalog?.commitId);
|
|
const reportCommit = sanitizeCommit(artifactReport?.artifactPublish?.sourceCommitId ?? artifactReport?.commitId);
|
|
const commitId = expectedCommit ?? firstKnown(deployCommit, catalogCommit, reportCommit);
|
|
const services = Array.isArray(catalog?.services) ? catalog.services : [];
|
|
const serviceStates = services.map((service) => {
|
|
const serviceCommit = sanitizeCommit(service?.commitId);
|
|
return {
|
|
serviceId: service?.serviceId ?? "unknown",
|
|
commitId: serviceCommit,
|
|
imageTag: String(service?.imageTag ?? imageTagFromImage(service?.image) ?? "unknown"),
|
|
digest: String(service?.digest ?? "unknown"),
|
|
publishState: service?.publishState ?? "unknown",
|
|
commitMatches: commitEquivalent(serviceCommit, commitId),
|
|
digestPresent: /^sha256:[a-f0-9]{64}$/u.test(String(service?.digest ?? ""))
|
|
};
|
|
});
|
|
const missingServices = SERVICE_IDS.filter((serviceId) => !serviceStates.some((service) => service.serviceId === serviceId));
|
|
const mismatches = [];
|
|
if (!commitId || commitId === "unknown") mismatches.push("expected commit not observed");
|
|
if (deployCommit !== "unknown" && !commitEquivalent(deployCommit, commitId)) mismatches.push("deploy commit mismatch");
|
|
if (catalogCommit !== "unknown" && !commitEquivalent(catalogCommit, commitId)) mismatches.push("catalog commit mismatch");
|
|
if (reportCommit !== "unknown" && !commitEquivalent(reportCommit, commitId)) mismatches.push("artifact report commit mismatch");
|
|
if (catalog?.artifactState !== "published") mismatches.push("artifact catalog is not published");
|
|
if (catalog?.publish?.ciPublished !== true || catalog?.publish?.registryVerified !== true) {
|
|
mismatches.push("artifact catalog publish flags are not green");
|
|
}
|
|
if (missingServices.length > 0) mismatches.push(`missing services: ${missingServices.join(",")}`);
|
|
for (const service of serviceStates) {
|
|
if (service.publishState !== "published") mismatches.push(`${service.serviceId} is not published`);
|
|
if (!service.commitMatches) mismatches.push(`${service.serviceId} commit mismatch`);
|
|
if (!service.digestPresent) mismatches.push(`${service.serviceId} digest missing`);
|
|
}
|
|
|
|
const status = mismatches.length === 0 ? "pass" : "blocked";
|
|
return {
|
|
commitId,
|
|
shortCommitId: commitId === "unknown" ? "unknown" : commitId.slice(0, 7),
|
|
imageTag: commitId === "unknown" ? "unknown" : commitId.slice(0, 7),
|
|
deployCommit,
|
|
catalogCommit,
|
|
artifactReportCommit: reportCommit,
|
|
serviceCount: serviceStates.length,
|
|
requiredServiceCount: SERVICE_IDS.length,
|
|
services: serviceStates,
|
|
check: {
|
|
id: "expected-artifact-current",
|
|
status,
|
|
summary: status === "pass"
|
|
? `Expected DEV artifact commit ${commitId.slice(0, 7)} is published for all required services.`
|
|
: `Expected DEV artifact state is blocked: ${mismatches.join("; ")}.`,
|
|
evidenceLevel: status === "pass" ? "SOURCE" : "BLOCKED",
|
|
observations: {
|
|
commitId,
|
|
deployCommit,
|
|
catalogCommit,
|
|
artifactReportCommit: reportCommit,
|
|
serviceCount: serviceStates.length,
|
|
missingServices,
|
|
mismatches
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function sourceHarnessCheck(root) {
|
|
const requiredFiles = [
|
|
"scripts/rpt004-mvp-e2e-harness.mjs",
|
|
"scripts/src/rpt004-mvp-e2e-harness.mjs",
|
|
"scripts/src/rpt004-mvp-e2e-harness.test.mjs",
|
|
"scripts/dev-cloud-workbench-layout-smoke.mjs",
|
|
"scripts/m3-io-control-e2e.mjs",
|
|
"scripts/code-agent-chat-smoke.mjs",
|
|
"skills/hwlab-agent-runtime/SKILL.md"
|
|
];
|
|
const missing = requiredFiles.filter((file) => !pathExists(root, file));
|
|
return {
|
|
id: "rpt004-source-contract",
|
|
status: missing.length === 0 ? "pass" : "blocked",
|
|
summary: missing.length === 0
|
|
? "RPT-004 source harness and reused smoke entrypoints are present."
|
|
: `RPT-004 source harness is missing files: ${missing.join(", ")}.`,
|
|
evidenceLevel: missing.length === 0 ? "SOURCE" : "BLOCKED",
|
|
observations: {
|
|
requiredFiles,
|
|
missing
|
|
}
|
|
};
|
|
}
|
|
|
|
function classifyLiveRevision({ expected, apiHealth, webHealth }) {
|
|
const mismatches = [];
|
|
if (expected.check.status !== "pass") mismatches.push("expected artifact state is not green");
|
|
if (!apiHealth.reachable) mismatches.push("api health not reachable");
|
|
if (!webHealth.reachable) mismatches.push("web health not reachable");
|
|
if (apiHealth.environment !== ENVIRONMENT_DEV) mismatches.push(`api environment=${apiHealth.environment}`);
|
|
if (webHealth.environment !== ENVIRONMENT_DEV) mismatches.push(`web environment=${webHealth.environment}`);
|
|
if (!commitEquivalent(apiHealth.revision, expected.commitId) && apiHealth.imageTag !== expected.imageTag) {
|
|
mismatches.push(`api revision ${apiHealth.revision}/${apiHealth.imageTag} != ${expected.shortCommitId}`);
|
|
}
|
|
if (!commitEquivalent(webHealth.revision, expected.commitId) && webHealth.imageTag !== expected.imageTag) {
|
|
mismatches.push(`web revision ${webHealth.revision}/${webHealth.imageTag} != ${expected.shortCommitId}`);
|
|
}
|
|
const status = mismatches.length === 0 ? "pass" : "not_current";
|
|
return {
|
|
id: "live-revision-current",
|
|
status,
|
|
summary: status === "pass"
|
|
? `Live 16666/16667 revisions match expected artifact commit ${expected.shortCommitId}.`
|
|
: `Live revision is NOT_CURRENT: ${mismatches.join("; ")}.`,
|
|
evidenceLevel: status === "pass" ? "DEV-LIVE" : "BLOCKED",
|
|
observations: {
|
|
expectedCommit: expected.commitId,
|
|
expectedImageTag: expected.imageTag,
|
|
api: {
|
|
endpoint: apiHealth.endpoint,
|
|
serviceId: apiHealth.serviceId,
|
|
revision: apiHealth.revision,
|
|
imageTag: apiHealth.imageTag,
|
|
digest: apiHealth.digest
|
|
},
|
|
web: {
|
|
endpoint: webHealth.endpoint,
|
|
serviceId: webHealth.serviceId,
|
|
revision: webHealth.revision,
|
|
imageTag: webHealth.imageTag,
|
|
digest: webHealth.digest
|
|
},
|
|
mismatches
|
|
}
|
|
};
|
|
}
|
|
|
|
function classifyLiveHealth(apiHealth, webHealth) {
|
|
const blockers = [];
|
|
if (!apiHealth.reachable) blockers.push("api /health/live unreachable");
|
|
if (!webHealth.reachable) blockers.push("web /health/live unreachable");
|
|
if (!["ready", "ok"].includes(apiHealth.healthStatus) || apiHealth.ready !== true) {
|
|
blockers.push(`api health ${apiHealth.healthStatus}; ready=${apiHealth.ready}`);
|
|
}
|
|
if (apiHealth.runtime.durable !== true || apiHealth.runtime.ready !== true || apiHealth.readiness.durability.ready !== true) {
|
|
blockers.push(`runtime durable not green: ${apiHealth.runtime.status}/${apiHealth.runtime.blocker}`);
|
|
}
|
|
if (!["ok", "ready"].includes(webHealth.healthStatus)) {
|
|
blockers.push(`web health ${webHealth.healthStatus}`);
|
|
}
|
|
return {
|
|
id: "live-health-ready",
|
|
status: blockers.length === 0 ? "pass" : "blocked",
|
|
summary: blockers.length === 0
|
|
? "Live health is ready and runtime durable readiness is green."
|
|
: `Live health is BLOCKED/degraded: ${blockers.join("; ")}.`,
|
|
evidenceLevel: blockers.length === 0 ? "DEV-LIVE" : "BLOCKED",
|
|
observations: {
|
|
apiStatus: apiHealth.healthStatus,
|
|
apiReady: apiHealth.ready,
|
|
runtime: apiHealth.runtime,
|
|
readiness: apiHealth.readiness,
|
|
blockerCodes: apiHealth.blockerCodes,
|
|
webStatus: webHealth.healthStatus,
|
|
blockers
|
|
}
|
|
};
|
|
}
|
|
|
|
function classifyDefaultChineseWorkbench(html) {
|
|
const missingLabels = requiredWorkbenchLabels.filter((label) => !html.text.includes(label));
|
|
const missingMarkers = workbenchMarkers.filter((marker) => !html.text.includes(marker));
|
|
const workspaceDefault = /data-view=["']workspace["'][^>]*>/u.test(html.text) &&
|
|
!/<section[^>]*data-view=["']workspace["'][^>]*hidden/u.test(html.text);
|
|
const gateHidden = /data-view=["']gate["'][^>]*hidden/u.test(html.text);
|
|
const blockers = [];
|
|
if (!html.ok) blockers.push(`index HTML HTTP ${html.httpStatus ?? "not_observed"}`);
|
|
if (missingLabels.length > 0) blockers.push(`missing Chinese labels: ${missingLabels.join(",")}`);
|
|
if (missingMarkers.length > 0) blockers.push(`missing workbench markers: ${missingMarkers.join(",")}`);
|
|
if (!workspaceDefault) blockers.push("workspace view is not the default visible view");
|
|
if (!gateHidden) blockers.push("internal gate view is not hidden by default");
|
|
return {
|
|
id: "16666-default-chinese-workbench",
|
|
status: blockers.length === 0 ? "pass" : "blocked",
|
|
summary: blockers.length === 0
|
|
? "16666 default route serves the Chinese user workbench."
|
|
: `16666 default Chinese workbench check is blocked: ${blockers.join("; ")}.`,
|
|
evidenceLevel: blockers.length === 0 ? "DEV-LIVE" : "BLOCKED",
|
|
observations: {
|
|
url: html.url,
|
|
httpStatus: html.httpStatus,
|
|
contentType: html.contentType,
|
|
missingLabels,
|
|
missingMarkers,
|
|
workspaceDefault,
|
|
gateHidden,
|
|
bodyHash: html.bodyHash
|
|
}
|
|
};
|
|
}
|
|
|
|
async function runLayoutDimension(parsed, options) {
|
|
const layoutRunner = options.layoutRunner ?? runDevCloudWorkbenchLayoutSmoke;
|
|
const layout = await layoutRunner({
|
|
live: true,
|
|
urlExplicit: true,
|
|
url: parsed.url,
|
|
timeoutMs: parsed.timeoutMs,
|
|
artifactDir: path.join("tmp", "rpt004-layout", safeTimestamp())
|
|
});
|
|
const pass = layout.status === "pass";
|
|
return {
|
|
id: "layout-smoke-hit-target",
|
|
status: pass ? "pass" : "blocked",
|
|
summary: pass
|
|
? "Live layout smoke passed across desktop/mobile hit target, overflow, and route checks."
|
|
: `Live layout smoke is blocked: ${layout.summary ?? layout.status}.`,
|
|
evidenceLevel: pass ? "DEV-LIVE-LAYOUT" : "BLOCKED",
|
|
observations: {
|
|
status: layout.status,
|
|
sourceMode: layout.sourceMode,
|
|
url: layout.url,
|
|
failureCount: Array.isArray(layout.failures) ? layout.failures.length : 0,
|
|
blockers: layout.blockers ?? [],
|
|
screenshotDir: layout.artifacts?.screenshotDir ?? null,
|
|
screenshotCount: Array.isArray(layout.artifacts?.screenshots) ? layout.artifacts.screenshots.length : 0
|
|
}
|
|
};
|
|
}
|
|
|
|
async function runCodeAgentSkillPathDimension(parsed, options) {
|
|
const endpoint = joinBasePath(parsed.apiUrl, "/v1/agent/chat");
|
|
const response = options.codeAgentRunner
|
|
? await options.codeAgentRunner({ endpoint, parsed })
|
|
: await postJson(endpoint, {
|
|
conversationId: `cnv_rpt004_skills_${randomUUID()}`,
|
|
traceId: `trc_rpt004_skills_${randomUUID()}`,
|
|
projectId: "prj_hwlab-cloud-workbench",
|
|
message: "列出你能使用的所有skill"
|
|
}, parsed.timeoutMs);
|
|
const payload = response.json ?? response.body ?? null;
|
|
const check = classifyCodeAgentSkillPath(payload, {
|
|
httpStatus: response.status,
|
|
httpOk: response.ok
|
|
});
|
|
return {
|
|
...check,
|
|
observations: {
|
|
...check.observations,
|
|
endpoint,
|
|
httpStatus: response.status,
|
|
traceId: payload?.traceId ?? null,
|
|
messageId: payload?.messageId ?? null,
|
|
assistantReplyStored: false
|
|
}
|
|
};
|
|
}
|
|
|
|
async function runCodeAgentSessionDimension(parsed, options) {
|
|
const endpoint = joinBasePath(parsed.apiUrl, "/v1/agent/chat");
|
|
const conversationId = `cnv_rpt004_session_${randomUUID()}`;
|
|
const requestedSessionId = `ses_rpt004_session_${randomUUID()}`;
|
|
const turns = [];
|
|
for (const turn of codeAgentSessionTurns) {
|
|
const response = options.codeAgentRunner
|
|
? await options.codeAgentRunner({
|
|
endpoint,
|
|
parsed,
|
|
conversationId,
|
|
sessionId: requestedSessionId,
|
|
turn
|
|
})
|
|
: await postJson(endpoint, {
|
|
conversationId,
|
|
sessionId: requestedSessionId,
|
|
traceId: `trc_rpt004_${turn.kind}_${randomUUID()}`,
|
|
projectId: "prj_hwlab-cloud-workbench",
|
|
message: turn.message
|
|
}, parsed.timeoutMs);
|
|
turns.push({
|
|
kind: turn.kind,
|
|
message: turn.message,
|
|
response: {
|
|
ok: response.ok,
|
|
status: response.status
|
|
},
|
|
payload: response.json ?? response.body ?? null
|
|
});
|
|
}
|
|
|
|
const sessionCheck = classifyCodeAgentSessionTurns(turns, { expectedConversationId: conversationId });
|
|
const skillPayload = turns.find((turn) => turn.kind === "skills")?.payload ?? null;
|
|
const skillResponse = turns.find((turn) => turn.kind === "skills")?.response ?? {};
|
|
const skillPathCheck = classifyCodeAgentSkillPath(skillPayload, {
|
|
httpStatus: skillResponse.status ?? null,
|
|
httpOk: skillResponse.ok === true
|
|
});
|
|
|
|
return {
|
|
sessionCheck: {
|
|
...sessionCheck,
|
|
observations: {
|
|
...sessionCheck.observations,
|
|
endpoint
|
|
}
|
|
},
|
|
skillPathCheck: {
|
|
...skillPathCheck,
|
|
observations: {
|
|
...skillPathCheck.observations,
|
|
endpoint,
|
|
conversationId,
|
|
sessionId: requestedSessionId,
|
|
traceId: skillPayload?.traceId ?? null,
|
|
messageId: skillPayload?.messageId ?? null,
|
|
assistantReplyStored: false
|
|
}
|
|
},
|
|
summary: {
|
|
endpoint,
|
|
status: sessionCheck.status === "pass" && skillPathCheck.status === "pass" ? "pass" : "blocked",
|
|
conversationId,
|
|
sessionId: requestedSessionId,
|
|
turns: sessionCheck.observations.turns.map((turn) => ({
|
|
kind: turn.kind,
|
|
status: turn.status,
|
|
runnerKind: turn.runnerKind,
|
|
sessionMode: turn.sessionMode,
|
|
capabilityLevel: turn.capabilityLevel,
|
|
traceId: turn.traceId,
|
|
toolNames: turn.toolNames
|
|
}))
|
|
}
|
|
};
|
|
}
|
|
|
|
async function runSkillCliM3RouteDimension(parsed, options) {
|
|
const endpoint = joinBasePath(parsed.apiUrl, "/v1/agent/chat");
|
|
const conversationId = `cnv_rpt004_skill_m3_${randomUUID()}`;
|
|
const sessionId = `ses_rpt004_skill_m3_${randomUUID()}`;
|
|
const response = options.skillCliRunner
|
|
? await options.skillCliRunner({
|
|
endpoint,
|
|
parsed,
|
|
conversationId,
|
|
sessionId,
|
|
message: m3ReadOnlySkillMessage
|
|
})
|
|
: await postJson(endpoint, {
|
|
conversationId,
|
|
sessionId,
|
|
traceId: `trc_rpt004_skill_m3_${randomUUID()}`,
|
|
projectId: "prj_hwlab-cloud-workbench",
|
|
message: m3ReadOnlySkillMessage
|
|
}, parsed.timeoutMs);
|
|
const payload = response.json ?? response.body ?? null;
|
|
const check = classifySkillCliM3Route(payload, {
|
|
httpStatus: response.status,
|
|
httpOk: response.ok === true
|
|
});
|
|
return {
|
|
...check,
|
|
observations: {
|
|
...check.observations,
|
|
endpoint,
|
|
conversationId,
|
|
sessionId,
|
|
message: m3ReadOnlySkillMessage,
|
|
liveMutationUsed: false
|
|
}
|
|
};
|
|
}
|
|
|
|
async function runM3TrustedLoop(parsed, options) {
|
|
const m3Runner = options.m3Runner ?? defaultM3Runner;
|
|
const fullReport = await m3Runner(parsed);
|
|
const operations = Array.isArray(fullReport.liveOperations) ? fullReport.liveOperations : [];
|
|
const durableCheck = classifyM3DurableEvidence(operations);
|
|
const trustedGreen = fullReport.summary?.trustedGreen === true &&
|
|
fullReport.summary?.status === "pass" &&
|
|
durableCheck.status === "pass";
|
|
const loopCheck = {
|
|
id: "m3-v1-io-trusted-loop",
|
|
status: trustedGreen ? "pass" : "blocked",
|
|
summary: trustedGreen
|
|
? "M3 /v1/m3/io DO1 true/read/false/read trusted loop passed through the patch-panel route."
|
|
: `M3 /v1/m3/io trusted loop is blocked: ${fullReport.summary?.classification ?? "unknown"}.`,
|
|
evidenceLevel: trustedGreen ? "DEV-LIVE" : "BLOCKED",
|
|
observations: {
|
|
route: M3_IO_CONTROL_ROUTE,
|
|
requiredRoute: `${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort} -> ${M3_IO_CHAIN.patchPanelServiceId} -> ${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort}`,
|
|
classification: fullReport.summary?.classification ?? null,
|
|
trustedGreen: fullReport.summary?.trustedGreen === true,
|
|
operationCount: operations.length,
|
|
operations: operations.map((operation) => ({
|
|
id: operation.id,
|
|
action: operation.action,
|
|
status: operation.status,
|
|
resultValue: Object.hasOwn(operation, "resultValue") ? operation.resultValue : null,
|
|
operationId: operation.operationId ?? null,
|
|
traceId: operation.traceId ?? null,
|
|
auditId: operation.auditId ?? null,
|
|
evidenceId: operation.evidenceId ?? null,
|
|
evidenceState: {
|
|
status: operation.evidenceState?.status ?? null,
|
|
sourceKind: operation.evidenceState?.sourceKind ?? null,
|
|
durable: operation.evidenceState?.durable === true,
|
|
writeStatus: operation.evidenceState?.writeStatus ?? null
|
|
}
|
|
}))
|
|
}
|
|
};
|
|
return {
|
|
loopCheck,
|
|
durableCheck,
|
|
summary: {
|
|
mode: fullReport.mode,
|
|
status: fullReport.summary?.status ?? "unknown",
|
|
classification: fullReport.summary?.classification ?? null,
|
|
trustedGreen: fullReport.summary?.trustedGreen === true,
|
|
operationCount: operations.length
|
|
}
|
|
};
|
|
}
|
|
|
|
async function defaultM3Runner(parsed) {
|
|
const sourceReport = await buildM3IoControlSourceReport({ repoRoot });
|
|
return runM3IoControlLiveReport({
|
|
flags: new Set(["--live", "--confirm-dev", "--confirmed-non-production"]),
|
|
live: true,
|
|
confirmDev: true,
|
|
confirmedNonProduction: true,
|
|
target: "api",
|
|
frontendUrl: parsed.url,
|
|
apiUrl: joinBasePath(parsed.apiUrl, M3_IO_CONTROL_ROUTE),
|
|
timeoutMs: parsed.timeoutMs
|
|
}, sourceReport);
|
|
}
|
|
|
|
function addNotRunDimensions(report, reason) {
|
|
for (const [key, id] of [
|
|
["layout", "layout-smoke-hit-target"],
|
|
["codeAgentSession", "code-agent-session-thread"],
|
|
["codeAgentSkillPath", "code-agent-skill-path"],
|
|
["skillCliM3Route", "skill-cli-hwlab-api-m3-route"],
|
|
["m3TrustedLoop", "m3-v1-io-trusted-loop"],
|
|
["durableEvidence", "operation-audit-evidence-durable-green"]
|
|
]) {
|
|
if (report.dimensions[key]) continue;
|
|
const check = {
|
|
id,
|
|
status: "not_run",
|
|
summary: reason,
|
|
evidenceLevel: "BLOCKED",
|
|
observations: {
|
|
skipped: true,
|
|
reason
|
|
}
|
|
};
|
|
report.dimensions[key] = check;
|
|
addCheck(report, check);
|
|
}
|
|
}
|
|
|
|
function addReadOnlyM3Dimensions(report) {
|
|
const loopCheck = {
|
|
id: "m3-v1-io-trusted-loop",
|
|
status: "pass",
|
|
summary: "Default RPT-004 live mode did not write DO1; M3 route readiness is represented by the Skill CLI HWLAB API read-only route check.",
|
|
evidenceLevel: "DEV-LIVE",
|
|
observations: {
|
|
skippedWriteLoop: true,
|
|
liveMutationUsed: false,
|
|
allowM3Write: false,
|
|
requiredExplicitFlag: "--allow-m3-write",
|
|
route: M3_IO_CONTROL_ROUTE,
|
|
reason: "Default mode must not perform DO1 true/false writes."
|
|
}
|
|
};
|
|
const durableCheck = {
|
|
id: "operation-audit-evidence-durable-green",
|
|
status: "pass",
|
|
summary: "No DO1 write was attempted in default read-only mode; durable write evidence is not required unless --allow-m3-write is explicitly provided.",
|
|
evidenceLevel: "DEV-LIVE",
|
|
observations: {
|
|
notApplicableInDefaultReadOnlyMode: true,
|
|
liveMutationUsed: false,
|
|
allowM3Write: false,
|
|
requiredExplicitFlag: "--allow-m3-write",
|
|
evidenceIds: report.dimensions.skillCliM3Route?.observations
|
|
? [report.dimensions.skillCliM3Route.observations.evidenceId].filter(Boolean)
|
|
: []
|
|
}
|
|
};
|
|
report.live.m3 = {
|
|
mode: "read-only-route-check",
|
|
status: "pass",
|
|
classification: "READ_ONLY_ROUTE_CHECK",
|
|
trustedGreen: false,
|
|
liveMutationUsed: false,
|
|
operationCount: 0
|
|
};
|
|
report.dimensions.m3TrustedLoop = loopCheck;
|
|
report.dimensions.durableEvidence = durableCheck;
|
|
addCheck(report, loopCheck);
|
|
addCheck(report, durableCheck);
|
|
}
|
|
|
|
function finalizeReport(report) {
|
|
const conclusion = classifyRpt004Conclusion(report.checks, {
|
|
allowM3Write: report.runPolicy?.allowM3Write === true
|
|
});
|
|
report.status = conclusion.status;
|
|
report.conclusion = conclusion;
|
|
report.evidenceLevel = conclusion.mvpPass ? "DEV-LIVE-MVP" : conclusion.classification;
|
|
report.devLive = conclusion.mvpPass;
|
|
report.liveMutationUsed = report.runPolicy?.liveMutationUsed === true;
|
|
report.localSmoke.status = report.mode === "source-check" ? "pass" : "not_run";
|
|
report.devPreconditions.status = conclusion.status === "pass" ? "pass" : conclusion.status;
|
|
report.blockers = report.checks
|
|
.filter((check) => !["pass"].includes(check.status))
|
|
.filter((check) => !(conclusion.classification === "CHECK_ONLY" && check.status === "not_run"))
|
|
.map((check) => ({
|
|
type: check.status === "not_current" ? "runtime_blocker" : "observability_blocker",
|
|
scope: check.id,
|
|
status: "open",
|
|
summary: check.summary,
|
|
evidenceLevel: check.evidenceLevel ?? "BLOCKED"
|
|
}));
|
|
report.commanderSummary = buildCommanderSummary(report);
|
|
return report;
|
|
}
|
|
|
|
function buildCommanderSummary(report) {
|
|
const route = report.dimensions.skillCliM3Route?.observations ?? {};
|
|
const session = report.dimensions.codeAgentSession?.observations ?? {};
|
|
const skillPath = report.dimensions.codeAgentSkillPath?.observations ?? {};
|
|
const ids = {
|
|
operationId: route.operationId ?? null,
|
|
traceId: route.traceId ?? session.turns?.find((turn) => turn.traceId)?.traceId ?? null,
|
|
auditId: route.auditId ?? null,
|
|
evidenceId: route.evidenceId ?? null
|
|
};
|
|
const routeEvidenceContract = {
|
|
route: route.route ?? null,
|
|
expectedRoute: route.expectedRoute ?? HWLAB_M3_IO_API_ROUTE,
|
|
method: route.method ?? null,
|
|
operationId: ids.operationId,
|
|
traceId: ids.traceId,
|
|
auditId: ids.auditId,
|
|
evidenceId: ids.evidenceId,
|
|
accepted: route.accepted ?? null,
|
|
status: route.toolCallStatus ?? null,
|
|
controlReady: route.controlReady ?? null,
|
|
readback: route.readback ?? null,
|
|
visible: route.route === HWLAB_M3_IO_API_ROUTE &&
|
|
route.method === "POST" &&
|
|
Boolean(ids.operationId && ids.traceId && ids.auditId && ids.evidenceId)
|
|
};
|
|
return {
|
|
reportClass: "live-mutating",
|
|
allowedLiveMutationUsed: report.liveMutationUsed === true,
|
|
commit: report.expected?.commitId ?? "unknown",
|
|
revision: {
|
|
status: report.dimensions.revision?.status ?? "not_run",
|
|
expectedCommit: report.expected?.commitId ?? "unknown",
|
|
apiRevision: report.dimensions.revision?.observations?.api?.revision ?? null,
|
|
webRevision: report.dimensions.revision?.observations?.web?.revision ?? null
|
|
},
|
|
sessionResult: {
|
|
status: report.dimensions.codeAgentSession?.status ?? "not_run",
|
|
conversationIds: session.conversationIds ?? [],
|
|
sessionIds: session.sessionIds ?? [],
|
|
turnCount: session.turnCount ?? 0,
|
|
runnerKinds: [...new Set((session.turns ?? []).map((turn) => turn.runnerKind).filter(Boolean))],
|
|
sessionModes: [...new Set((session.turns ?? []).map((turn) => turn.sessionMode).filter(Boolean))],
|
|
capabilityLevels: [...new Set((session.turns ?? []).map((turn) => turn.capabilityLevel).filter(Boolean))]
|
|
},
|
|
skillCliResult: {
|
|
status: report.dimensions.codeAgentSkillPath?.status ?? "not_run",
|
|
runnerKind: skillPath.runnerKind ?? null,
|
|
skillsStatus: skillPath.skills?.status ?? null,
|
|
repoSkillReady: skillPath.skills?.repoSkillReady === true,
|
|
openAiFallback: skillPath.openAiFallback === true
|
|
},
|
|
m3RouteResult: {
|
|
status: report.dimensions.skillCliM3Route?.status ?? "not_run",
|
|
route: route.route ?? null,
|
|
expectedRoute: route.expectedRoute ?? HWLAB_M3_IO_API_ROUTE,
|
|
method: route.method ?? null,
|
|
structuredBlocker: route.structuredBlocker === true,
|
|
idsPresent: route.idsPresent === true,
|
|
accepted: route.accepted ?? null,
|
|
controlReady: route.controlReady ?? null,
|
|
readback: route.readback ?? null,
|
|
safety: route.safety ?? null
|
|
},
|
|
exactRouteEvidenceContract: routeEvidenceContract,
|
|
evidenceIds: Object.fromEntries(Object.entries(ids).filter(([, value]) => Boolean(value))),
|
|
blockers: report.blockers.map((blocker) => ({
|
|
scope: blocker.scope,
|
|
summary: blocker.summary,
|
|
evidenceLevel: blocker.evidenceLevel
|
|
})),
|
|
artifactReport: "not applicable: harness/scripts/tests only; no runtime service image was changed by RPT-004."
|
|
};
|
|
}
|
|
|
|
function addCheck(report, check) {
|
|
if (!check) return;
|
|
report.checks.push(check);
|
|
}
|
|
|
|
function compactReport(report) {
|
|
return {
|
|
reportKind: report.reportKind,
|
|
taskId: report.taskId,
|
|
mode: report.mode,
|
|
status: report.status,
|
|
conclusion: report.conclusion,
|
|
expected: {
|
|
commitId: report.expected?.commitId,
|
|
imageTag: report.expected?.imageTag,
|
|
serviceCount: report.expected?.serviceCount
|
|
},
|
|
target: report.target,
|
|
dimensions: Object.fromEntries(Object.entries(report.dimensions).map(([key, value]) => [
|
|
key,
|
|
value ? {
|
|
status: value.status,
|
|
summary: value.summary
|
|
} : null
|
|
])),
|
|
liveMutationUsed: report.liveMutationUsed,
|
|
commanderSummary: report.commanderSummary,
|
|
blockers: report.blockers,
|
|
artifacts: report.artifacts ?? null,
|
|
safety: report.safety
|
|
};
|
|
}
|
|
|
|
async function writeReports(report, args, root) {
|
|
const jsonPath = ensureNotRepoReportsPath(root, args.reportPath, "--report");
|
|
const markdownPath = ensureNotRepoReportsPath(root, args.markdownPath, "--markdown");
|
|
report.artifacts = {
|
|
jsonReportPath: relativeOrAbsolute(jsonPath, root),
|
|
markdownReportPath: relativeOrAbsolute(markdownPath, root),
|
|
artifactReport: "not applicable: harness/scripts/tests only; no runtime service image was changed by RPT-004."
|
|
};
|
|
await mkdir(path.dirname(jsonPath), { recursive: true });
|
|
await mkdir(path.dirname(markdownPath), { recursive: true });
|
|
await writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
await writeFile(markdownPath, renderMarkdown(report), "utf8");
|
|
}
|
|
|
|
export function renderMarkdown(report) {
|
|
const rows = Object.entries(report.dimensions).map(([key, check]) =>
|
|
`| ${key} | ${check?.status ?? "missing"} | ${escapeMarkdown(check?.summary ?? "missing")} |`
|
|
);
|
|
return [
|
|
"# RPT-004 MVP E2E Harness Report",
|
|
"",
|
|
`- status: ${report.status}`,
|
|
`- classification: ${report.conclusion?.classification ?? "unknown"}`,
|
|
`- mvpPass: ${report.conclusion?.mvpPass === true}`,
|
|
`- generatedAt: ${report.generatedAt}`,
|
|
`- expectedCommit: ${report.expected?.commitId ?? "unknown"}`,
|
|
`- liveMutationUsed: ${report.liveMutationUsed === true}`,
|
|
`- allowM3Write: ${report.runPolicy?.allowM3Write === true}`,
|
|
`- skillCliRoute: ${report.commanderSummary?.m3RouteResult?.route ?? "not_run"}`,
|
|
`- skillCliMethod: ${report.commanderSummary?.m3RouteResult?.method ?? "not_run"}`,
|
|
`- operationId: ${report.commanderSummary?.evidenceIds?.operationId ?? "none"}`,
|
|
`- traceId: ${report.commanderSummary?.evidenceIds?.traceId ?? "none"}`,
|
|
`- auditId: ${report.commanderSummary?.evidenceIds?.auditId ?? "none"}`,
|
|
`- evidenceId: ${report.commanderSummary?.evidenceIds?.evidenceId ?? "none"}`,
|
|
"",
|
|
"| Dimension | Status | Summary |",
|
|
"| --- | --- | --- |",
|
|
...rows,
|
|
"",
|
|
"## Blockers",
|
|
"",
|
|
...(report.blockers.length === 0
|
|
? ["- none"]
|
|
: report.blockers.map((blocker) => `- ${blocker.scope}: ${blocker.summary}`)),
|
|
"",
|
|
"## Safety",
|
|
"",
|
|
`- deployApplyAttempted: ${report.safety.deployApplyAttempted}`,
|
|
`- rolloutAttempted: ${report.safety.rolloutAttempted}`,
|
|
`- secretValuesPrinted: ${report.safety.secretValuesPrinted}`,
|
|
`- openAiFallbackCountsAsPass: ${report.safety.openAiFallbackCountsAsPass}`,
|
|
`- liveMutationUsed: ${report.safety.liveMutationUsed === true}`,
|
|
""
|
|
].join("\n");
|
|
}
|
|
|
|
function summarizeHealthResponse(serviceId, endpoint, response) {
|
|
const json = response.json ?? {};
|
|
const revision = sanitizeCommit(json.commit?.id ?? json.commitId ?? json.revision);
|
|
const imageTag = sanitizeRuntimeString(json.image?.tag ?? imageTagFromImage(json.image?.reference));
|
|
return {
|
|
endpoint,
|
|
reachable: response.ok === true && Boolean(response.json),
|
|
httpStatus: response.status,
|
|
serviceId: sanitizeRuntimeString(json.serviceId ?? serviceId),
|
|
environment: sanitizeRuntimeString(json.environment),
|
|
healthStatus: sanitizeRuntimeString(json.status),
|
|
ready: json.ready === true,
|
|
revision,
|
|
imageTag,
|
|
digest: sanitizeRuntimeString(json.image?.digest),
|
|
observedAt: sanitizeTimestamp(json.observedAt),
|
|
runtime: {
|
|
durable: json.runtime?.durable === true,
|
|
ready: json.runtime?.ready === true,
|
|
status: sanitizeRuntimeString(json.runtime?.status),
|
|
blocker: sanitizeRuntimeString(json.runtime?.blocker),
|
|
liveRuntimeEvidence: json.runtime?.liveRuntimeEvidence === true
|
|
},
|
|
readiness: {
|
|
ready: json.readiness?.ready === true,
|
|
status: sanitizeRuntimeString(json.readiness?.status),
|
|
durability: {
|
|
ready: json.readiness?.durability?.ready === true,
|
|
status: sanitizeRuntimeString(json.readiness?.durability?.status),
|
|
blocker: sanitizeRuntimeString(json.readiness?.durability?.blocker),
|
|
blockedLayer: sanitizeRuntimeString(json.readiness?.durability?.blockedLayer),
|
|
queryResult: sanitizeRuntimeString(json.readiness?.durability?.queryResult)
|
|
}
|
|
},
|
|
blockerCodes: Array.isArray(json.blockerCodes)
|
|
? json.blockerCodes.filter((item) => typeof item === "string")
|
|
: [],
|
|
error: response.error ? redactFailureText(response.error) : null
|
|
};
|
|
}
|
|
|
|
function summarizeHtmlResponse(url, response) {
|
|
const text = typeof response.body === "string" ? response.body : "";
|
|
return {
|
|
url,
|
|
ok: response.ok === true && response.status === 200 && /text\/html/iu.test(response.contentType ?? ""),
|
|
httpStatus: response.status,
|
|
contentType: response.contentType ?? "",
|
|
text,
|
|
bodyHash: createHash("sha256").update(text).digest("hex").slice(0, 16),
|
|
error: response.error ? redactFailureText(response.error) : null
|
|
};
|
|
}
|
|
|
|
async function getJson(url, timeoutMs) {
|
|
const response = await request(url, { method: "GET", timeoutMs, headers: { accept: "application/json" } });
|
|
return {
|
|
...response,
|
|
json: parseJson(response.body)
|
|
};
|
|
}
|
|
|
|
async function getText(url, timeoutMs) {
|
|
return request(url, { method: "GET", timeoutMs, headers: { accept: "text/html,*/*" } });
|
|
}
|
|
|
|
async function postJson(url, body, timeoutMs) {
|
|
const response = await request(url, {
|
|
method: "POST",
|
|
timeoutMs,
|
|
headers: {
|
|
accept: "application/json",
|
|
"content-type": "application/json"
|
|
},
|
|
body: JSON.stringify(body)
|
|
});
|
|
return {
|
|
...response,
|
|
json: parseJson(response.body)
|
|
};
|
|
}
|
|
|
|
async function request(url, { method, timeoutMs, headers, body } = {}) {
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
try {
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers,
|
|
body,
|
|
signal: controller.signal
|
|
});
|
|
const text = await response.text();
|
|
return {
|
|
ok: response.ok,
|
|
status: response.status,
|
|
contentType: response.headers.get("content-type") ?? "",
|
|
body: text,
|
|
error: response.ok ? null : response.statusText
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
status: 0,
|
|
contentType: "",
|
|
body: "",
|
|
error: error.name === "AbortError" ? `request timed out after ${timeoutMs}ms` : error.message
|
|
};
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
export function validateLiveBoundary(parsed) {
|
|
for (const [name, value, expectedPort] of [
|
|
["--url", parsed.url, 16666],
|
|
["--api-url", parsed.apiUrl, 16667]
|
|
]) {
|
|
const url = new URL(value);
|
|
const port = Number(url.port || (url.protocol === "http:" ? 80 : 443));
|
|
if (url.protocol !== "http:" || url.hostname !== "74.48.78.17" || port !== expectedPort) {
|
|
throw new Error(`${name} must target active DEV http://74.48.78.17:${expectedPort}/`);
|
|
}
|
|
if (port === 6666 || port === 6667) {
|
|
throw new Error(`${name} must use active DEV ports 16666/16667, not deprecated ${port}`);
|
|
}
|
|
if (/\bprod(?:uction)?\b/iu.test(url.href)) {
|
|
throw new Error(`${name} must not target PROD`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function normalizeArgs(args) {
|
|
return {
|
|
...parseArgs([]),
|
|
...args,
|
|
timeoutMs: args.timeoutMs ?? parseArgs([]).timeoutMs
|
|
};
|
|
}
|
|
|
|
function observeSource(root) {
|
|
const commitId = runGit(root, ["rev-parse", "--verify", "HEAD"]) ?? "unknown";
|
|
const shortCommitId = commitId === "unknown" ? "unknown" : commitId.slice(0, 12);
|
|
const status = runGit(root, ["status", "--porcelain"]);
|
|
const dirty = status === null ? null : status.length > 0;
|
|
return {
|
|
status: commitId === "unknown" ? "unknown" : "observed",
|
|
commitId,
|
|
shortCommitId,
|
|
reportCommitId: commitId !== "unknown" && dirty === false ? shortCommitId : "unknown",
|
|
worktreeState: dirty === null ? "unknown" : dirty ? "dirty" : "clean"
|
|
};
|
|
}
|
|
|
|
function runGit(root, args) {
|
|
try {
|
|
return execFileSync("git", args, {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
timeout: 5000
|
|
}).trim();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function readJsonOptional(root, relativePath) {
|
|
try {
|
|
return JSON.parse(await readFile(path.join(root, relativePath), "utf8"));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function pathExists(root, relativePath) {
|
|
try {
|
|
execFileSync("test", ["-f", path.join(root, relativePath)], { stdio: "ignore", timeout: 1000 });
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function joinBasePath(value, targetPath) {
|
|
const base = new URL(value);
|
|
return new URL(targetPath, `${base.protocol}//${base.host}/`).toString();
|
|
}
|
|
|
|
function readArg(argv, index, flag) {
|
|
const value = argv[index];
|
|
if (!value || value.startsWith("--")) {
|
|
throw new Error(`${flag} requires a value`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function parseTimeout(value) {
|
|
const parsed = Number.parseInt(value, 10);
|
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
throw new Error("--timeout-ms must be a positive integer");
|
|
}
|
|
return Math.min(parsed, 180000);
|
|
}
|
|
|
|
function worstStatus(statuses) {
|
|
return statuses
|
|
.filter(Boolean)
|
|
.sort((a, b) => (statusRank[b] ?? 2) - (statusRank[a] ?? 2))[0] ?? "not_run";
|
|
}
|
|
|
|
function firstKnown(...values) {
|
|
return values.find((value) => value && value !== "unknown") ?? "unknown";
|
|
}
|
|
|
|
function commitEquivalent(actual, expected) {
|
|
if (!actual || !expected || actual === "unknown" || expected === "unknown") return false;
|
|
return actual === expected || actual.startsWith(expected) || expected.startsWith(actual);
|
|
}
|
|
|
|
function sanitizeCommit(value) {
|
|
if (typeof value !== "string") return "unknown";
|
|
const trimmed = value.trim().toLowerCase();
|
|
return /^[a-f0-9]{7,40}$/u.test(trimmed) ? trimmed : "unknown";
|
|
}
|
|
|
|
function sanitizeRuntimeString(value) {
|
|
return typeof value === "string" && value.trim() ? value.trim() : "unknown";
|
|
}
|
|
|
|
function sanitizeTimestamp(value) {
|
|
return typeof value === "string" && !Number.isNaN(Date.parse(value)) ? value : new Date().toISOString();
|
|
}
|
|
|
|
function imageTagFromImage(image) {
|
|
if (typeof image !== "string") return null;
|
|
return image.match(/:([^:/]+)$/u)?.[1] ?? null;
|
|
}
|
|
|
|
function parseJson(value) {
|
|
try {
|
|
return value ? JSON.parse(value) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function isOpenAiFallbackPayload(payload) {
|
|
return payload?.provider === "openai-responses" ||
|
|
payload?.runner?.kind === "openai-responses-fallback" ||
|
|
payload?.implementationType === "openai-responses-fallback" ||
|
|
payload?.providerTrace?.fallbackUsed === true;
|
|
}
|
|
|
|
function normalizeM3RouteBlockers(payload, skillTool) {
|
|
const blockers = [];
|
|
for (const raw of [
|
|
skillTool?.blocker,
|
|
skillTool?.capabilityBlocker,
|
|
skillTool?.trustBlocker,
|
|
payload?.error,
|
|
payload?.availability?.blocker,
|
|
...(Array.isArray(skillTool?.blockers) ? skillTool.blockers : []),
|
|
...(Array.isArray(payload?.skills?.blockers) ? payload.skills.blockers : []),
|
|
...(Array.isArray(payload?.runnerTrace?.blockers) ? payload.runnerTrace.blockers : []),
|
|
...(Array.isArray(payload?.availability?.blockers) ? payload.availability.blockers : [])
|
|
]) {
|
|
const blocker = normalizeBlocker(raw);
|
|
if (!blocker) continue;
|
|
if (blockers.some((item) => item.code === blocker.code && item.message === blocker.message)) continue;
|
|
blockers.push(blocker);
|
|
}
|
|
if (skillTool?.stderrSummary && skillTool.stderrSummary !== "") {
|
|
const blocker = normalizeBlocker({
|
|
code: skillTool.stderrSummary,
|
|
message: skillTool.stderrSummary
|
|
});
|
|
if (blocker && !blockers.some((item) => item.code === blocker.code)) blockers.push(blocker);
|
|
}
|
|
return blockers;
|
|
}
|
|
|
|
function normalizeBlocker(raw) {
|
|
if (!raw) return null;
|
|
if (typeof raw === "string") {
|
|
return {
|
|
code: raw,
|
|
message: raw,
|
|
category: "blocked"
|
|
};
|
|
}
|
|
if (typeof raw !== "object") return null;
|
|
const code = raw.code ?? raw.blocker ?? raw.reason ?? raw.status ?? null;
|
|
const message = raw.message ?? raw.summary ?? raw.zh ?? raw.reason ?? code;
|
|
if (!code && !message) return null;
|
|
return {
|
|
code: String(code ?? "structured_blocker"),
|
|
message: String(message ?? code ?? "structured blocker"),
|
|
category: raw.category ?? raw.layer ?? "blocked",
|
|
sourceIssue: raw.sourceIssue ?? null
|
|
};
|
|
}
|
|
|
|
function directHardwareTargetMentioned(payload) {
|
|
const urls = [];
|
|
const collect = (value) => {
|
|
if (typeof value === "string") {
|
|
if (/https?:\/\//iu.test(value) || /\/invoke\b|\/sync\/tick\b/iu.test(value)) urls.push(value);
|
|
return;
|
|
}
|
|
if (!value || typeof value !== "object") return;
|
|
for (const [key, child] of Object.entries(value)) {
|
|
if (/policy|blocked|blocker|summary|message|note|limitations?|allowed|safety/iu.test(key)) continue;
|
|
collect(child);
|
|
}
|
|
};
|
|
collect(payload);
|
|
return urls.some((value) => forbiddenDirectHardwareTarget.test(value));
|
|
}
|
|
|
|
function safeTimestamp() {
|
|
return new Date().toISOString().replace(/[:.]/gu, "-");
|
|
}
|
|
|
|
function relativeOrAbsolute(absolutePath, root) {
|
|
const relative = path.relative(root, absolutePath);
|
|
return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : absolutePath;
|
|
}
|
|
|
|
function escapeMarkdown(value) {
|
|
return String(value ?? "").replace(/\|/gu, "\\|").replace(/\n/gu, " ");
|
|
}
|
|
|
|
function redactFailureText(value) {
|
|
return String(value ?? "")
|
|
.replace(/postgres(?:ql)?:\/\/[^\s"'<>]+/giu, "[redacted-postgres-url]")
|
|
.replace(/(password\s*[=:]\s*)[^\s,;]+/giu, "$1[redacted]")
|
|
.replace(/(token\s*[=:]\s*)[^\s,;]+/giu, "$1[redacted]")
|
|
.replace(/(OPENAI_API_KEY\s*[=:]\s*)[^\s,;]+/giu, "$1[redacted]");
|
|
}
|
|
|
|
function usage() {
|
|
return [
|
|
"Usage: node scripts/rpt004-mvp-e2e-harness.mjs --live --url http://74.48.78.17:16666/ --api-url http://74.48.78.17:16667/",
|
|
"",
|
|
"Default --check/source mode validates the harness only and does not claim MVP PASS.",
|
|
"--live reads DEV health and the 16666 workbench, then runs layout, Code Agent same-session checks, and Skill CLI /v1/m3/io route classification after current+healthy preconditions pass.",
|
|
"--allow-m3-write is required for the bounded DEV-only M3 DO/DI write loop; default live mode does not write DO1.",
|
|
"The command writes JSON and Markdown reports by default in --live mode. It never deploys, rolls out, reads secrets, or touches PROD."
|
|
].join("\n");
|
|
}
|
|
|
|
export function formatRpt004Failure(error) {
|
|
const message = redactFailureText(error instanceof Error ? error.message : String(error));
|
|
return {
|
|
reportKind,
|
|
issue,
|
|
taskId,
|
|
status: "blocked",
|
|
conclusion: {
|
|
status: "blocked",
|
|
classification: "BLOCKED",
|
|
mvpPass: false,
|
|
summary: "RPT-004 harness failed before producing a full report."
|
|
},
|
|
error: message,
|
|
safety: {
|
|
prodTouched: false,
|
|
deployApplyAttempted: false,
|
|
rolloutAttempted: false,
|
|
secretValuesPrinted: false
|
|
}
|
|
};
|
|
}
|