feat(techniques): show test status on template cards
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

Each template card in 'Available Test Templates' now shows contextual
status derived from technique.tests (already loaded):

- Active test (draft/executing/evaluating/in_review):
    blue 'Executing / In Review' badge + 'View test →' button
    (prevents blind duplicate creation)
- Validated / detected (fresh):
    green 'Detected' badge + dimmed 'Re-run' button
- Validated / not_detected or partial:
    red/yellow result badge + full 'Run This Test' button (re-run encouraged)
- Validated but stale (review_required=true):
    result badge + '⚠ Coverage may be stale' line
- No tests: normal 'Run This Test' button

No extra API calls — status is derived from the technique detail
already in-memory.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-29 10:59:39 +02:00
parent c467459b51
commit 727b8af7fd

View File

@@ -373,11 +373,70 @@ export default function TechniqueDetailPage() {
</div> </div>
) : templates.length > 0 ? ( ) : templates.length > 0 ? (
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
{templates.map((tpl) => ( {templates.map((tpl) => {
// ── Derive test status for this technique ──────────────
const allTests = (technique as any).tests ?? [];
// Any test currently in a non-terminal state
const ACTIVE_STATES: TestState[] = [
"draft", "red_executing", "blue_evaluating", "in_review",
];
const activeTest = allTests.find(
(t: { state: TestState }) => ACTIVE_STATES.includes(t.state)
) ?? null;
// Most recent validated test
const latestValidated: {
id: string;
detection_result: TestResult | null;
updated_at: string | null;
created_at: string;
} | null =
[...allTests]
.filter((t: { state: TestState }) => t.state === "validated")
.sort((a: { updated_at: string | null; created_at: string }, b: { updated_at: string | null; created_at: string }) =>
(b.updated_at || b.created_at).localeCompare(a.updated_at || a.created_at)
)[0] ?? null;
// Coverage is stale if the sync job raised the review flag
// (only meaningful when we already have a validated test)
const isStale = technique.review_required && !!latestValidated;
// ── Helpers ────────────────────────────────────────────
const ACTIVE_LABEL: Partial<Record<TestState, string>> = {
draft: "Draft",
red_executing: "Executing",
blue_evaluating: "Evaluating",
in_review: "In Review",
};
const detResult = latestValidated?.detection_result;
const detBadgeStyle =
detResult === "detected"
? "border-green-500/30 bg-green-500/10 text-green-400"
: detResult === "partially_detected"
? "border-yellow-500/30 bg-yellow-500/10 text-yellow-400"
: detResult === "not_detected"
? "border-red-500/30 bg-red-500/10 text-red-400"
: "border-gray-600/30 bg-gray-800/50 text-gray-400";
const detLabel =
detResult === "detected" ? "Detected"
: detResult === "partially_detected" ? "Partial"
: detResult === "not_detected" ? "Not detected"
: "Validated";
const needsReRun =
!activeTest &&
latestValidated &&
(detResult !== "detected" || isStale);
return (
<div <div
key={tpl.id} key={tpl.id}
className="flex items-center justify-between rounded-lg border border-gray-800 bg-gray-800/30 p-4 transition-colors hover:border-gray-700" className="flex items-start justify-between rounded-lg border border-gray-800 bg-gray-800/30 p-4 transition-colors hover:border-gray-700"
> >
{/* Left: template info */}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-gray-200">{tpl.name}</p> <p className="truncate text-sm font-medium text-gray-200">{tpl.name}</p>
<div className="mt-1 flex flex-wrap gap-1"> <div className="mt-1 flex flex-wrap gap-1">
@@ -396,15 +455,60 @@ export default function TechniqueDetailPage() {
)} )}
</div> </div>
</div> </div>
{/* Right: status + action */}
<div className="ml-3 flex shrink-0 flex-col items-end gap-2">
{/* ── Status badge ── */}
{activeTest && (
<span className="inline-flex items-center gap-1 rounded-full border border-blue-500/30 bg-blue-500/10 px-2 py-0.5 text-[10px] font-medium text-blue-400">
<Clock className="h-2.5 w-2.5" />
{ACTIVE_LABEL[activeTest.state as TestState] ?? activeTest.state.replace(/_/g, " ")}
</span>
)}
{!activeTest && latestValidated && (
<div className="flex flex-col items-end gap-0.5">
<span className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] font-medium ${detBadgeStyle}`}>
{detResult === "detected"
? <CheckCircle className="h-2.5 w-2.5" />
: <AlertTriangle className="h-2.5 w-2.5" />
}
{detLabel}
</span>
{isStale && (
<span className="text-[9px] font-medium text-amber-400">
Coverage may be stale
</span>
)}
</div>
)}
{/* ── Action button ── */}
{activeTest ? (
<button
onClick={() => navigate(`/tests/${activeTest.id}`)}
className="flex items-center gap-1 rounded-lg border border-blue-500/30 bg-blue-500/10 px-3 py-1.5 text-xs font-medium text-blue-400 hover:bg-blue-500/20 transition-colors"
>
<ExternalLink className="h-3.5 w-3.5" />
View test
</button>
) : (
<button <button
onClick={() => setTemplateFormId(tpl.id)} onClick={() => setTemplateFormId(tpl.id)}
className="ml-3 flex shrink-0 items-center gap-1 rounded-lg border border-cyan-500/30 bg-cyan-500/10 px-3 py-1.5 text-xs font-medium text-cyan-400 hover:bg-cyan-500/20 transition-colors" className={`flex items-center gap-1 rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors ${
latestValidated && detResult === "detected" && !isStale
? "border-gray-600/30 bg-gray-800/50 text-gray-400 hover:border-gray-600 hover:text-gray-300"
: "border-cyan-500/30 bg-cyan-500/10 text-cyan-400 hover:bg-cyan-500/20"
}`}
> >
<FlaskConical className="h-3.5 w-3.5" /> <FlaskConical className="h-3.5 w-3.5" />
Run This Test {needsReRun ? "Run This Test" : latestValidated ? "Re-run" : "Run This Test"}
</button> </button>
)}
</div> </div>
))} </div>
);
})}
</div> </div>
) : ( ) : (
<div className="flex flex-col items-center justify-center py-8 text-gray-400"> <div className="flex flex-col items-center justify-center py-8 text-gray-400">