fix: 优化CaseRun列表与恢复状态
This commit is contained in:
@@ -89,6 +89,16 @@ export type CaseRunListItem = {
|
||||
launchContext: CaseRunLaunchContext | null;
|
||||
};
|
||||
|
||||
export type CaseRunListRecord = Pick<CaseRunRecord,
|
||||
"runId" | "caseId" | "workflowId" | "workflowRunId" | "status" | "stage" |
|
||||
"terminal" | "caseAuthority" | "launchContext" | "createdAt" | "updatedAt"
|
||||
> & {
|
||||
artifactCount?: number;
|
||||
evidenceStatus?: string | null;
|
||||
artifactManifestSha256?: string | null;
|
||||
result?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
export type ActivityResult = {
|
||||
runId: string;
|
||||
activityName: string;
|
||||
@@ -100,7 +110,7 @@ export interface HarnessRLRegistry {
|
||||
ensureSchema(): Promise<void>;
|
||||
createRun(input: StartCaseRunInput): Promise<CaseRunRecord>;
|
||||
getRun(runId: string): Promise<CaseRunRecord | null>;
|
||||
listRuns(input: ListCaseRunsInput): Promise<CaseRunRecord[]>;
|
||||
listRuns(input: ListCaseRunsInput): Promise<CaseRunListRecord[]>;
|
||||
updateRun(runId: string, patch: Partial<Pick<CaseRunRecord, "workflowRunId" | "status" | "stage" | "result" | "error" | "blocker">>): Promise<CaseRunRecord>;
|
||||
appendEvent(runId: string, status: CaseRunStatus, stage: string, payload?: Record<string, unknown>): Promise<CaseRunEvent>;
|
||||
listEvents(runId: string): Promise<CaseRunEvent[]>;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { caseRunAgentFailure, caseRunBuildFailure, caseRunWorkflowFailure } from
|
||||
import type { ActivityResult, CaseRunEvent, CaseRunRecord, CaseRunStatus, HarnessRLRegistry, HarnessRLTemporalGateway, StartCaseRunInput } from "./contracts.ts";
|
||||
import { terminalStatus } from "./contracts.ts";
|
||||
import { createHarnessRLHttpApp } from "./http.ts";
|
||||
import { PostgresHarnessRLRegistry } from "./registry.ts";
|
||||
import { createHarnessRLService } from "./service.ts";
|
||||
import { recoverAgentTaskStage } from "../../tools/src/hwlab-caserun-runtime.ts";
|
||||
|
||||
@@ -115,6 +116,33 @@ test("run history lists shared registry results newest first with an opaque curs
|
||||
assert.equal((await invalidResponse.json() as any).error.code, "invalid_status");
|
||||
});
|
||||
|
||||
test("PostgreSQL run history projects list facts without loading the full result JSONB", async () => {
|
||||
const queries: Array<{ text: string; values: unknown[] }> = [];
|
||||
const registry = Object.create(PostgresHarnessRLRegistry.prototype) as PostgresHarnessRLRegistry;
|
||||
(registry as any).ready = Promise.resolve();
|
||||
(registry as any).pool = {
|
||||
async query(text: string, values: unknown[]) {
|
||||
queries.push({ text, values });
|
||||
return { rows: [{
|
||||
run_id: "projected-run", case_id: "arm2d", workflow_id: "workflow-1", workflow_run_id: "workflow-run-1",
|
||||
status: "completed", stage: "completed", case_authority: { repository: "repo", ref: "main", commit: "a".repeat(40), definitionPath: "case.yaml", definitionSha256: "b".repeat(64) },
|
||||
launch_context: {}, artifact_count: 3, evidence_status: "recorded", artifact_manifest_sha256: "c".repeat(64),
|
||||
created_at: "2026-07-25T10:00:00.000Z", updated_at: "2026-07-25T10:01:00.000Z"
|
||||
}] };
|
||||
}
|
||||
};
|
||||
|
||||
const [record] = await registry.listRuns({ limit: 31 });
|
||||
|
||||
assert.doesNotMatch(queries[0].text, /SELECT\s+\*/iu);
|
||||
assert.doesNotMatch(queries[0].text, /\bresult\s*(?:,|FROM)/iu);
|
||||
assert.match(queries[0].text, /jsonb_array_length/iu);
|
||||
assert.equal(record.artifactCount, 3);
|
||||
assert.equal(record.evidenceStatus, "recorded");
|
||||
assert.equal(record.artifactManifestSha256, "c".repeat(64));
|
||||
assert.equal("result" in record, false);
|
||||
});
|
||||
|
||||
test("analysis read model loads the current registry revision without runtime work", async () => {
|
||||
const fixture = await softwareSmokeFixture();
|
||||
const registry = new MemoryRegistry();
|
||||
|
||||
@@ -5,7 +5,7 @@ import pg from "pg";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { buildPostgresPoolConfig, postgresSslModeFromConnectionString } from "../db/runtime-store-core.ts";
|
||||
import type { ActivityResult, CaseRunEvent, CaseRunRecord, CaseRunStatus, HarnessRLRegistry, ListCaseRunsInput, StartCaseRunInput } from "./contracts.ts";
|
||||
import type { ActivityResult, CaseRunEvent, CaseRunListRecord, CaseRunRecord, CaseRunStatus, HarnessRLRegistry, ListCaseRunsInput, StartCaseRunInput } from "./contracts.ts";
|
||||
import { terminalStatus } from "./contracts.ts";
|
||||
|
||||
const { Pool } = pg;
|
||||
@@ -76,11 +76,24 @@ export class PostgresHarnessRLRegistry implements HarnessRLRegistry {
|
||||
}
|
||||
values.push(input.limit);
|
||||
const result = await this.pool.query(
|
||||
`SELECT * FROM harnessrl_runs${where.length ? ` WHERE ${where.join(" AND ")}` : ""}
|
||||
`SELECT run_id,case_id,workflow_id,workflow_run_id,status,stage,case_authority,launch_context,
|
||||
created_at,updated_at,
|
||||
CASE
|
||||
WHEN jsonb_typeof(result #> '{summary,artifactCount}') = 'number'
|
||||
THEN (result #>> '{summary,artifactCount}')::integer
|
||||
WHEN jsonb_typeof(result #> '{summary,artifacts}') = 'array'
|
||||
THEN jsonb_array_length(result #> '{summary,artifacts}')
|
||||
WHEN jsonb_typeof(result #> '{evidence,artifacts}') = 'array'
|
||||
THEN jsonb_array_length(result #> '{evidence,artifacts}')
|
||||
ELSE 0
|
||||
END AS artifact_count,
|
||||
result #>> '{evidence,status}' AS evidence_status,
|
||||
result ->> 'artifactManifestSha256' AS artifact_manifest_sha256
|
||||
FROM harnessrl_runs${where.length ? ` WHERE ${where.join(" AND ")}` : ""}
|
||||
ORDER BY created_at DESC,run_id DESC LIMIT $${values.length}`,
|
||||
values
|
||||
);
|
||||
return result.rows.map(runRow);
|
||||
return result.rows.map(runListRow);
|
||||
}
|
||||
|
||||
async updateRun(runId: string, patch: Partial<Pick<CaseRunRecord, "workflowRunId" | "status" | "stage" | "result" | "error" | "blocker">>) {
|
||||
@@ -154,6 +167,17 @@ function runRow(row: any): CaseRunRecord {
|
||||
};
|
||||
}
|
||||
|
||||
function runListRow(row: any): CaseRunListRecord {
|
||||
const status = row.status as CaseRunStatus;
|
||||
return {
|
||||
runId: row.run_id, caseId: row.case_id, workflowId: row.workflow_id, workflowRunId: row.workflow_run_id,
|
||||
status, stage: row.stage, terminal: terminalStatus(status), caseAuthority: row.case_authority,
|
||||
launchContext: launchContextValue(row.launch_context), artifactCount: Number(row.artifact_count ?? 0),
|
||||
evidenceStatus: row.evidence_status ?? null, artifactManifestSha256: row.artifact_manifest_sha256 ?? null,
|
||||
createdAt: new Date(row.created_at).toISOString(), updatedAt: new Date(row.updated_at).toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
function launchContextValue(value: any) {
|
||||
return value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0 ? value : null;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Buffer } from "node:buffer";
|
||||
import { mkdir, readdir, stat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import type { CaseRunLaunchContext, CaseRunListItem, CaseRunRecord, CaseRunStatus, HarnessRLRegistry, HarnessRLTemporalGateway } from "./contracts.ts";
|
||||
import type { CaseRunLaunchContext, CaseRunListItem, CaseRunListRecord, CaseRunRecord, CaseRunStatus, HarnessRLRegistry, HarnessRLTemporalGateway } from "./contracts.ts";
|
||||
import type { CaseAuthorityResolver } from "./case-authority.ts";
|
||||
import { readCurrentCaseRunAnalysis } from "./analysis-read-model.ts";
|
||||
import { readCaseRunCaptureReview } from "../../tools/src/hwlab-caserun-diagnostics.ts";
|
||||
@@ -122,7 +122,7 @@ async function readCaptureReview(record: CaseRunRecord) {
|
||||
}
|
||||
}
|
||||
|
||||
function runListItem(record: CaseRunRecord): CaseRunListItem {
|
||||
function runListItem(record: CaseRunListRecord): CaseRunListItem {
|
||||
const summary = objectValue(record.result?.summary);
|
||||
const evidence = objectValue(record.result?.evidence);
|
||||
const artifacts = Array.isArray(summary?.artifacts) ? summary.artifacts : Array.isArray(evidence?.artifacts) ? evidence.artifacts : [];
|
||||
@@ -136,9 +136,9 @@ function runListItem(record: CaseRunRecord): CaseRunListItem {
|
||||
terminal: record.terminal,
|
||||
createdAt: record.createdAt,
|
||||
updatedAt: record.updatedAt,
|
||||
artifactCount: finiteNumber(summary?.artifactCount) ?? artifacts.length,
|
||||
evidenceStatus: stringValue(evidence?.status),
|
||||
artifactManifestSha256: stringValue(record.result?.artifactManifestSha256),
|
||||
artifactCount: finiteNumber(record.artifactCount) ?? finiteNumber(summary?.artifactCount) ?? artifacts.length,
|
||||
evidenceStatus: record.evidenceStatus ?? stringValue(evidence?.status),
|
||||
artifactManifestSha256: record.artifactManifestSha256 ?? stringValue(record.result?.artifactManifestSha256),
|
||||
caseAuthority: {
|
||||
repository: record.caseAuthority.repository,
|
||||
ref: record.caseAuthority.ref,
|
||||
|
||||
@@ -113,3 +113,63 @@ test("persistent analysis failure stays isolated from the loaded run", async ()
|
||||
expect(store.analysisError).toBe("HTTP 500");
|
||||
expect(store.error).toBeNull();
|
||||
});
|
||||
|
||||
test("aggregate retries one timeout while keeping the detail region loading", async () => {
|
||||
const firstAggregate = deferred<{ ok: false; status: number; error: string }>();
|
||||
const secondAggregate = deferred<{ ok: true; data: { runId: string; status: string; sha256: string; captureReview: { trace: { rows: never[] } } } }>();
|
||||
runRequest.mockResolvedValue({ ok: true, data: { runId: "run-1", caseId: "arm2d", status: "completed", stage: "completed", terminal: true } });
|
||||
eventsRequest.mockResolvedValue({ ok: true, data: { runId: "run-1", events: [] } });
|
||||
aggregateRequest
|
||||
.mockImplementationOnce(() => firstAggregate.promise)
|
||||
.mockImplementationOnce(() => secondAggregate.promise);
|
||||
const store = useCaseRunStore();
|
||||
|
||||
const refreshing = store.refreshRun("run-1");
|
||||
await vi.waitFor(() => expect(store.runLoading).toBe(true));
|
||||
firstAggregate.resolve({ ok: false, status: 504, error: "CaseRun aggregate timed out" });
|
||||
await vi.waitFor(() => expect(aggregateRequest).toHaveBeenCalledTimes(2));
|
||||
expect(store.detailsLoading).toBe(true);
|
||||
secondAggregate.resolve({ ok: true, data: { runId: "run-1", status: "completed", sha256: "capture-sha", captureReview: { trace: { rows: [] } } } });
|
||||
await refreshing;
|
||||
|
||||
expect(store.aggregate?.sha256).toBe("capture-sha");
|
||||
expect(store.detailsError).toBeNull();
|
||||
expect(store.runLoading).toBe(false);
|
||||
expect(store.detailsLoading).toBe(false);
|
||||
});
|
||||
|
||||
test("persistent aggregate timeout is visible without clearing the loaded run", async () => {
|
||||
runRequest.mockResolvedValue({ ok: true, data: { runId: "run-1", caseId: "arm2d", status: "completed", stage: "completed", terminal: true } });
|
||||
eventsRequest.mockResolvedValue({ ok: true, data: { runId: "run-1", events: [] } });
|
||||
aggregateRequest.mockResolvedValue({ ok: false, status: 504, error: "CaseRun aggregate timed out" });
|
||||
const store = useCaseRunStore();
|
||||
|
||||
await store.refreshRun("run-1");
|
||||
|
||||
expect(aggregateRequest).toHaveBeenCalledTimes(2);
|
||||
expect(store.currentRun?.runId).toBe("run-1");
|
||||
expect(store.aggregate).toBeNull();
|
||||
expect(store.detailsError).toBe("无法读取运行审查数据:请求超时,请重试。");
|
||||
expect(store.error).toBeNull();
|
||||
});
|
||||
|
||||
test("background polling does not replace a stable detail view with first-load state", async () => {
|
||||
runRequest.mockResolvedValue({ ok: true, data: { runId: "run-1", caseId: "arm2d", status: "running", stage: "agent-running", terminal: false } });
|
||||
eventsRequest.mockResolvedValue({ ok: true, data: { runId: "run-1", events: [] } });
|
||||
aggregateRequest.mockResolvedValue({ ok: true, data: { runId: "run-1", status: "running", sha256: "capture-sha" } });
|
||||
const store = useCaseRunStore();
|
||||
await store.refreshRun("run-1");
|
||||
const pendingRun = deferred<{ ok: true; data: { runId: string; caseId: string; status: string; stage: string; terminal: boolean } }>();
|
||||
runRequest.mockImplementationOnce(() => pendingRun.promise);
|
||||
|
||||
const polling = store.refreshRun(undefined, { foreground: false });
|
||||
expect(store.runLoading).toBe(false);
|
||||
pendingRun.resolve({ ok: true, data: { runId: "run-1", caseId: "arm2d", status: "running", stage: "agent-running", terminal: false } });
|
||||
await polling;
|
||||
});
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
const promise = new Promise<T>((done) => { resolve = done; });
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ export const useCaseRunStore = defineStore("caserun", () => {
|
||||
const starting = ref(false);
|
||||
const runsLoading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const casesError = ref<string | null>(null);
|
||||
const runsError = ref<string | null>(null);
|
||||
const detailsError = ref<string | null>(null);
|
||||
const analysisError = ref<string | null>(null);
|
||||
const selectedCaseId = computed(() => currentRun.value?.caseId ?? cases.value[0]?.caseId ?? "");
|
||||
const shouldPoll = computed(() => Boolean(currentRun.value?.runId && currentRun.value.terminal === false));
|
||||
@@ -25,10 +27,10 @@ export const useCaseRunStore = defineStore("caserun", () => {
|
||||
|
||||
async function refreshCases(): Promise<void> {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
casesError.value = null;
|
||||
const result = await caserunAPI.cases();
|
||||
cases.value = result.data?.cases ?? [];
|
||||
error.value = result.ok ? null : result.error ?? "无法读取 CaseRun 用例";
|
||||
casesError.value = result.ok ? null : visibleRequestError(result.error, "无法读取可执行用例");
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -54,6 +56,7 @@ export const useCaseRunStore = defineStore("caserun", () => {
|
||||
events.value = [];
|
||||
aggregate.value = null;
|
||||
analysis.value = null;
|
||||
detailsError.value = null;
|
||||
const result = await caserunAPI.startRun(caseId);
|
||||
starting.value = false;
|
||||
if (!result.ok || !result.data) {
|
||||
@@ -66,11 +69,12 @@ export const useCaseRunStore = defineStore("caserun", () => {
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async function refreshRun(runId = currentRun.value?.runId ?? ""): Promise<boolean> {
|
||||
async function refreshRun(runId = currentRun.value?.runId ?? "", options: { foreground?: boolean } = {}): Promise<boolean> {
|
||||
if (!runId) return false;
|
||||
const requestId = ++runRequestId;
|
||||
const switchingRun = currentRun.value?.runId !== runId;
|
||||
if (switchingRun) runLoading.value = true;
|
||||
const foreground = switchingRun || options.foreground !== false;
|
||||
if (foreground) runLoading.value = true;
|
||||
const result = await caserunAPI.run(runId);
|
||||
if (requestId !== runRequestId) return false;
|
||||
if (!result.ok || !result.data) {
|
||||
@@ -80,6 +84,7 @@ export const useCaseRunStore = defineStore("caserun", () => {
|
||||
events.value = [];
|
||||
aggregate.value = null;
|
||||
analysis.value = null;
|
||||
detailsError.value = null;
|
||||
}
|
||||
runLoading.value = false;
|
||||
return false;
|
||||
@@ -89,6 +94,7 @@ export const useCaseRunStore = defineStore("caserun", () => {
|
||||
events.value = [];
|
||||
aggregate.value = null;
|
||||
analysis.value = null;
|
||||
detailsError.value = null;
|
||||
}
|
||||
currentRun.value = result.data;
|
||||
runLoading.value = false;
|
||||
@@ -107,14 +113,24 @@ export const useCaseRunStore = defineStore("caserun", () => {
|
||||
|
||||
async function refreshAggregate(runId = currentRun.value?.runId ?? ""): Promise<void> {
|
||||
if (!runId) return;
|
||||
const result = await caserunAPI.aggregate(runId);
|
||||
if (result.ok && result.data?.runId === runId && currentRun.value?.runId === runId) aggregate.value = result.data;
|
||||
let result = await caserunAPI.aggregate(runId);
|
||||
if (currentRun.value?.runId === runId && isTransientRequestFailure(result)) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
if (currentRun.value?.runId !== runId) return;
|
||||
result = await caserunAPI.aggregate(runId);
|
||||
}
|
||||
if (result.ok && result.data?.runId === runId && currentRun.value?.runId === runId) {
|
||||
aggregate.value = result.data;
|
||||
detailsError.value = null;
|
||||
} else if (currentRun.value?.runId === runId) {
|
||||
detailsError.value = visibleRequestError(result.error, "无法读取运行审查数据");
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAnalysis(runId = currentRun.value?.runId ?? ""): Promise<void> {
|
||||
if (!runId) return;
|
||||
let result = await caserunAPI.analysis(runId);
|
||||
if (currentRun.value?.runId === runId && isTransientAnalysisFailure(result)) {
|
||||
if (currentRun.value?.runId === runId && isTransientRequestFailure(result)) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
if (currentRun.value?.runId !== runId) return;
|
||||
result = await caserunAPI.analysis(runId);
|
||||
@@ -134,14 +150,14 @@ export const useCaseRunStore = defineStore("caserun", () => {
|
||||
return message || fallback;
|
||||
}
|
||||
|
||||
function isTransientAnalysisFailure(result: { ok: boolean; status?: number; error?: unknown }): boolean {
|
||||
function isTransientRequestFailure(result: { ok: boolean; status?: number; error?: unknown }): boolean {
|
||||
return !result.ok && ([500, 502, 503, 504].includes(result.status ?? 0)
|
||||
|| /socket hang up|connection terminated|econnreset|fetch failed/iu.test(String(result.error ?? "")));
|
||||
|| /timeout|timed out|socket hang up|connection terminated|econnreset|fetch failed/iu.test(String(result.error ?? "")));
|
||||
}
|
||||
|
||||
return {
|
||||
cases, runs, nextRunsCursor, currentRun, events, aggregate, analysis,
|
||||
loading, runLoading, detailsLoading, runsLoading, starting, error, runsError, analysisError,
|
||||
loading, runLoading, detailsLoading, runsLoading, starting, error, casesError, runsError, detailsError, analysisError,
|
||||
selectedCaseId, shouldPoll,
|
||||
refreshCases, refreshRuns, startRun, refreshRun, refreshEvents, refreshAggregate, refreshAnalysis
|
||||
};
|
||||
|
||||
@@ -54,6 +54,8 @@ const analysisReport = computed(() => analysis.value?.report ?? null);
|
||||
const analysisEvidence = computed(() => analysisReport.value?.evidence ?? null);
|
||||
const review = computed(() => analysisReport.value?.review ?? aggregate.value?.captureReview ?? null);
|
||||
const reviewSource = computed<"analysis" | "capture" | null>(() => analysisReport.value?.review ? "analysis" : aggregate.value?.captureReview ? "capture" : null);
|
||||
const workspaceLoading = computed(() => caserun.runLoading || caserun.detailsLoading
|
||||
|| (caserun.runsLoading && !caserun.runs.length && !review.value));
|
||||
const analysisScoring = computed(() => analysisReport.value?.scoring ?? null);
|
||||
const analysisFindings = computed(() => analysisReport.value?.findings ?? []);
|
||||
const resultSummary = computed(() => aggregate.value?.result?.summary ?? aggregate.value?.summary ?? currentRun.value?.result?.summary ?? null);
|
||||
@@ -106,7 +108,7 @@ onMounted(async () => {
|
||||
await router.replace({ name: "CaseRunDetail", params: { runId: latestRun.runId }, query: { case: latestRun.caseId } });
|
||||
}
|
||||
pollTimer = setInterval(() => {
|
||||
if (caserun.shouldPoll) void caserun.refreshRun();
|
||||
if (caserun.shouldPoll) void caserun.refreshRun(undefined, { foreground: false });
|
||||
if (caserun.runsError) void caserun.refreshRuns();
|
||||
}, 5000);
|
||||
});
|
||||
@@ -123,8 +125,11 @@ async function startRun(): Promise<void> {
|
||||
}
|
||||
|
||||
async function refresh(): Promise<void> {
|
||||
await Promise.all([caserun.refreshCases(), caserun.refreshRuns()]);
|
||||
if (runId.value) await caserun.refreshRun(runId.value);
|
||||
await Promise.all([
|
||||
caserun.refreshCases(),
|
||||
caserun.refreshRuns(),
|
||||
runId.value ? caserun.refreshRun(runId.value) : Promise.resolve(false),
|
||||
]);
|
||||
}
|
||||
|
||||
async function selectCase(caseId: string): Promise<void> {
|
||||
@@ -213,13 +218,14 @@ function artifactSize(bytes?: number): string { return Number.isFinite(bytes) ?
|
||||
<code>{{ item.runId }}</code>
|
||||
<small>{{ stageLabel(item.stage) }}<time>{{ historyTime(item.createdAt) }}</time></small>
|
||||
</button>
|
||||
<p v-if="!caserun.runs.length && !caserun.runsLoading" class="pane-empty">{{ caserun.runsError || "共享 registry 中暂无运行。" }}</p>
|
||||
<p v-if="!caserun.runs.length && !caserun.runsLoading" class="pane-empty" :class="{ 'pane-error': caserun.runsError }">{{ caserun.runsError || "共享 registry 中暂无运行。" }}</p>
|
||||
<button v-if="caserun.nextRunsCursor" class="load-more" type="button" :disabled="caserun.runsLoading" @click="caserun.refreshRuns({ append: true })">{{ caserun.runsLoading ? "读取中" : "加载更早运行" }}</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="case-list">
|
||||
<LoadingState v-if="caserun.loading && !caserun.cases.length" class="region-loading" label="正在加载可执行用例" compact />
|
||||
<p v-else-if="caserun.casesError && !caserun.cases.length" class="pane-empty pane-error" role="alert">{{ caserun.casesError }}</p>
|
||||
<button v-for="item in caserun.cases" :key="item.caseId" class="case-row" :data-selected="item.caseId === selectedCaseId" type="button" @click="selectCase(item.caseId)"><Cpu :size="15" aria-hidden="true" /><span><strong>{{ item.title || item.caseId }}</strong><small>{{ item.caseId }}</small></span><Check v-if="item.available !== false" :size="14" aria-hidden="true" /></button>
|
||||
</div>
|
||||
<button id="caserun-start" class="start-run" type="button" :disabled="!selectedCaseId || caserun.starting" @click="startRun"><TerminalSquare :size="16" aria-hidden="true" />{{ caserun.starting ? "提交中" : "启动 CaseRun" }}</button>
|
||||
@@ -236,8 +242,8 @@ function artifactSize(bytes?: number): string { return Number.isFinite(bytes) ?
|
||||
<button v-if="runId" class="icon-button" type="button" title="复制 runId" aria-label="复制 runId" @click="copy(runId)"><Clipboard :size="15" aria-hidden="true" /></button>
|
||||
</div>
|
||||
</header>
|
||||
<LoadingState v-if="caserun.runLoading" class="main-region-loading" label="正在加载所选运行" />
|
||||
<CaseRunReviewPane v-else :review="review" :review-source="reviewSource" :scoring="analysisScoring" :run-id="currentRun?.runId" :metadata="overviewMetadata" :hwpod-evidence="resultEvidence?.hwpod" :loading="caserun.detailsLoading" :error="caserun.error" />
|
||||
<LoadingState v-if="workspaceLoading" class="main-region-loading" :label="runId ? '正在恢复运行审查数据' : '正在加载最近运行'" />
|
||||
<CaseRunReviewPane v-else :review="review" :review-source="reviewSource" :scoring="analysisScoring" :run-id="currentRun?.runId" :metadata="overviewMetadata" :hwpod-evidence="resultEvidence?.hwpod" :error="caserun.error || caserun.detailsError" />
|
||||
</main>
|
||||
|
||||
<aside class="caserun-inspector" aria-label="CaseRun 证据检查器">
|
||||
|
||||
Reference in New Issue
Block a user