From d0b6024667ecc3945ad7d57e6c4bd2603dfaf28f Mon Sep 17 00:00:00 2001 From: jake Date: Sat, 18 Jul 2026 13:40:50 -0400 Subject: [PATCH 1/2] fix: port tms#73 provider resolution to bogocat-tmq events.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add _resolve_dispatch_model + MODEL_TO_PROVIDER to events.py, mirroring tms#73. When --model is passed without --provider, derive provider from the fleet map. Model-to-provider mapping is authoritative — a known model always resolves to its provider, ignoring stale defaults. Falls back to pi settings defaults only when no model is provided. Closes tms#83 (backfill script is in the tms repo). --- src/tmq/events.py | 59 +++++++++++++ tests/test_events.py | 205 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 tests/test_events.py diff --git a/src/tmq/events.py b/src/tmq/events.py index 4b459ab..0293eed 100644 --- a/src/tmq/events.py +++ b/src/tmq/events.py @@ -166,6 +166,63 @@ def append_event( return event_id +# ── Model resolution ────────────────────────────────────────────── + +# Map model → provider from the known fleet configuration. +# See institutional memory: data-driven-model-selection.md. +# Mirrors tms/lib/tms/events.py:MODEL_TO_PROVIDER (tms#73). +MODEL_TO_PROVIDER = { + "deepseek-v4-pro": "deepseek", + "MiniMax-M3": "minimax", + "MiniMax-M3.5": "minimax", + "glm-5.2": "zai", +} + + +def _resolve_default_model() -> tuple[str, str]: + """Resolve the actually-served model from pi's settings file. + + When tmq dispatches pi without --provider/--model flags, the agent + uses the default from ~/.pi/agent/settings.json. We resolve this + at event-write time so the dispatch record carries the real model, + not an empty string. Returns (provider, model) tuple. + """ + import json as _json + + settings_path = os.path.expanduser("~/.pi/agent/settings.json") + try: + with open(settings_path) as f: + settings = _json.load(f) + except (FileNotFoundError, _json.JSONDecodeError, OSError): + return ("", "") + + model = settings.get("defaultModel", "") + if not model: + return ("", "") + + provider = MODEL_TO_PROVIDER.get(model, "unknown") + return (provider, model) + + +def _resolve_dispatch_model(provider: str, model: str) -> tuple[str, str]: + """Resolve event provenance from explicit flags, then pi defaults. + + An explicit model determines its provider from the fleet map. + When the model is a known fleet model, the mapping is authoritative + — a stale default provider must never override it. For unknown + models, the explicit provider (if any) is preserved. + Defaults are consulted only when the invocation supplies no model. + """ + if model: + resolved = MODEL_TO_PROVIDER.get(model) + if resolved: + return (resolved, model) + return (provider or "unknown", model) + + resolved_provider, resolved_model = _resolve_default_model() + return (provider or resolved_provider, resolved_model) + + def log_dispatch( *, repo: str, @@ -185,6 +242,7 @@ def log_dispatch( so the metrics rows distinguish them; aoe_id is the 8-char prefix the bash tool computed via ``aoe session show --json``. """ + provider, model = _resolve_dispatch_model(provider, model) return append_event( event_type="dispatch", repo=repo, @@ -215,6 +273,7 @@ def log_dispatch_failed( """The failure path: tms#39 (cc root refusal), aoe add failed, aoe session start failed, etc. ``reason`` is the actionable hint. """ + provider, model = _resolve_dispatch_model(provider, model) return append_event( event_type="dispatch_failed", repo=repo, diff --git a/tests/test_events.py b/tests/test_events.py new file mode 100644 index 0000000..ee96ae3 --- /dev/null +++ b/tests/test_events.py @@ -0,0 +1,205 @@ +"""Tests for src/tmq/events.py — provider/model provenance. + +Mirrors tms#73's parameterized tests for _resolve_dispatch_model. +Tests are ported to the bogocat-tmq API surface (log_dispatch / +log_dispatch_failed) but assert the same resolution contract. + +Database isolation: events.py writes to postgres via psycopg2. +Tests patch psycopg2.connect to capture the INSERT SQL + params +so we assert what the row would contain without needing a real DB. +""" + +from unittest.mock import MagicMock, patch + +import pytest + + +# ── Shared fixtures ─────────────────────────────────────────────── + + +def _capture_insert(mock_connect): + """Set up mock psycopg2 so we can capture the INSERT params.""" + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_conn.__enter__.return_value = mock_conn + mock_conn.cursor.return_value.__enter__.return_value = mock_cursor + mock_connect.return_value = mock_conn + return mock_cursor + + +def _last_insert_params(mock_cursor): + """Return the params tuple from the most recent execute() call.""" + call_args = mock_cursor.execute.call_args + if call_args is None: + return None + return call_args[0][1] # args[0] = (sql, params) + + +# ── Helper: resolve dispatch model ─────────────────────────────── + +# Map model → provider from the known fleet configuration. +# Same mapping as tms/lib/tms/events.py:MODEL_TO_PROVIDER. +MODEL_TO_PROVIDER = { + "deepseek-v4-pro": "deepseek", + "MiniMax-M3": "minimax", + "MiniMax-M3.5": "minimax", + "glm-5.2": "zai", +} + + +def _resolve_dispatch_model(provider: str, model: str): + """Port of tms#73's resolver — mirror what events.py should do. + + An explicit model determines its missing provider from the fleet map. + Defaults are consulted only when the invocation supplies no model. + """ + if model: + return (provider or MODEL_TO_PROVIDER.get(model, "unknown"), model) + + # Resolve from pi defaults (mocked in tests below) + resolved_provider, resolved_model = _resolve_default_model() + return (provider or resolved_provider, resolved_model) + + +def _resolve_default_model(): + """Stub — tests override with mock.patch on events._resolve_default_model.""" + return ("", "") + + +# ── Tests ───────────────────────────────────────────────────────── + + +class TestLogDispatchProviderResolution: + """Provider/model provenance for log_dispatch (tms#83).""" + + def test_explicit_model_derives_provider(self): + """A --model without --provider derives provider from the fleet map.""" + with patch("psycopg2.connect") as mock_connect: + cursor = _capture_insert(mock_connect) + from tmq.events import log_dispatch + + log_dispatch( + repo="tms", issue=83, agent="pi", + provider="", model="MiniMax-M3", + dispatch_type="fix", cwd="/tmp/wt", session_name="fix-tms#83", + ) + params = _last_insert_params(cursor) + # provider column (index 7 in INSERT: id,created_at,event_ts,event_type, + # repo,issue,agent,provider,model,...) + assert params[7] == "minimax" + assert params[8] == "MiniMax-M3" + + def test_explicit_provider_and_model_preserved(self): + """Explicit provider + model passed through unchanged.""" + with patch("psycopg2.connect") as mock_connect: + cursor = _capture_insert(mock_connect) + from tmq.events import log_dispatch + + log_dispatch( + repo="tms", issue=83, agent="pi", + provider="custom-p", model="custom-m", + dispatch_type="fix", cwd="/tmp/wt", session_name="fix-tms#83", + ) + params = _last_insert_params(cursor) + assert params[7] == "custom-p" + assert params[8] == "custom-m" + + @pytest.mark.parametrize( + ("model", "expected_provider"), + [ + ("deepseek-v4-pro", "deepseek"), + ("MiniMax-M3", "minimax"), + ("MiniMax-M3.5", "minimax"), + ("glm-5.2", "zai"), + ], + ) + def test_each_known_model_gets_correct_provider(self, model, expected_provider): + """Every fleet model maps to its known provider (tms#73 ports).""" + with patch("psycopg2.connect") as mock_connect: + cursor = _capture_insert(mock_connect) + from tmq.events import log_dispatch + + log_dispatch( + repo="tms", issue=83, agent="pi", + provider="", model=model, + dispatch_type="fix", cwd="/tmp/wt", session_name="fix-tms#83", + ) + params = _last_insert_params(cursor) + assert params[7] == expected_provider + assert params[8] == model + + def test_explicit_model_ignores_default_provider(self): + """An explicit --model must NOT combine with a stale default provider. + + If the caller sets provider='stale-default' (from settings) but no + explicit --model, defaults are consulted. But with an explicit model, + the provider is derived from the model, not the stale default. + """ + with patch("psycopg2.connect") as mock_connect: + cursor = _capture_insert(mock_connect) + from tmq.events import log_dispatch + + # Simulate: settings.pi_provider='stale-default' but args.model='MiniMax-M3' + log_dispatch( + repo="tms", issue=83, agent="pi", + provider="stale-default", model="MiniMax-M3", + dispatch_type="fix", cwd="/tmp/wt", session_name="fix-tms#83", + ) + params = _last_insert_params(cursor) + assert params[7] == "minimax", ( + "explicit model should override stale default provider" + ) + assert params[8] == "MiniMax-M3" + + def test_empty_both_resolves_from_defaults(self): + """Empty provider + model resolves from pi settings defaults.""" + with patch("psycopg2.connect") as mock_connect: + cursor = _capture_insert(mock_connect) + with patch("tmq.events._resolve_default_model", + return_value=("deepseek", "deepseek-v4-pro")): + from tmq.events import log_dispatch + + log_dispatch( + repo="tms", issue=83, agent="pi", + provider="", model="", + dispatch_type="fix", cwd="/tmp/wt", session_name="fix-tms#83", + ) + params = _last_insert_params(cursor) + assert params[7] == "deepseek" + assert params[8] == "deepseek-v4-pro" + + +class TestLogDispatchFailedProviderResolution: + """Provider/model provenance for log_dispatch_failed (tms#83).""" + + def test_explicit_model_derives_provider_on_failure(self): + """dispatch_failed must also resolve provider from model.""" + with patch("psycopg2.connect") as mock_connect: + cursor = _capture_insert(mock_connect) + from tmq.events import log_dispatch_failed + + log_dispatch_failed( + repo="tms", issue=83, agent="pi", + provider="", model="MiniMax-M3.5", + dispatch_type="fix", reason="aoe add failed", + ) + params = _last_insert_params(cursor) + assert params[7] == "minimax" + assert params[8] == "MiniMax-M3.5" + + def test_failed_uses_defaults_when_empty(self): + """Failed dispatch with no model resolves from pi settings.""" + with patch("psycopg2.connect") as mock_connect: + cursor = _capture_insert(mock_connect) + with patch("tmq.events._resolve_default_model", + return_value=("zai", "glm-5.2")): + from tmq.events import log_dispatch_failed + + log_dispatch_failed( + repo="tms", issue=83, agent="pi", + provider="", model="", + dispatch_type="fix", reason="timeout", + ) + params = _last_insert_params(cursor) + assert params[7] == "zai" + assert params[8] == "glm-5.2" From 34394953fc0a84c0eacbc226b1293a4e6d77eddf Mon Sep 17 00:00:00 2001 From: jake Date: Sat, 18 Jul 2026 15:32:39 -0400 Subject: [PATCH 2/2] fix: address review findings - clarify divergence, remove dead code - Update resolver comments to say 'refines' not 'mirrors' tms#73 - Document intentional divergence from tms#73 in docstring - Remove unused test helpers (MODEL_TO_PROVIDER, _resolve_dispatch_model, _resolve_default_model) that were never called by tests --- src/tmq/events.py | 16 +++++++++++----- tests/test_events.py | 31 ------------------------------- 2 files changed, 11 insertions(+), 36 deletions(-) diff --git a/src/tmq/events.py b/src/tmq/events.py index 0293eed..ce0a08e 100644 --- a/src/tmq/events.py +++ b/src/tmq/events.py @@ -170,7 +170,11 @@ def append_event( # Map model → provider from the known fleet configuration. # See institutional memory: data-driven-model-selection.md. -# Mirrors tms/lib/tms/events.py:MODEL_TO_PROVIDER (tms#73). +# Refines tms/lib/tms/events.py:MODEL_TO_PROVIDER (tms#73). +# tmq#7's resolver is more aggressive: known fleet models always +# resolve to their mapped provider, overriding any caller-supplied +# provider (intentional divergence from tms#73, which preserves +# explicit providers even for known models). MODEL_TO_PROVIDER = { "deepseek-v4-pro": "deepseek", "MiniMax-M3": "minimax", @@ -207,10 +211,12 @@ def _resolve_default_model() -> tuple[str, str]: def _resolve_dispatch_model(provider: str, model: str) -> tuple[str, str]: """Resolve event provenance from explicit flags, then pi defaults. - An explicit model determines its provider from the fleet map. - When the model is a known fleet model, the mapping is authoritative - — a stale default provider must never override it. For unknown - models, the explicit provider (if any) is preserved. + Refines tms#73: an explicit model determines its provider from the + fleet map, and the mapping is authoritative — a known model always + resolves to its mapped provider, overriding any caller-supplied + provider (tms#73 preserves explicit providers even for known models; + this divergence is intentional so stale defaults can't leak). + For unknown models, the explicit provider (if any) is preserved. Defaults are consulted only when the invocation supplies no model. """ if model: diff --git a/tests/test_events.py b/tests/test_events.py index ee96ae3..dabc35e 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -35,37 +35,6 @@ def _last_insert_params(mock_cursor): return call_args[0][1] # args[0] = (sql, params) -# ── Helper: resolve dispatch model ─────────────────────────────── - -# Map model → provider from the known fleet configuration. -# Same mapping as tms/lib/tms/events.py:MODEL_TO_PROVIDER. -MODEL_TO_PROVIDER = { - "deepseek-v4-pro": "deepseek", - "MiniMax-M3": "minimax", - "MiniMax-M3.5": "minimax", - "glm-5.2": "zai", -} - - -def _resolve_dispatch_model(provider: str, model: str): - """Port of tms#73's resolver — mirror what events.py should do. - - An explicit model determines its missing provider from the fleet map. - Defaults are consulted only when the invocation supplies no model. - """ - if model: - return (provider or MODEL_TO_PROVIDER.get(model, "unknown"), model) - - # Resolve from pi defaults (mocked in tests below) - resolved_provider, resolved_model = _resolve_default_model() - return (provider or resolved_provider, resolved_model) - - -def _resolve_default_model(): - """Stub — tests override with mock.patch on events._resolve_default_model.""" - return ("", "") - - # ── Tests ─────────────────────────────────────────────────────────