From 67724aec6d2c85aecc084053442bd7efc084f98b Mon Sep 17 00:00:00 2001 From: Ashen <310210685+ashen0x@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:02:58 +0530 Subject: [PATCH] fix: constrain terminal outcome source labels in the schema --- .../c8e4a1f2b3d5_add_terminal_outcome_ledger.py | 7 +++++++ routstr/core/db.py | 10 ++++++++++ tests/unit/test_terminal_outcome_migration.py | 10 +++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/migrations/versions/c8e4a1f2b3d5_add_terminal_outcome_ledger.py b/migrations/versions/c8e4a1f2b3d5_add_terminal_outcome_ledger.py index 8de0522e..4c36bd37 100644 --- a/migrations/versions/c8e4a1f2b3d5_add_terminal_outcome_ledger.py +++ b/migrations/versions/c8e4a1f2b3d5_add_terminal_outcome_ledger.py @@ -109,6 +109,13 @@ def upgrade() -> None: "AND cache_creation_input_tokens >= 0 AND revenue_msats >= 0", name="ck_terminal_outcomes_nonnegative", ), + sa.CheckConstraint( + "input_source IN ('reported', 'estimated', 'missing') " + "AND output_source IN ('reported', 'estimated', 'missing') " + "AND cache_read_source IN ('reported', 'estimated', 'missing') " + "AND cache_creation_source IN ('reported', 'estimated', 'missing')", + name="ck_terminal_outcomes_sources", + ), sa.PrimaryKeyConstraint("outcome_id"), ) op.create_index( diff --git a/routstr/core/db.py b/routstr/core/db.py index 591d6a7c..f381e925 100644 --- a/routstr/core/db.py +++ b/routstr/core/db.py @@ -809,6 +809,9 @@ class ReservationRelease(SQLModel, table=True): # type: ignore class TerminalOutcome(SQLModel, table=True): # type: ignore + """One settled request. Ledger times are UTC epoch milliseconds, and + terminal_day is the UTC day of terminal_at_ms, kept for indexed day reads.""" + __tablename__ = "terminal_outcomes" __table_args__ = ( Index( @@ -822,6 +825,13 @@ class TerminalOutcome(SQLModel, table=True): # type: ignore "AND cache_creation_input_tokens >= 0 AND revenue_msats >= 0", name="ck_terminal_outcomes_nonnegative", ), + CheckConstraint( + "input_source IN ('reported', 'estimated', 'missing') " + "AND output_source IN ('reported', 'estimated', 'missing') " + "AND cache_read_source IN ('reported', 'estimated', 'missing') " + "AND cache_creation_source IN ('reported', 'estimated', 'missing')", + name="ck_terminal_outcomes_sources", + ), ) outcome_id: str = Field(primary_key=True) diff --git a/tests/unit/test_terminal_outcome_migration.py b/tests/unit/test_terminal_outcome_migration.py index 785ab3f3..ae19cf74 100644 --- a/tests/unit/test_terminal_outcome_migration.py +++ b/tests/unit/test_terminal_outcome_migration.py @@ -134,7 +134,7 @@ def test_terminal_outcome_migration_round_trips(tmp_path: Path) -> None: run_sql = connection.execute( "SELECT sql FROM sqlite_master WHERE name = 'terminal_outcome_writer_runs'" ).fetchone() - assert outcome_sql is not None and outcome_sql[0].count("CHECK") == 1 + assert outcome_sql is not None and outcome_sql[0].count("CHECK") == 2 assert epoch_sql is not None and epoch_sql[0].count("CHECK") == 3 assert run_sql is not None and run_sql[0].count("CHECK") == 4 @@ -164,6 +164,14 @@ def test_terminal_outcome_migration_round_trips(tmp_path: Path) -> None: "('request-invalid', 2, '2026-08-31', 'author/model', " "-1, 0, 0, 0, 0)" ) + with pytest.raises(sqlite3.IntegrityError): + connection.execute( + "INSERT INTO terminal_outcomes " + "(outcome_id, terminal_at_ms, terminal_day, model_identifier, " + "input_source, input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens, revenue_msats) VALUES " + "('request-bad-label', 3, '2026-08-31', 'author/model', " + "'guessed', 0, 0, 0, 0, 0)" + ) with pytest.raises(sqlite3.IntegrityError): connection.execute( "INSERT INTO terminal_outcome_epochs VALUES (1, '2026-09-02', NULL, 1)"