mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 21:47:02 +01:00
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
177 lines
5.6 KiB
Python
177 lines
5.6 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from python_pkg.token_audit import attribute
|
|
from python_pkg.token_audit.imagecost import image_cost
|
|
from python_pkg.token_audit.model import Session, ToolCall, Turn
|
|
|
|
|
|
def _turn(
|
|
context: int = 0,
|
|
out: int = 0,
|
|
read: int = 0,
|
|
create: int = 0,
|
|
*,
|
|
sidechain: bool = False,
|
|
model: str = "claude-opus-5",
|
|
) -> Turn:
|
|
return Turn(
|
|
usage={
|
|
"output_tokens": out,
|
|
"cache_read_input_tokens": read,
|
|
"cache_creation_input_tokens": create,
|
|
},
|
|
context=context,
|
|
model=model,
|
|
is_sidechain=sidechain,
|
|
)
|
|
|
|
|
|
def _session(
|
|
cwd: str | None = "/home/kuhy/p",
|
|
turns: list[Turn] | None = None,
|
|
tools: list[ToolCall] | None = None,
|
|
image_cost_value: float = 0.0,
|
|
) -> Session:
|
|
session = Session(session_id="s", path="p", cwd=cwd)
|
|
session.turns = turns or []
|
|
session.tools = tools or []
|
|
session.image_cost = image_cost_value
|
|
return session
|
|
|
|
|
|
def test_totals_sum_each_token_kind() -> None:
|
|
session = _session(turns=[_turn(out=2, read=100), _turn(out=1, read=50)])
|
|
totals, _ = attribute.build([session])
|
|
assert totals.by_kind["output_tokens"] == 3
|
|
assert totals.by_kind["cache_read_input_tokens"] == 150
|
|
assert totals.turns == 2
|
|
|
|
|
|
def test_unknown_cwd_is_labelled() -> None:
|
|
_, axes = attribute.build([_session(cwd=None, turns=[_turn(out=1)])])
|
|
assert "unknown" in axes.project
|
|
|
|
|
|
def test_sidechain_cost_isolated() -> None:
|
|
session = _session(turns=[_turn(out=1, sidechain=True), _turn(out=1)])
|
|
_, axes = attribute.build([session])
|
|
assert axes.sidechain.count == 1
|
|
assert axes.sidechain.total == pytest.approx(5.0)
|
|
|
|
|
|
def test_tool_mcp_and_skill_axes() -> None:
|
|
tools = [
|
|
ToolCall(name="Read", result_tokens=10, path="/a.png"),
|
|
ToolCall(name="Read", result_tokens=5, path="/a.py"),
|
|
ToolCall(name="mcp__aseprite__draw_line", result_tokens=1),
|
|
ToolCall(name="Skill", result_tokens=1, skill="finish"),
|
|
]
|
|
_, axes = attribute.build([_session(tools=tools)])
|
|
assert axes.tool_tokens["Read"] == 15
|
|
assert axes.tool_calls["Read"] == 2
|
|
assert axes.mcp_calls["aseprite"] == 1
|
|
assert axes.skills["finish"] == 1
|
|
assert axes.images.count == 1
|
|
assert axes.images.total == 10
|
|
|
|
|
|
def test_long_session_threshold() -> None:
|
|
short = _session(turns=[_turn(out=1)])
|
|
long_turns = [_turn(out=1) for _ in range(attribute.LONG_SESSION_TURNS + 1)]
|
|
long = _session(turns=long_turns)
|
|
_, axes = attribute.build([short, long])
|
|
assert axes.long_sessions.count == 1
|
|
assert axes.long_sessions.total == pytest.approx(len(long_turns) * 5.0)
|
|
|
|
|
|
def test_cold_start_only_counted_when_nonzero() -> None:
|
|
cold = _session(turns=[_turn(create=1000)])
|
|
warm = _session(turns=[_turn(read=10, create=5)])
|
|
_, axes = attribute.build([cold, warm])
|
|
assert axes.cold_start.count == 1
|
|
assert axes.cold_start.total == 1000
|
|
|
|
|
|
def test_image_cost_is_accumulated_from_sessions() -> None:
|
|
_, axes = attribute.build([_session(image_cost_value=7.5)])
|
|
assert axes.image_cost == pytest.approx(7.5)
|
|
|
|
|
|
def test_models_axis_tracks_output() -> None:
|
|
session = _session(turns=[_turn(out=4, model="claude-sonnet-5")])
|
|
_, axes = attribute.build([session])
|
|
assert axes.models["claude-sonnet-5"] == 4
|
|
|
|
|
|
def test_reconcile_zero_when_consistent() -> None:
|
|
session = _session(turns=[_turn(out=1)])
|
|
totals, _ = attribute.build([session])
|
|
assert attribute.reconcile(totals, [session]) == pytest.approx(0.0)
|
|
|
|
|
|
def test_reconcile_detects_drift() -> None:
|
|
session = _session(turns=[_turn(out=1)])
|
|
totals, _ = attribute.build([session])
|
|
totals.weighted *= 2
|
|
assert attribute.reconcile(totals, [session]) == pytest.approx(0.5)
|
|
|
|
|
|
def test_reconcile_guards_zero_total() -> None:
|
|
totals = attribute.Totals()
|
|
assert attribute.reconcile(totals, []) == 0.0
|
|
|
|
|
|
# --- imagecost ---------------------------------------------------------------
|
|
|
|
|
|
def _ev_turn(context: int) -> tuple[str, Turn]:
|
|
return ("turn", _turn(context=context))
|
|
|
|
|
|
def _ev_image(tokens: int) -> tuple[str, ToolCall]:
|
|
return ("tool", ToolCall(name="Read", result_tokens=tokens, path="/s.png"))
|
|
|
|
|
|
def test_image_charged_once_per_following_turn() -> None:
|
|
events = [_ev_image(1000), _ev_turn(10), _ev_turn(20), _ev_turn(30)]
|
|
assert image_cost(events) == pytest.approx(300.0)
|
|
|
|
|
|
def test_image_not_charged_for_earlier_turns() -> None:
|
|
events = [_ev_turn(10), _ev_image(1000), _ev_turn(20)]
|
|
assert image_cost(events) == pytest.approx(100.0)
|
|
|
|
|
|
def test_compaction_clears_live_images() -> None:
|
|
# context collapses from 100 to 20 (below the 0.7 ratio) => history dropped
|
|
events = [_ev_image(1000), _ev_turn(100), _ev_turn(20), _ev_turn(30)]
|
|
assert image_cost(events) == pytest.approx(100.0)
|
|
|
|
|
|
def test_growth_below_ratio_does_not_trigger_compaction() -> None:
|
|
events = [_ev_image(1000), _ev_turn(100), _ev_turn(90)]
|
|
assert image_cost(events) == pytest.approx(200.0)
|
|
|
|
|
|
def test_non_image_tools_are_free() -> None:
|
|
events = [
|
|
("tool", ToolCall(name="Bash", result_tokens=9999, path=None)),
|
|
("tool", ToolCall(name="Read", result_tokens=9999, path="/a.py")),
|
|
_ev_turn(10),
|
|
]
|
|
assert image_cost(events) == 0.0
|
|
|
|
|
|
def test_unknown_event_payloads_are_ignored() -> None:
|
|
assert image_cost([("tool", "not a toolcall"), ("turn", "not a turn")]) == 0.0
|
|
|
|
|
|
def test_annotate_sets_session_image_cost() -> None:
|
|
from python_pkg.token_audit import imagecost
|
|
|
|
session = _session()
|
|
imagecost.annotate(session, [_ev_image(100), _ev_turn(5)])
|
|
assert session.image_cost == pytest.approx(10.0)
|