237 lines
8.9 KiB
TypeScript
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();
|