"""SQLAlchemy model for archived test rounds. Each row is a snapshot of one team's round of work (procedure/results for Red, detection/containment for Blue) taken right before a lead reopens the test for rework. Reopening resets the live fields on ``Test`` for a fresh attempt, but the prior attempt's data must not be lost — it's archived here so the full history stays visible (e.g. to Blue Team, who need to see what Red actually did on earlier rounds, not just the latest one). """ import uuid from sqlalchemy import Column, DateTime, Enum, ForeignKey, Integer, String, Text, func from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship from app.database import Base from app.models.enums import AttackSuccessResult, ContainmentResult, TestResult class TestRoundHistory(Base): """Archived snapshot of one red/blue round of a test, taken on reopen.""" __tablename__ = "test_round_history" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) test_id = Column(UUID(as_uuid=True), ForeignKey("tests.id"), nullable=False) team = Column(String(10), nullable=False) # "red" or "blue" round_number = Column(Integer, nullable=False) started_at = Column(DateTime, nullable=True) ended_at = Column(DateTime, nullable=True) paused_seconds = Column(Integer, default=0) # Red-side round output procedure_text = Column(Text, nullable=True) tool_used = Column(String, nullable=True) # native_enum=False — stored as plain VARCHAR, not the shared Postgres # enum type used by tests.attack_success. This is an archive table with # no DB-level constraint needs, and reusing the native type name here # would make Alembic try to (re)create it. attack_success = Column(Enum(AttackSuccessResult, name="test_round_history_attack_success", native_enum=False), nullable=True) red_summary = Column(Text, nullable=True) execution_start_time = Column(DateTime, nullable=True) execution_end_time = Column(DateTime, nullable=True) # Blue-side round output detection_result = Column(Enum(TestResult, name="test_round_history_detection_result", native_enum=False), nullable=True) containment_result = Column(Enum(ContainmentResult, name="test_round_history_containment_result", native_enum=False), nullable=True) detection_time = Column(DateTime, nullable=True) containment_time = Column(DateTime, nullable=True) blue_summary = Column(Text, nullable=True) detect_procedure = Column(Text, nullable=True) # Why the round was closed review_notes = Column(Text, nullable=True) reviewed_by = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) archived_at = Column(DateTime(timezone=True), server_default=func.now()) test = relationship("Test", back_populates="round_history") reviewer = relationship("User", foreign_keys=[reviewed_by])