Files
2026-07-13 13:12:33 +08:00

116 lines
3.9 KiB
Python

"""``opensquilla router calibrate`` CLI.
Seeds a synthetic ``router_decisions`` table (hand-created — never real state)
and drives the command through Typer's ``CliRunner``. The output file always
lands under a monkeypatched ``OPENSQUILLA_STATE_DIR`` temp dir, never the real
home.
"""
from __future__ import annotations
import json
import sqlite3
from pathlib import Path
from typing import Any
from typer.testing import CliRunner
from opensquilla.cli.main import app
from opensquilla.persistence.router_decision_writer import RouterDecisionWriter
runner = CliRunner()
_CREATE_TABLE = (
"CREATE TABLE router_decisions ("
" decision_id TEXT PRIMARY KEY, session_key TEXT NOT NULL,"
" turn_index INTEGER, ts_ms INTEGER NOT NULL, classifier TEXT,"
" proposed_tier TEXT, confidence REAL, probs TEXT, flags TEXT,"
" final_tier TEXT, provider TEXT, model TEXT, thinking_level TEXT,"
" source TEXT, trail TEXT, baseline_model TEXT, savings_pct REAL,"
" executed_kind TEXT, ensemble_profile TEXT,"
" fallback_hops INTEGER NOT NULL DEFAULT 0)"
)
def _seed_db(path: Path, *, count: int = 40) -> None:
conn = sqlite3.connect(str(path), check_same_thread=False, isolation_level=None)
conn.row_factory = sqlite3.Row
conn.execute(_CREATE_TABLE)
writer = RouterDecisionWriter(conn)
for index in range(count):
writer.record_decision(
{
"decision_id": f"c{index}",
"session_key": "agent:calib:main",
"turn_index": index,
"proposed_tier": "c2",
"final_tier": "c1",
"source": "v4_phase3",
"trail": [{"stage": "confidence_gate", "applied": True}],
}
)
writer.close()
def _env(monkeypatch: Any, tmp_path: Path, db: Path | None) -> None:
monkeypatch.setenv("OPENSQUILLA_STATE_DIR", str(tmp_path / "home"))
if db is not None:
monkeypatch.setenv("OPENSQUILLA_ROUTER_DECISIONS_DB", str(db))
else:
monkeypatch.delenv("OPENSQUILLA_ROUTER_DECISIONS_DB", raising=False)
def _output_file(tmp_path: Path) -> Path:
return tmp_path / "home" / "state" / "router_calibration.json"
def test_calibrate_dry_run_prints_without_writing(tmp_path: Path, monkeypatch: Any) -> None:
db = tmp_path / "sessions.db"
_seed_db(db)
_env(monkeypatch, tmp_path, db)
result = runner.invoke(app, ["router", "calibrate", "--dry-run"])
assert result.exit_code == 0, result.output
assert "samples:" in result.output
assert "threshold_adjust:" in result.output
assert "dry-run" in result.output
assert not _output_file(tmp_path).exists()
def test_calibrate_writes_file(tmp_path: Path, monkeypatch: Any) -> None:
db = tmp_path / "sessions.db"
_seed_db(db)
_env(monkeypatch, tmp_path, db)
result = runner.invoke(app, ["router", "calibrate"])
assert result.exit_code == 0, result.output
out = _output_file(tmp_path)
assert out.exists()
payload = json.loads(out.read_text(encoding="utf-8"))
assert payload["sample_count"] == 40
# 40 clean c2 downgrades -> bias c2 down (clamped), threshold up.
assert payload["per_class_bias"] == {"c2": -0.15}
assert payload["threshold_adjust"] > 0.0
def test_calibrate_json_output(tmp_path: Path, monkeypatch: Any) -> None:
db = tmp_path / "sessions.db"
_seed_db(db)
_env(monkeypatch, tmp_path, db)
result = runner.invoke(app, ["router", "calibrate", "--json"])
assert result.exit_code == 0, result.output
doc = json.loads(result.output)
assert doc["wrote"] is True
assert doc["calibration"]["sample_count"] == 40
def test_calibrate_missing_db_is_neutral_no_crash(tmp_path: Path, monkeypatch: Any) -> None:
_env(monkeypatch, tmp_path, tmp_path / "does-not-exist.db")
result = runner.invoke(app, ["router", "calibrate", "--dry-run"])
assert result.exit_code == 0, result.output
assert "samples: 0" in result.output
assert not _output_file(tmp_path).exists()