Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -19,7 +19,7 @@ export const useCaseRunStore = defineStore("caserun", () => {
|
||||
error.value = null;
|
||||
const result = await caserunAPI.cases();
|
||||
cases.value = result.data?.cases ?? [];
|
||||
error.value = result.ok ? null : result.error ?? "CaseRun cases unavailable";
|
||||
error.value = result.ok ? null : result.error ?? "无法读取 CaseRun 用例";
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export const useCaseRunStore = defineStore("caserun", () => {
|
||||
const result = await caserunAPI.startRun(caseId);
|
||||
starting.value = false;
|
||||
if (!result.ok || !result.data) {
|
||||
error.value = result.error ?? "CaseRun start failed";
|
||||
error.value = result.error ?? "CaseRun 启动失败";
|
||||
return null;
|
||||
}
|
||||
currentRun.value = result.data;
|
||||
@@ -43,7 +43,7 @@ export const useCaseRunStore = defineStore("caserun", () => {
|
||||
if (!runId) return;
|
||||
const result = await caserunAPI.run(runId);
|
||||
if (!result.ok || !result.data) {
|
||||
error.value = result.error ?? "CaseRun run unavailable";
|
||||
error.value = result.error ?? "无法读取 CaseRun 运行";
|
||||
return;
|
||||
}
|
||||
currentRun.value = result.data;
|
||||
|
||||
@@ -517,12 +517,14 @@ export interface HwlabNodeUpdateMetadata {
|
||||
}
|
||||
export interface CaseRunCaseSummary { caseId: string; title?: string; mode?: string; available?: boolean; hwpodSpec?: string; subject?: Record<string, unknown> | null; expected?: Record<string, unknown> | null; runtime?: Record<string, unknown> | null; [key: string]: unknown }
|
||||
export interface CaseRunCasesResponse { ok?: boolean; mode?: string; contractVersion?: string; count?: number; cases?: CaseRunCaseSummary[]; caseAuthority?: Record<string, unknown>; [key: string]: unknown }
|
||||
export interface CaseRunEvent { at?: string; status?: string; stage?: string; payload?: Record<string, unknown>; [key: string]: unknown }
|
||||
export interface CaseRunEvent { id?: number; at?: string; createdAt?: string; status?: string; stage?: string; payload?: Record<string, unknown>; [key: string]: unknown }
|
||||
export interface CaseRunBlocker { code: string; summary: string; layer?: string | null; details?: Record<string, unknown> | null }
|
||||
export interface CaseRunReference { href?: string; ref?: string | null; sha256?: string | null; count?: number; relationship?: Record<string, unknown> | null; [key: string]: unknown }
|
||||
export interface CaseRunRunResponse { ok?: boolean; mode?: string; contractVersion?: string; runId?: string; caseId?: string; status?: string; stage?: string; terminal?: boolean; blocker?: CaseRunBlocker | null; createdAt?: string; updatedAt?: string; facts?: { jobId?: string | null; artifactCount?: number; evidenceStatus?: string | null }; references?: { status?: CaseRunReference; events?: CaseRunReference; aggregate?: CaseRunReference; manifest?: CaseRunReference | null; replay?: CaseRunReference | null; trace?: Record<string, unknown> | null; hwpod?: Record<string, unknown> | null }; sourceAuthority?: Record<string, unknown>; error?: { code?: string; message?: string } | null; statusUrl?: string; [key: string]: unknown }
|
||||
export interface CaseRunArtifact { path?: string; name?: string; bytes?: number; sha256?: string; [key: string]: unknown }
|
||||
export interface CaseRunResult { run?: { mode?: string; hardwareRequired?: boolean; [key: string]: unknown } | null; summary?: { mode?: string; status?: string; artifactCount?: number; artifacts?: CaseRunArtifact[]; [key: string]: unknown } | null; evidence?: { mode?: string; status?: string; artifacts?: CaseRunArtifact[]; [key: string]: unknown } | null; [key: string]: unknown }
|
||||
export interface CaseRunRunResponse { ok?: boolean; mode?: string; contractVersion?: string; runId?: string; caseId?: string; workflowId?: string; workflowRunId?: string; status?: string; stage?: string; terminal?: boolean; blocker?: CaseRunBlocker | null; createdAt?: string; updatedAt?: string; caseRepo?: string; runDir?: string; result?: CaseRunResult | null; facts?: { jobId?: string | null; artifactCount?: number; evidenceStatus?: string | null }; references?: { status?: CaseRunReference; events?: CaseRunReference; aggregate?: CaseRunReference; manifest?: CaseRunReference | null; replay?: CaseRunReference | null; trace?: Record<string, unknown> | null; hwpod?: Record<string, unknown> | null }; sourceAuthority?: Record<string, unknown>; error?: { code?: string; message?: string } | null; statusUrl?: string; [key: string]: unknown }
|
||||
export interface CaseRunEventsResponse { ok?: boolean; mode?: string; contractVersion?: string; runId?: string; caseId?: string; status?: string; stage?: string; terminal?: boolean; sourceAuthority?: Record<string, unknown>; events?: CaseRunEvent[]; [key: string]: unknown }
|
||||
export interface CaseRunAggregateResponse { ok?: boolean; mode?: string; contractVersion?: string; runId?: string; caseId?: string; status?: string; stage?: string; terminal?: boolean; sourceAuthority?: Record<string, unknown>; sha256?: string; summary?: Record<string, unknown> | null; evidence?: Record<string, unknown> | null; run?: Record<string, unknown> | null; [key: string]: unknown }
|
||||
export interface CaseRunAggregateResponse { ok?: boolean; mode?: string; contractVersion?: string; runId?: string; caseId?: string; status?: string; stage?: string; terminal?: boolean; sourceAuthority?: Record<string, unknown>; sha256?: string | null; result?: CaseRunResult | null; summary?: CaseRunResult["summary"]; evidence?: CaseRunResult["evidence"]; run?: CaseRunResult["run"]; [key: string]: unknown }
|
||||
export interface ProviderProfilesResponse { ok?: boolean; status?: string; profiles?: Array<Record<string, unknown>>; [key: string]: unknown }
|
||||
export interface ProviderProfileCatalogItem {
|
||||
profile: string;
|
||||
|
||||
@@ -6,10 +6,14 @@ import {
|
||||
Activity,
|
||||
Archive,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
Clock3,
|
||||
Clipboard,
|
||||
Cpu,
|
||||
ExternalLink,
|
||||
FileCheck2,
|
||||
FileCode2,
|
||||
GitBranch,
|
||||
RefreshCw,
|
||||
ShieldAlert,
|
||||
TerminalSquare,
|
||||
@@ -19,17 +23,17 @@ import PageCommandBar from "@/components/layout/PageCommandBar.vue";
|
||||
import StatusStrip, { type ConsoleStatusItem } from "@/components/layout/StatusStrip.vue";
|
||||
import StatusBadge from "@/components/common/StatusBadge.vue";
|
||||
import { useCaseRunStore } from "@/stores/caserun";
|
||||
import type { CaseRunCaseSummary, CaseRunEvent } from "@/types";
|
||||
import type { CaseRunArtifact, CaseRunCaseSummary, CaseRunEvent } from "@/types";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const caserun = useCaseRunStore();
|
||||
const selectedCaseId = ref("");
|
||||
type EvidenceTab = "aggregate" | "artifacts" | "trace" | "hwpod";
|
||||
type EvidenceTab = "aggregate" | "artifacts" | "workflow" | "hwpod";
|
||||
const evidenceTabs: Array<{ id: EvidenceTab; label: string }> = [
|
||||
{ id: "aggregate", label: "汇总" },
|
||||
{ id: "aggregate", label: "结果" },
|
||||
{ id: "artifacts", label: "产物" },
|
||||
{ id: "trace", label: "链路" },
|
||||
{ id: "workflow", label: "编排" },
|
||||
{ id: "hwpod", label: "HWPOD" },
|
||||
];
|
||||
const selectedTab = ref<EvidenceTab>("aggregate");
|
||||
@@ -46,25 +50,28 @@ const currentEvents = computed(() => caserun.events ?? []);
|
||||
const currentBlocker = computed(() => currentRun.value?.blocker ?? null);
|
||||
const aggregate = computed(() => caserun.aggregate);
|
||||
const terminal = computed(() => currentRun.value?.terminal === true);
|
||||
const resultSummary = computed(() => aggregate.value?.result?.summary ?? aggregate.value?.summary ?? currentRun.value?.result?.summary ?? null);
|
||||
const resultEvidence = computed(() => aggregate.value?.result?.evidence ?? aggregate.value?.evidence ?? currentRun.value?.result?.evidence ?? null);
|
||||
const resultRun = computed(() => aggregate.value?.result?.run ?? aggregate.value?.run ?? currentRun.value?.result?.run ?? null);
|
||||
const artifacts = computed<CaseRunArtifact[]>(() => resultSummary.value?.artifacts ?? resultEvidence.value?.artifacts ?? []);
|
||||
const artifactCount = computed(() => resultSummary.value?.artifactCount ?? currentRun.value?.facts?.artifactCount ?? artifacts.value.length);
|
||||
const evidenceStatus = computed(() => String(resultEvidence.value?.status ?? currentRun.value?.facts?.evidenceStatus ?? "pending"));
|
||||
const duration = computed(() => formatDuration(currentRun.value?.createdAt, currentRun.value?.updatedAt));
|
||||
const statusTone = computed(() => {
|
||||
if (currentStatusCode.value === "completed") return "ok";
|
||||
if (["failed", "blocked"].includes(currentStatusCode.value)) return "error";
|
||||
if (["queued", "running", "cancel_requested"].includes(currentStatusCode.value)) return "running";
|
||||
return "pending";
|
||||
});
|
||||
const fixtureMode = computed(() => String(aggregate.value?.mode ?? currentRun.value?.mode ?? selectedCase.value?.mode ?? "live"));
|
||||
const fixtureMode = computed(() => String(resultSummary.value?.mode ?? resultRun.value?.mode ?? aggregate.value?.mode ?? currentRun.value?.mode ?? selectedCase.value?.mode ?? "live"));
|
||||
const fixtureModeLabel = computed(() => modeLabel(fixtureMode.value));
|
||||
const stages = ["queued", "preparing", "agent-running", "building", "downloading", "uart-reading", "aggregating", "completed"];
|
||||
const stageIndex = computed(() => {
|
||||
const index = stages.indexOf(String(currentStageCode.value));
|
||||
return index >= 0 ? index : currentStatusCode.value === "failed" || currentStatusCode.value === "blocked" ? 2 : -1;
|
||||
});
|
||||
const statusItems = computed<ConsoleStatusItem[]>(() => [
|
||||
{ id: "cases", label: "可用用例", value: caserun.cases.length, tone: "info" },
|
||||
{ id: "status", label: "当前状态", value: currentStatus.value, tone: statusTone.value === "ok" ? "ok" : statusTone.value === "error" ? "error" : "info" },
|
||||
{ id: "stage", label: "当前阶段", value: currentStage.value, tone: "neutral" },
|
||||
{ id: "duration", label: "运行耗时", value: duration.value, tone: "neutral" },
|
||||
{ id: "events", label: "事件", value: currentEvents.value.length, tone: "neutral" },
|
||||
{ id: "artifacts", label: "产物", value: currentRun.value?.facts?.artifactCount ?? 0, tone: "neutral" },
|
||||
{ id: "artifacts", label: "产物", value: artifactCount.value, tone: "neutral" },
|
||||
{ id: "evidence", label: "证据", value: statusLabel(evidenceStatus.value), tone: evidenceStatus.value === "recorded" ? "ok" : "neutral" },
|
||||
]);
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -72,7 +79,10 @@ onMounted(async () => {
|
||||
selectedTab.value = parseTab(route.query.tab);
|
||||
await caserun.refreshCases();
|
||||
if (!selectedCaseId.value) selectedCaseId.value = caserun.cases[0]?.caseId ?? "";
|
||||
if (runId.value) await caserun.refreshRun(runId.value);
|
||||
if (runId.value) {
|
||||
await caserun.refreshRun(runId.value);
|
||||
if (caserun.currentRun?.caseId) selectedCaseId.value = caserun.currentRun.caseId;
|
||||
}
|
||||
pollTimer = setInterval(() => {
|
||||
if (caserun.shouldPoll) void caserun.refreshRun();
|
||||
}, 5000);
|
||||
@@ -89,7 +99,7 @@ watch(() => route.params.runId, async (value) => {
|
||||
watch(() => route.query.tab, (value) => { selectedTab.value = parseTab(value); });
|
||||
|
||||
function parseTab(value: unknown): EvidenceTab {
|
||||
return value === "artifacts" || value === "trace" || value === "hwpod" ? value : "aggregate";
|
||||
return value === "artifacts" || value === "workflow" || value === "hwpod" ? value : "aggregate";
|
||||
}
|
||||
|
||||
async function startRun(): Promise<void> {
|
||||
@@ -121,14 +131,24 @@ function eventLabel(event: CaseRunEvent): string {
|
||||
return `${stageLabel(event.stage)} · ${statusLabel(event.status)}`;
|
||||
}
|
||||
|
||||
function eventDetail(event: CaseRunEvent): string {
|
||||
const payload = event.payload ?? {};
|
||||
if (typeof payload.artifactSha256 === "string") return `产物 SHA256 · ${payload.artifactSha256}`;
|
||||
if (typeof payload.worker === "string") return `执行者 · ${payload.worker}`;
|
||||
if (typeof payload.mode === "string") return `运行模式 · ${modeLabel(payload.mode)}`;
|
||||
if (typeof payload.caseId === "string") return `用例 · ${payload.caseId}`;
|
||||
const entries = Object.entries(payload).filter(([, value]) => value !== null && value !== undefined && typeof value !== "object");
|
||||
return entries.length > 0 ? entries.map(([key, value]) => `${key}=${String(value)}`).join(" · ") : "阶段事实已记录";
|
||||
}
|
||||
|
||||
function statusLabel(value: unknown): string {
|
||||
const code = String(value ?? "unknown");
|
||||
return ({ idle: "空闲", pending: "等待中", queued: "已排队", running: "运行中", completed: "已完成", failed: "失败", blocked: "受阻", canceled: "已取消", cancelled: "已取消", cancel_requested: "正在取消", unknown: "未知" } as Record<string, string>)[code] ?? code;
|
||||
return ({ idle: "空闲", pending: "等待中", queued: "已排队", running: "运行中", completed: "已完成", recorded: "已记录", failed: "失败", blocked: "受阻", canceled: "已取消", cancelled: "已取消", cancel_requested: "正在取消", unknown: "未知" } as Record<string, string>)[code] ?? code;
|
||||
}
|
||||
|
||||
function stageLabel(value: unknown): string {
|
||||
const code = String(value ?? "unknown");
|
||||
return ({ "pending-selection": "等待选择用例", queued: "排队", preparing: "准备", prepared: "准备完成", "agent-running": "代理执行", building: "构建", downloading: "下载", "uart-reading": "串口读取", aggregating: "汇总", completed: "完成", failed: "失败", blocked: "受阻", canceled: "取消", cancelled: "取消", unknown: "未知阶段" } as Record<string, string>)[code] ?? code;
|
||||
return ({ "pending-selection": "等待选择用例", queued: "进入队列", prepare: "准备运行", preparing: "准备运行", prepared: "准备完成", "agent-running": "代理执行", building: "构建中", "build-completed": "构建完成", downloading: "下载", "uart-reading": "串口读取", "collect-completed": "证据归集", aggregating: "结果汇总", completed: "运行完成", failed: "运行失败", blocked: "运行受阻", canceled: "已取消", cancelled: "已取消", unknown: "未知阶段" } as Record<string, string>)[code] ?? code;
|
||||
}
|
||||
|
||||
function modeLabel(value: string): string {
|
||||
@@ -143,6 +163,25 @@ function eventTime(value?: string): string {
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? "--:--:--" : date.toLocaleTimeString("zh-CN", { hour12: false });
|
||||
}
|
||||
|
||||
function formatDuration(start?: string, end?: string): string {
|
||||
if (!start || !end) return "-";
|
||||
const elapsed = new Date(end).getTime() - new Date(start).getTime();
|
||||
if (!Number.isFinite(elapsed) || elapsed < 0) return "-";
|
||||
if (elapsed < 10_000) return `${(elapsed / 1000).toFixed(1)} 秒`;
|
||||
if (elapsed < 60_000) return `${Math.round(elapsed / 1000)} 秒`;
|
||||
return `${Math.floor(elapsed / 60_000)} 分 ${Math.round((elapsed % 60_000) / 1000)} 秒`;
|
||||
}
|
||||
|
||||
function artifactName(artifact: CaseRunArtifact): string {
|
||||
return artifact.path || artifact.name || "未命名产物";
|
||||
}
|
||||
|
||||
function artifactSize(bytes?: number): string {
|
||||
if (!Number.isFinite(bytes)) return "大小未知";
|
||||
if (Number(bytes) < 1024) return `${bytes} B`;
|
||||
return `${(Number(bytes) / 1024).toFixed(1)} KiB`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -191,18 +230,23 @@ function eventTime(value?: string): string {
|
||||
<p id="caserun-run-id">{{ currentRun?.runId || "启动 CaseRun 后,这里会显示稳定的运行标识。" }}</p>
|
||||
</div>
|
||||
<dl class="run-facts">
|
||||
<div><dt>阶段</dt><dd id="caserun-status">{{ currentStage }}</dd></div>
|
||||
<div><dt>终态</dt><dd id="caserun-terminal">{{ currentRun ? (currentRun.terminal ? "是" : "否") : "-" }}</dd></div>
|
||||
<div><dt>作业</dt><dd>{{ currentRun?.facts?.jobId || "-" }}</dd></div>
|
||||
<div><dt>产物</dt><dd>{{ currentRun?.facts?.artifactCount ?? 0 }}</dd></div>
|
||||
<div><dt>模式</dt><dd>{{ modeLabel(fixtureMode) }}</dd></div>
|
||||
<div><dt>耗时</dt><dd><Clock3 :size="12" aria-hidden="true" />{{ duration }}</dd></div>
|
||||
<div><dt>证据</dt><dd>{{ statusLabel(evidenceStatus) }}</dd></div>
|
||||
<div><dt>产物</dt><dd>{{ artifactCount }} 项</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="caserun-timeline" aria-label="CaseRun 执行阶段">
|
||||
<div class="section-heading"><div><span class="caserun-kicker">执行路径</span><h3>执行阶段</h3></div><span class="timeline-note">API 读取模型 · {{ currentRun?.updatedAt || "等待读取" }}</span></div>
|
||||
<div class="stage-track">
|
||||
<div v-for="(stage, index) in stages" :key="stage" class="stage-node" :class="{ reached: index <= stageIndex, active: index === stageIndex }"><span class="stage-dot"><Check v-if="index < stageIndex || terminal" :size="12" aria-hidden="true" /></span><span>{{ stageLabel(stage) }}</span></div>
|
||||
<div class="section-heading"><div><span class="caserun-kicker">实际路径</span><h3>服务端阶段</h3></div><span class="timeline-note">{{ currentEvents.length }} 个事实节点 · {{ duration }}</span></div>
|
||||
<div v-if="currentEvents.length" class="stage-track" :style="{ gridTemplateColumns: `repeat(${currentEvents.length}, minmax(96px, 1fr))` }">
|
||||
<div v-for="(event, index) in currentEvents" :key="event.id ?? `${event.createdAt || event.at}-${index}`" class="stage-node reached" :class="{ active: index === currentEvents.length - 1 }">
|
||||
<span class="stage-dot"><Check :size="12" aria-hidden="true" /></span>
|
||||
<span>{{ stageLabel(event.stage) }}</span>
|
||||
<time>{{ eventTime(event.createdAt || event.at) }}</time>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="timeline-empty">运行事件到达后,将按 API 返回顺序显示真实执行路径。</div>
|
||||
<div v-if="currentBlocker" class="caserun-blocker" role="status"><ShieldAlert :size="18" aria-hidden="true" /><div><strong>{{ currentBlocker.code }}</strong><p>{{ currentBlocker.summary }}</p><small>{{ currentBlocker.layer || "runtime" }}</small></div></div>
|
||||
<div v-else-if="caserun.error" class="caserun-error" role="alert"><XCircle :size="18" aria-hidden="true" /><span>{{ caserun.error }}</span></div>
|
||||
</section>
|
||||
@@ -211,7 +255,7 @@ function eventTime(value?: string): string {
|
||||
<div class="section-heading"><div><span class="caserun-kicker">事件流</span><h3>运行事件</h3></div><span class="timeline-note">{{ currentEvents.length }} 条事件</span></div>
|
||||
<div v-if="!currentEvents.length" class="event-empty">尚未读取事件。启动 CaseRun 或从深链载入 run 后,服务端事件会显示在这里。</div>
|
||||
<ol v-else class="event-list" id="caserun-event-text">
|
||||
<li v-for="(event, index) in currentEvents" :key="`${event.at}-${index}`"><time>{{ eventTime(event.at) }}</time><span class="event-dot" /><div><strong>{{ eventLabel(event) }}</strong><small>{{ event.payload ? JSON.stringify(event.payload) : "服务端状态事件" }}</small></div></li>
|
||||
<li v-for="(event, index) in currentEvents" :key="event.id ?? `${event.createdAt || event.at}-${index}`"><time>{{ eventTime(event.createdAt || event.at) }}</time><span class="event-dot" /><div><strong>{{ eventLabel(event) }}</strong><small>{{ eventDetail(event) }}</small></div></li>
|
||||
</ol>
|
||||
</section>
|
||||
</main>
|
||||
@@ -223,12 +267,40 @@ function eventTime(value?: string): string {
|
||||
</div>
|
||||
<div class="evidence-body" id="caserun-aggregate">
|
||||
<template v-if="selectedTab === 'aggregate'">
|
||||
<div class="evidence-state"><span class="state-label">汇总状态</span><strong id="caserun-aggregate-status">{{ statusLabel(aggregate?.status || "pending") }}</strong><small id="caserun-aggregate-run-id">{{ aggregate?.runId || "等待 CaseRun" }}</small></div>
|
||||
<div class="hash-row"><span>SHA256</span><button type="button" title="复制汇总 SHA" aria-label="复制汇总 SHA" @click="copy(aggregate?.sha256 || '')"><code id="caserun-aggregate-sha">{{ aggregate?.sha256 || "-" }}</code><Clipboard :size="14" aria-hidden="true" /></button></div>
|
||||
<dl class="evidence-facts"><div><dt>摘要</dt><dd>{{ aggregate?.summary ? "已读取" : "-" }}</dd></div><div><dt>证据</dt><dd>{{ aggregate?.evidence ? "已读取" : "-" }}</dd></div><div><dt>事实来源</dt><dd>{{ aggregate?.sourceAuthority?.mode || currentRun?.sourceAuthority?.mode || fixtureMode }}</dd></div></dl>
|
||||
<div class="result-banner" :class="{ complete: terminal }">
|
||||
<CheckCircle2 :size="22" aria-hidden="true" />
|
||||
<div><span class="state-label">运行结果</span><strong id="caserun-aggregate-status">{{ statusLabel(aggregate?.status || currentRun?.status || "pending") }}</strong><small id="caserun-aggregate-run-id">{{ aggregate?.runId || currentRun?.runId || "等待 CaseRun" }}</small></div>
|
||||
</div>
|
||||
<dl class="evidence-facts result-facts">
|
||||
<div><dt>运行模式</dt><dd>{{ modeLabel(fixtureMode) }}</dd></div>
|
||||
<div><dt>执行耗时</dt><dd>{{ duration }}</dd></div>
|
||||
<div><dt>证据状态</dt><dd id="caserun-evidence-status">{{ statusLabel(evidenceStatus) }}</dd></div>
|
||||
<div><dt>产物数量</dt><dd>{{ artifactCount }} 项</dd></div>
|
||||
<div><dt>需要硬件</dt><dd>{{ resultRun?.hardwareRequired === false ? "否" : resultRun?.hardwareRequired === true ? "是" : "-" }}</dd></div>
|
||||
</dl>
|
||||
<div v-if="aggregate?.sha256" class="hash-row"><span>汇总 SHA256</span><button type="button" title="复制汇总 SHA" aria-label="复制汇总 SHA" @click="copy(aggregate.sha256 || '')"><code id="caserun-aggregate-sha">{{ aggregate.sha256 }}</code><Clipboard :size="14" aria-hidden="true" /></button></div>
|
||||
</template>
|
||||
<template v-else-if="selectedTab === 'artifacts'">
|
||||
<div class="evidence-section-heading"><span class="state-label">产物清单</span><strong>{{ artifactCount }} 项</strong></div>
|
||||
<div v-if="artifacts.length" class="artifact-list">
|
||||
<article v-for="artifact in artifacts" :key="artifactName(artifact)" class="artifact-row caserun-artifact-row">
|
||||
<FileCheck2 :size="17" aria-hidden="true" />
|
||||
<div><strong>{{ artifactName(artifact) }}</strong><small>{{ artifactSize(artifact.bytes) }}</small><code>{{ artifact.sha256 || "未提供 SHA256" }}</code></div>
|
||||
<button v-if="artifact.sha256" type="button" title="复制产物 SHA256" aria-label="复制产物 SHA256" @click="copy(artifact.sha256)"><Clipboard :size="14" aria-hidden="true" /></button>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else class="evidence-empty">当前运行尚未登记产物。</div>
|
||||
</template>
|
||||
<template v-else-if="selectedTab === 'workflow'">
|
||||
<div class="workflow-identity"><GitBranch :size="20" aria-hidden="true" /><div><span class="state-label">Temporal Workflow</span><strong id="caserun-workflow-id">{{ currentRun?.workflowId || "暂无 workflowId" }}</strong></div></div>
|
||||
<dl class="identity-list">
|
||||
<div><dt>workflowRunId</dt><dd>{{ currentRun?.workflowRunId || "-" }}</dd></div>
|
||||
<div><dt>runId</dt><dd>{{ currentRun?.runId || "-" }}</dd></div>
|
||||
<div><dt>创建时间</dt><dd>{{ currentRun?.createdAt || "-" }}</dd></div>
|
||||
<div><dt>更新时间</dt><dd>{{ currentRun?.updatedAt || "-" }}</dd></div>
|
||||
<div v-if="currentRun?.references?.trace?.traceId"><dt>traceId</dt><dd>{{ currentRun.references.trace.traceId }}</dd></div>
|
||||
</dl>
|
||||
</template>
|
||||
<template v-else-if="selectedTab === 'artifacts'"><div class="evidence-state"><span class="state-label">产物清单</span><strong>{{ currentRun?.references?.manifest?.count ?? 0 }} 项</strong><small>{{ currentRun?.references?.manifest?.href || "暂无清单引用" }}</small></div></template>
|
||||
<template v-else-if="selectedTab === 'trace'"><div class="evidence-state"><span class="state-label">链路标识</span><strong>{{ currentRun?.references?.trace?.traceId || "暂无链路" }}</strong><small>只读链路引用,执行证据仍以 CaseRun API 为准。</small></div></template>
|
||||
<template v-else><div class="evidence-state"><span class="state-label">HWPOD 操作</span><strong>{{ currentRun?.references?.hwpod?.hwpodId || "暂无 HWPOD" }}</strong><small>节点 {{ currentRun?.references?.hwpod?.nodeId || "-" }}</small></div></template>
|
||||
</div>
|
||||
<div class="inspector-footer"><FileCode2 :size="15" aria-hidden="true" /><span>证据来自当前 CaseRun API,不在客户端合成终态。</span><ExternalLink :size="14" aria-hidden="true" /></div>
|
||||
|
||||
Reference in New Issue
Block a user