Files
pikasTech-HWLAB/web/hwlab-cloud-web/scripts/caserun-native-fixtures.ts
T

237 lines
8.9 KiB
TypeScript

import { createHash } from "node:crypto";
type NativeStatus = "queued" | "running" | "completed" | "failed" | "blocked" | "canceled";
interface NativeStep {
status: NativeStatus;
stage: string;
terminal: boolean;
blocker?: { code: string; summary: string; layer: string };
}
interface NativeRunState {
runId: string;
caseId: string;
cursor: number;
steps: NativeStep[];
createdAt: string;
}
const MODE = "native-test";
const CONTRACT_VERSION = "caserun.v1";
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: "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 }] }
};
let runSequence = 0;
const runs = new Map<string, NativeRunState>();
function timestamp(index: number): string {
return new Date(BASE_TIME + index * 1000).toISOString();
}
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}`,
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" ? [artifact] : []
};
const archivedRun = {
runId: run.runId,
caseId: run.caseId,
status: step.status,
stage: step.stage,
terminal: step.terminal,
mode: MODE,
hardwareRequired: false
};
const source = { runId: run.runId, caseId: run.caseId, status: step.status, summary, evidence, run: archivedRun };
return {
ok: true,
mode: MODE,
contractVersion: "caserun.aggregate.v1",
runId: run.runId,
caseId: run.caseId,
status: step.status,
stage: step.stage,
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
};
}
function runResponse(run: NativeRunState) {
const step = run.steps[run.cursor] ?? run.steps.at(-1)!;
return {
ok: true,
mode: MODE,
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,
blocker: step.blocker ?? null,
createdAt: run.createdAt,
updatedAt: new Date(Date.parse(run.createdAt) + run.cursor * 1000).toISOString(),
facts: {
jobId: `job-${run.runId}`,
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 },
aggregate: { href: `/v1/caserun/runs/${run.runId}/aggregate`, sha256: aggregateResponse(run).sha256 },
manifest: step.status === "completed" ? { href: `/v1/caserun/runs/${run.runId}/manifest`, count: 2 } : null,
replay: null,
trace: { traceId: `trace-${run.runId}` },
hwpod: { hwpodId: "native-hwpod", nodeId: "native-node" }
},
sourceAuthority: { mode: MODE, sequence: "request-driven" },
error: null,
statusUrl: `/v1/caserun/runs/${run.runId}`
};
}
export function listNativeCases() {
const cases = Object.entries(scenarios).map(([caseId, scenario]) => ({
caseId,
title: scenario.title,
mode: MODE,
available: true,
hwpodSpec: "native-test",
runtime: { sequence: scenario.steps.map((step) => step.status) }
}));
return { ok: true, mode: MODE, contractVersion: CONTRACT_VERSION, count: cases.length, cases, caseAuthority: { mode: MODE } };
}
export function startNativeRun(caseId: string) {
const scenario = scenarios[caseId];
if (!scenario) return null;
runSequence += 1;
const runId = `${caseId}-${runSequence}`;
const run: NativeRunState = { runId, caseId, cursor: 0, steps: scenario.steps, createdAt: timestamp(30 + runSequence) };
runs.set(runId, run);
return runResponse(run);
}
export function listNativeRuns(query: { status?: string; limit?: number; cursor?: string } = {}) {
const limit = query.limit ?? 30;
const before = query.cursor ? decodeCursor(query.cursor) : null;
const records = [...runs.values()]
.map((run) => runResponse(run))
.filter((run) => !query.status || run.status === query.status)
.filter((run) => !before || run.createdAt < before.createdAt || (run.createdAt === before.createdAt && run.runId < before.runId))
.sort((left, right) => right.createdAt.localeCompare(left.createdAt) || right.runId.localeCompare(left.runId));
const page = records.slice(0, limit);
const hasMore = records.length > limit;
return {
ok: true,
mode: MODE,
contractVersion: "hwlab-caserun-runs-v1",
count: page.length,
runs: page.map((run) => ({
runId: run.runId,
caseId: run.caseId,
workflowId: run.workflowId,
workflowRunId: run.workflowRunId,
status: run.status,
stage: run.stage,
terminal: run.terminal,
createdAt: run.createdAt,
updatedAt: run.updatedAt,
artifactCount: run.facts.artifactCount,
evidenceStatus: run.facts.evidenceStatus,
artifactManifestSha256: run.references.aggregate.sha256
})),
nextCursor: hasMore && page.length ? encodeCursor(page.at(-1)!) : null
};
}
export function readNativeRun(runId: string, advance = true) {
const run = runs.get(runId);
if (!run) return null;
if (advance && run.cursor < run.steps.length - 1) run.cursor += 1;
return runResponse(run);
}
export function readNativeEvents(runId: string) {
const run = runs.get(runId);
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 }
}));
return { ok: true, mode: MODE, contractVersion: CONTRACT_VERSION, runId, caseId: run.caseId, status: current.status, stage: current.stage, terminal: current.terminal, sourceAuthority: current.sourceAuthority, events };
}
export function readNativeAggregate(runId: string) {
const run = runs.get(runId);
return run ? aggregateResponse(run) : null;
}
export function resetNativeFixtures(): void {
runSequence = 0;
runs.clear();
const steps = scenarios["native-completed"]!.steps;
runs.set("cli-software-smoke-passed", {
runId: "cli-software-smoke-passed",
caseId: "software-smoke",
cursor: steps.length - 1,
steps,
createdAt: timestamp(20)
});
}
function encodeCursor(run: { createdAt: string; runId: string }): string {
return Buffer.from(JSON.stringify({ createdAt: run.createdAt, runId: run.runId }), "utf8").toString("base64url");
}
function decodeCursor(cursor: string): { createdAt: string; runId: string } {
const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
return { createdAt: String(parsed.createdAt), runId: String(parsed.runId) };
}
resetNativeFixtures();