diff --git a/backend/app/services/mission_tests.py b/backend/app/services/mission_tests.py index e285c2d..3193acc 100644 --- a/backend/app/services/mission_tests.py +++ b/backend/app/services/mission_tests.py @@ -611,13 +611,17 @@ def update_mission_test_fields( # Implicit lifecycle (post-amendement 2026-05-15 bis): the explicit # workflow is gone from the UI. A write to ANY red field implies the - # test was executed (auto-stamp executed_at if the operator didn't - # supply one); a write to ANY blue field on an executed test implies - # the blue team reviewed it. The /transition endpoint stays for - # back-fill but is no longer the primary path. + # test was executed AND the review (if any) is potentially stale — + # so the state always lands on `executed`, even from `reviewed_by_blue` + # (red just changed something, blue needs to re-review). A write to + # ANY blue field on an executed test implies the blue team reviewed + # it. Note that a same-PUT red+blue (e.g. an admin filling both + # sides) flows `* → executed → reviewed_by_blue` and lands on + # `reviewed_by_blue`. The /transition endpoint stays for back-fill + # but is no longer the primary path. red_touched = bool(touched & _RED_FIELDS) blue_touched = bool(touched & _BLUE_FIELDS) - if red_touched and test.state in {"pending", "skipped", "blocked"}: + if red_touched: if test.executed_at is None: test.executed_at = datetime.now(tz=timezone.utc) test.executed_at_overridden = False diff --git a/backend/tests/test_mission_tests.py b/backend/tests/test_mission_tests.py index 481336b..b7c36e9 100644 --- a/backend/tests/test_mission_tests.py +++ b/backend/tests/test_mission_tests.py @@ -644,6 +644,41 @@ def test_red_writing_any_red_field_implicitly_executes_and_stamps( assert body["executed_at_overridden"] is False +def test_red_write_on_reviewed_reverts_to_executed( + client, admin_token, catalogue, red_user, blue_user +): + """When red modifies a test that blue had already reviewed, the state + reverts to `executed` (awaiting re-review). The red edits invalidate + the previous review — user feedback 2026-05-15 ter.""" + mission = _make_mission( + client, admin_token, name="m7-red-after-review", + scenario_id=catalogue["scenario"]["id"], + red_id=red_user["id"], blue_id=blue_user["id"], + ) + tid = _first_test_id(mission) + # Red executes. + client.put( + f"/api/v1/missions/{mission['id']}/tests/{tid}", + headers=_bearer(red_user["token"]), + json={"red_command": "first attempt"}, + ) + # Blue reviews — state becomes reviewed_by_blue. + blue_done = client.put( + f"/api/v1/missions/{mission['id']}/tests/{tid}", + headers=_bearer(blue_user["token"]), + json={"blue_comment_md": "alert raised"}, + ) + assert blue_done.get_json()["state"] == "reviewed_by_blue" + # Red modifies their command — state should drop back to executed. + re_red = client.put( + f"/api/v1/missions/{mission['id']}/tests/{tid}", + headers=_bearer(red_user["token"]), + json={"red_command": "second attempt"}, + ) + assert re_red.status_code == 200 + assert re_red.get_json()["state"] == "executed" + + def test_blue_writing_any_blue_field_promotes_executed_to_reviewed( client, admin_token, catalogue, red_user, blue_user ): diff --git a/frontend/src/pages/MissionScenarioTable.tsx b/frontend/src/pages/MissionScenarioTable.tsx index 7bb459e..c9128c1 100644 --- a/frontend/src/pages/MissionScenarioTable.tsx +++ b/frontend/src/pages/MissionScenarioTable.tsx @@ -168,17 +168,16 @@ function CommentairesCell({ test: MissionTest; detectionLevels: DetectionLevel[]; }) { + // Commentaires is blue-team content only — the workflow state lives in + // the Test column (cf. user feedback 2026-05-15 ter). const lvl = detectionLevels.find((l) => l.id === test.detection_level_id); return (
-
- - {MISSION_TEST_STATE_LABEL[test.state]} - - {lvl && ( + {lvl && ( +
{lvl.label_en} - )} -
+
+ )} {test.blue_comment_md && (

{truncate(test.blue_comment_md, 220)} @@ -371,6 +370,11 @@ export function MissionScenarioTable({ >

+
+ + {MISSION_TEST_STATE_LABEL[t.state]} + +
{t.snapshot_name}
{t.mitre_tags.slice(0, 3).map((tag) => (