feat(tests): reopen rejected test keeps all content + rejection notes
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Backend (reopen_test): - Preserve red/blue validation NOTES — teams see exactly what to fix without losing the rejection context. Previously both notes were cleared. - Preserve all content fields: procedure_text, tool_used, red_summary, attack_success, blue_summary, detection_result (already the case). - Preserve evidences (separate table, unaffected — already the case). - Still clear: validation statuses + who/when validated (fresh re-validation required). Phase timing reset so the new execution starts clean. Frontend: - Button label: 'Reopen Test' → 'Continue Test' (more accurate intent) - Dialog title: 'Reopen Test' → 'Continue Test' - Dialog message: replaces alarming 'workflow will be restarted / clear all' with accurate description of what is preserved vs reset - Toast: explains what to do next
This commit is contained in:
@@ -655,27 +655,33 @@ def get_retest_chain(db: Session, test_id) -> list[Test]:
|
|||||||
|
|
||||||
|
|
||||||
def reopen_test(db: Session, test: Test, user: User) -> Test:
|
def reopen_test(db: Session, test: Test, user: User) -> Test:
|
||||||
"""Move a ``rejected`` test back to ``draft``, clearing validation fields.
|
"""Move a ``rejected`` test back to ``draft`` for continued work.
|
||||||
|
|
||||||
This allows the teams to redo the test cycle.
|
Preserves all content (procedure, summaries, evidences) and — crucially —
|
||||||
|
the rejection NOTES so teams know what to fix without losing context.
|
||||||
|
|
||||||
|
Clears validation decisions (status, who validated, when) so leads must
|
||||||
|
re-validate the updated submission. Phase timing is reset so the timer
|
||||||
|
starts fresh for the new execution attempt.
|
||||||
"""
|
"""
|
||||||
test = transition_state(
|
test = transition_state(
|
||||||
db, test, TestState.draft, user,
|
db, test, TestState.draft, user,
|
||||||
action_name="reopen_test",
|
action_name="reopen_test",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Clear dual-validation fields
|
# Clear validation DECISIONS — leads must re-validate the new attempt.
|
||||||
|
# Rejection NOTES are intentionally kept so teams see what needs fixing.
|
||||||
test.red_validation_status = None
|
test.red_validation_status = None
|
||||||
test.red_validated_by = None
|
test.red_validated_by = None
|
||||||
test.red_validated_at = None
|
test.red_validated_at = None
|
||||||
test.red_validation_notes = None
|
# test.red_validation_notes → KEEP (rejection reason / clarification needed)
|
||||||
|
|
||||||
test.blue_validation_status = None
|
test.blue_validation_status = None
|
||||||
test.blue_validated_by = None
|
test.blue_validated_by = None
|
||||||
test.blue_validated_at = None
|
test.blue_validated_at = None
|
||||||
test.blue_validation_notes = None
|
# test.blue_validation_notes → KEEP (rejection reason / clarification needed)
|
||||||
|
|
||||||
# Clear phase timing fields
|
# Reset phase timing so the new execution starts fresh
|
||||||
test.red_started_at = None
|
test.red_started_at = None
|
||||||
test.blue_started_at = None
|
test.blue_started_at = None
|
||||||
test.blue_work_started_at = None
|
test.blue_work_started_at = None
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ export default function TestDetailHeader({
|
|||||||
className="flex items-center gap-1.5 rounded-lg border border-cyan-500/30 bg-cyan-900/20 px-4 py-2 text-sm font-medium text-cyan-400 hover:bg-cyan-900/40 transition-colors disabled:opacity-50"
|
className="flex items-center gap-1.5 rounded-lg border border-cyan-500/30 bg-cyan-900/20 px-4 py-2 text-sm font-medium text-cyan-400 hover:bg-cyan-900/40 transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{isTransitioning ? <Loader2 className="h-4 w-4 animate-spin" /> : <RotateCcw className="h-4 w-4" />}
|
{isTransitioning ? <Loader2 className="h-4 w-4 animate-spin" /> : <RotateCcw className="h-4 w-4" />}
|
||||||
Reopen Test
|
Continue Test
|
||||||
</button>,
|
</button>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ export default function TestDetailPage() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
invalidateAll();
|
invalidateAll();
|
||||||
setConfirmReopen(false);
|
setConfirmReopen(false);
|
||||||
showToast("Test reopened", "success");
|
showToast("Test returned to Draft — address the feedback and resubmit", "success");
|
||||||
},
|
},
|
||||||
onError: (err: unknown) => {
|
onError: (err: unknown) => {
|
||||||
setConfirmReopen(false);
|
setConfirmReopen(false);
|
||||||
@@ -587,9 +587,9 @@ export default function TestDetailPage() {
|
|||||||
{/* Confirm Reopen Dialog */}
|
{/* Confirm Reopen Dialog */}
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={confirmReopen}
|
open={confirmReopen}
|
||||||
title="Reopen Test"
|
title="Continue Test"
|
||||||
message="This will move the test back to Draft state and clear all validation decisions. The Red/Blue workflow will need to be restarted. Are you sure?"
|
message="The test will return to Draft so you can address the rejection feedback. All content is preserved — procedure, summaries, evidences and rejection notes. Only the validation decisions are cleared so leads can re-validate the updated submission."
|
||||||
confirmLabel="Reopen"
|
confirmLabel="Continue"
|
||||||
variant="warning"
|
variant="warning"
|
||||||
isLoading={reopenMutation.isPending}
|
isLoading={reopenMutation.isPending}
|
||||||
onConfirm={() => reopenMutation.mutate()}
|
onConfirm={() => reopenMutation.mutate()}
|
||||||
|
|||||||
Reference in New Issue
Block a user