feat: 完善 CaseRun 原生证据视图
Pipelines as Code CI / hwlab-nc01-v03-ci-poll- Success

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
root
2026-07-18 06:01:58 +02:00
parent e64eb95cd4
commit 2bc5fc653a
7 changed files with 255 additions and 47 deletions
@@ -15,9 +15,13 @@ test("native CaseRun fixtures expose six deterministic API-owned states", () =>
const running = readNativeRun(started!.runId);
assert.equal(running?.status, "running");
assert.equal(running?.terminal, false);
for (let index = 0; index < 3; index += 1) readNativeRun(started!.runId);
const completed = readNativeRun(started!.runId);
assert.equal(completed?.status, "completed");
assert.equal(completed?.terminal, true);
assert.equal(completed?.workflowId, `harnessrl-caserun-${started!.runId}`);
assert.equal(completed?.result.summary.artifactCount, 1);
assert.equal(completed?.result.summary.artifacts[0].path, "native-result.json");
assert.match(completed?.references.aggregate.sha256 ?? "", /^[a-f0-9]{64}$/);
const aggregate = readNativeAggregate(started!.runId);
@@ -27,7 +31,8 @@ test("native CaseRun fixtures expose six deterministic API-owned states", () =>
assert.equal(aggregate?.sha256, completed?.references.aggregate.sha256);
const events = readNativeEvents(started!.runId);
assert.deepEqual(events?.events.map((event) => event.status), ["queued", "running", "completed"]);
assert.deepEqual(events?.events.map((event) => event.stage), ["queued", "prepare", "prepared", "build-completed", "collect-completed", "completed"]);
assert.equal(events?.events[0]?.createdAt, events?.events[0]?.at);
assert.equal(events?.mode, "native-test");
});
@@ -23,7 +23,14 @@ const BASE_TIME = Date.parse("2026-07-17T00:00:00.000Z");
const scenarios: Record<string, { title: string; steps: NativeStep[] }> = {
"native-queued": { title: "原生排队中", steps: [{ status: "queued", stage: "queued", terminal: false }] },
"native-running": { title: "原生运行中", steps: [{ status: "queued", stage: "queued", terminal: false }, { status: "running", stage: "agent-running", terminal: false }] },
"native-completed": { title: "原生已完成", steps: [{ status: "queued", stage: "queued", terminal: false }, { status: "running", stage: "agent-running", terminal: false }, { status: "completed", stage: "completed", terminal: true }] },
"native-completed": { title: "原生已完成", steps: [
{ status: "queued", stage: "queued", terminal: false },
{ status: "running", stage: "prepare", terminal: false },
{ status: "running", stage: "prepared", terminal: false },
{ status: "running", stage: "build-completed", terminal: false },
{ status: "running", stage: "collect-completed", terminal: false },
{ status: "completed", stage: "completed", terminal: true }
] },
"native-failed": { title: "原生失败", steps: [{ status: "queued", stage: "queued", terminal: false }, { status: "running", stage: "building", terminal: false }, { status: "failed", stage: "failed", terminal: true, blocker: { code: "native-build-failed", summary: "原生构建按确定性场景失败", layer: "build" } }] },
"native-blocked": { title: "原生受阻", steps: [{ status: "queued", stage: "queued", terminal: false }, { status: "blocked", stage: "preparing", terminal: true, blocker: { code: "native-capability-blocked", summary: "原生能力按确定性场景受阻", layer: "hwpod" } }] },
"native-canceled": { title: "原生已取消", steps: [{ status: "queued", stage: "queued", terminal: false }, { status: "running", stage: "agent-running", terminal: false }, { status: "canceled", stage: "canceled", terminal: true }] }
@@ -38,15 +45,23 @@ function timestamp(index: number): string {
function aggregateResponse(run: NativeRunState) {
const step = run.steps[run.cursor] ?? run.steps.at(-1)!;
const artifact = {
path: "native-result.json",
bytes: 335,
sha256: createHash("sha256").update(`${run.runId}:native-result.json`).digest("hex")
};
const summary = {
mode: MODE,
status: step.status,
jobId: `job-${run.runId}`,
artifactCount: step.status === "completed" ? 2 : 0
artifacts: step.status === "completed" ? [artifact] : [],
artifactCount: step.status === "completed" ? 1 : 0
};
const evidence = {
mode: MODE,
status: step.status,
blocker: step.blocker ?? null,
artifacts: step.status === "completed" ? [{ name: "firmware.bin" }, { name: "caserun.log" }] : []
artifacts: step.status === "completed" ? [artifact] : []
};
const archivedRun = {
runId: run.runId,
@@ -54,7 +69,8 @@ function aggregateResponse(run: NativeRunState) {
status: step.status,
stage: step.stage,
terminal: step.terminal,
mode: MODE
mode: MODE,
hardwareRequired: false
};
const source = { runId: run.runId, caseId: run.caseId, status: step.status, summary, evidence, run: archivedRun };
return {
@@ -68,6 +84,7 @@ function aggregateResponse(run: NativeRunState) {
terminal: step.terminal,
sourceAuthority: { mode: MODE, sequence: "request-driven" },
sha256: createHash("sha256").update(JSON.stringify(source)).digest("hex"),
result: { run: archivedRun, summary, evidence },
summary,
evidence,
run: archivedRun
@@ -82,6 +99,8 @@ function runResponse(run: NativeRunState) {
contractVersion: CONTRACT_VERSION,
runId: run.runId,
caseId: run.caseId,
workflowId: `harnessrl-caserun-${run.runId}`,
workflowRunId: `native-workflow-${run.runId}`,
status: step.status,
stage: step.stage,
terminal: step.terminal,
@@ -90,9 +109,10 @@ function runResponse(run: NativeRunState) {
updatedAt: timestamp(run.cursor),
facts: {
jobId: `job-${run.runId}`,
artifactCount: step.status === "completed" ? 2 : 0,
artifactCount: step.status === "completed" ? 1 : 0,
evidenceStatus: step.status
},
result: aggregateResponse(run).result,
references: {
status: { href: `/v1/caserun/runs/${run.runId}` },
events: { href: `/v1/caserun/runs/${run.runId}/events`, count: run.cursor + 1 },
@@ -142,7 +162,9 @@ export function readNativeEvents(runId: string) {
if (!run) return null;
const current = runResponse(run);
const events = run.steps.slice(0, run.cursor + 1).map((step, index) => ({
id: index + 1,
at: timestamp(index),
createdAt: timestamp(index),
status: step.status,
stage: step.stage,
payload: { mode: MODE, terminal: step.terminal, blocker: step.blocker ?? null }
@@ -9,23 +9,29 @@ export default async function caserunNativeSmoke({ page, goto, wait, screenshot,
await page.locator("#caserun-refresh").click();
await wait(300);
const state = await page.locator("#caserun-status").evaluate((element) => element.textContent || "");
const mode = (await page.locator("#caserun-mode").textContent())?.trim() || "";
const terminal = (await page.locator("#caserun-terminal").textContent())?.trim() || "";
const runId = (await page.locator("#caserun-run-id").textContent())?.trim() || "";
const events = await page.locator("#caserun-event-text").textContent();
const aggregateStatus = (await page.locator("#caserun-aggregate-status").textContent())?.trim() || "";
const aggregateRunId = (await page.locator("#caserun-aggregate-run-id").textContent())?.trim() || "";
const aggregateSha = (await page.locator("#caserun-aggregate-sha").textContent())?.trim() || "";
const evidenceStatus = (await page.locator("#caserun-evidence-status").textContent())?.trim() || "";
await page.getByRole("tab", { name: "产物" }).click();
const artifactText = (await page.locator(".caserun-artifact-row").textContent()) || "";
await page.getByRole("tab", { name: "编排" }).click();
const workflowId = (await page.locator("#caserun-workflow-id").textContent())?.trim() || "";
await page.getByRole("tab", { name: "结果" }).click();
const conditions = {
mode: mode.includes("native-test"),
terminal: terminal === "是",
aggregateStatus: aggregateStatus === "已完成",
aggregateRunId: aggregateRunId === runId && runId.startsWith("native-completed-"),
aggregateSha: /^[a-f0-9]{64}$/.test(aggregateSha),
queuedEvent: events?.includes("queued") === true,
runningEvent: events?.includes("running") === true,
completedEvent: events?.includes("completed") === true
evidenceStatus: evidenceStatus === "已完成",
artifact: artifactText.includes("native-result.json") && artifactText.includes("335 B"),
workflow: workflowId === `harnessrl-caserun-${runId}`,
queuedEvent: events?.includes("进入队列") === true,
buildEvent: events?.includes("构建完成") === true,
completedEvent: events?.includes("运行完成") === true
};
const image = await screenshot("caserun-native-completed.png");
recordStep("caserun-native-completed", { ok: Object.values(conditions).every(Boolean), conditions });