feat(techniques): show test status on template cards
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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:
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user