From b48de743b6f32193a53bcda553f4bdda777b61b3 Mon Sep 17 00:00:00 2001 From: kitos Date: Mon, 13 Jul 2026 12:46:46 +0200 Subject: [PATCH] fix(tests): keep free-text round fields in place on reopen, only blank verdicts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only the per-round verdict/timing fields (attack_success, execution start/end for red; detection_result, containment_result, detection/ containment time for blue) get cleared on reopen — the operator must answer those fresh for each round, and the prior round's answer stays visible via round_history + the archived Phase Timeline card. Free-text fields (procedure_text, tool_used, red_summary, blue_summary) are no longer cleared: the operator edits them in place for the new round instead of retyping everything from scratch. --- backend/app/services/test_workflow_service.py | 19 +++++++++++-------- backend/tests/test_workflow.py | 19 ++++++++++++------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/backend/app/services/test_workflow_service.py b/backend/app/services/test_workflow_service.py index 83f8465..cad8a08 100644 --- a/backend/app/services/test_workflow_service.py +++ b/backend/app/services/test_workflow_service.py @@ -405,12 +405,14 @@ def reopen_red_review(db: Session, test: Test, user: User, notes: str) -> Test: test.red_review_at = datetime.utcnow() test.red_review_notes = notes.strip() test.red_round_number = (test.red_round_number or 1) + 1 - # New sheet — the operator fills these in fresh for this round; the - # values that were just archived stay visible via round_history. - test.procedure_text = None - test.tool_used = None + # Only the per-round verdict/timing fields get a blank slate — the + # operator must explicitly answer "was THIS attempt successful" and + # "when did THIS attempt run" rather than silently inheriting round 1's + # answer. The old values aren't lost: they're in round_history and + # shown on the archived Phase Timeline card. Free-text fields + # (procedure, tool, summary) are NOT cleared — the operator edits them + # in place for the new round instead of retyping from scratch. test.attack_success = None - test.red_summary = None test.execution_start_time = None test.execution_end_time = None db.flush() @@ -640,13 +642,14 @@ def reopen_blue_review(db: Session, test: Test, user: User, notes: str) -> Test: test.blue_review_at = datetime.utcnow() test.blue_review_notes = notes.strip() test.blue_round_number = (test.blue_round_number or 1) + 1 - # New sheet for this round; the archived values above remain visible - # via round_history. + # Same split as the red side: verdict/timing fields get a blank slate + # for this round (old values live on in round_history + the archived + # Phase Timeline card), but blue_summary is free text the operator + # edits in place rather than retyping. test.detection_result = None test.containment_result = None test.detection_time = None test.containment_time = None - test.blue_summary = None db.flush() log_action( diff --git a/backend/tests/test_workflow.py b/backend/tests/test_workflow.py index 488eef3..558031f 100644 --- a/backend/tests/test_workflow.py +++ b/backend/tests/test_workflow.py @@ -740,9 +740,11 @@ class TestReviewDecisions: @patch("app.services.test_workflow_service.log_action") def test_reopen_red_review_archives_round_and_resets_fields(self, mock_log): - """Reopening must not erase what Red already submitted — it archives - the round (visible via round_history) and only then blanks the live - fields for a fresh attempt, bumping the round counter.""" + """Reopening must not erase what Red already submitted. The round is + archived (visible via round_history) and only the per-round verdict + fields (attack_success, execution start/end) are blanked for a fresh + answer — free-text fields (procedure, tool, summary) stay in place + for the operator to edit, not retype from scratch.""" from app.models.test_round_history import TestRoundHistory test = _make_test( @@ -766,10 +768,12 @@ class TestReviewDecisions: assert archived[0].red_summary == "Got a shell." assert archived[0].review_notes == "incomplete evidence" - assert test.procedure_text is None - assert test.tool_used is None + assert test.procedure_text == "ran mimikatz" + assert test.tool_used == "mimikatz" + assert test.red_summary == "Got a shell." assert test.attack_success is None - assert test.red_summary is None + assert test.execution_start_time is None + assert test.execution_end_time is None assert test.red_round_number == 2 @patch("app.services.test_workflow_service.log_action") @@ -797,7 +801,8 @@ class TestReviewDecisions: assert archived[0].review_notes == "missing containment steps" assert test.detection_result is None - assert test.blue_summary is None + assert test.containment_result is None + assert test.blue_summary == "Caught it via EDR." assert test.blue_round_number == 2 @patch("app.services.test_workflow_service.log_action")