import importlib.util from pathlib import Path import unittest spec = importlib.util.spec_from_file_location("incident_exercise", Path(__file__).resolve().parents[1] / "deploy" / "incident_exercise.py") exercise = importlib.util.module_from_spec(spec) spec.loader.exec_module(exercise) def valid_record(gate="B"): required = exercise.GATE_C_SCENARIOS if gate == "C" else exercise.GATE_B_SCENARIOS return { "schemaVersion": 1, "system": "guestops-incident-exercise", "targetGate": gate, "dataClassification": "synthetic-only", "releaseCommit": "a" * 40, "releaseRecordSha256": "b" * 64, "environment": "https://sandbox-guestops.futuresens.co.uk", "operator": "Exercise operator", "incidentCommander": "Incident commander", "reviewedBy": "Independent reviewer", "startedAt": "2026-09-29T09:00:00Z", "endedAt": "2026-09-29T10:00:00Z", "reviewedAt": "2026-09-29T11:00:00Z", "targetsMinutes": {"detection": 10, "containment": 20, "recovery": 60}, "observedMinutes": {"detection": 5, "containment": 15, "recovery": 45}, "scenarios": [ {"id": scenario, "status": "pass", "evidence": [f"restricted-ticket-{index}"]} for index, scenario in enumerate(sorted(required), 1) ], "postExerciseState": { "externalWrites": "disabled", "faqMode": "off", "unresolvedOperations": 0, }, } class IncidentExerciseTests(unittest.TestCase): def test_gate_b_and_c_complete_records_pass(self): exercise.validate(valid_record("B")) exercise.validate(valid_record("C")) def test_gate_specific_scenarios_are_required(self): record = valid_record("C") record["scenarios"].pop() with self.assertRaisesRegex(ValueError, "exact incident scenario"): exercise.validate(record) def test_timings_must_meet_preagreed_targets(self): record = valid_record() record["observedMinutes"]["containment"] = 21 with self.assertRaisesRegex(ValueError, "Observed containment"): exercise.validate(record) record = valid_record() record["targetsMinutes"]["recovery"] = True with self.assertRaisesRegex(ValueError, "recovery target"): exercise.validate(record) def test_independent_people_and_safe_evidence_are_required(self): record = valid_record() record["reviewedBy"] = record["operator"] with self.assertRaisesRegex(ValueError, "different people"): exercise.validate(record) record = valid_record() record["scenarios"][0]["evidence"] = ["guest@example.invalid"] with self.assertRaisesRegex(ValueError, "safe opaque"): exercise.validate(record) def test_safe_post_exercise_state_is_required(self): record = valid_record() record["postExerciseState"]["externalWrites"] = "enabled" with self.assertRaisesRegex(ValueError, "writes disabled"): exercise.validate(record) if __name__ == "__main__": unittest.main()