from __future__ import annotations import json import tempfile import unittest from datetime import datetime, timedelta, timezone from pathlib import Path from .services.claude_usage_backfill import ( load_claude_usage_candidates, match_claude_usage, ) class ClaudeUsageBackfillTest(unittest.TestCase): def test_loads_cache_inclusive_usage_without_exposing_text(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: path = Path(temp_dir) / "session.jsonl" path.write_text( json.dumps( { "type": "assistant", "uuid": "assistant-1", "timestamp": "2026-07-01T00:00:00Z", "message": { "model": "claude-opus-4-8", "content": [{"type": "text", "text": "응답"}], "usage": { "input_tokens": 11, "cache_read_input_tokens": 101, "cache_creation_input_tokens": 23, "output_tokens": 7, }, }, }, ensure_ascii=False, ) + "\n", encoding="utf-8", ) candidates = load_claude_usage_candidates(Path(temp_dir)) self.assertEqual(len(candidates), 1) self.assertEqual(candidates[0].tokens_in, 135) self.assertEqual(candidates[0].tokens_out, 7) self.assertNotIn("응답", candidates[0].source_key) def test_matches_only_one_exact_text_and_time_candidate(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) for index, seconds in enumerate((0, 400)): (root / f"{index}.jsonl").write_text( json.dumps( { "type": "assistant", "uuid": f"assistant-{index}", "timestamp": ( datetime(2026, 7, 1, tzinfo=timezone.utc) + timedelta(seconds=seconds) ).isoformat(), "message": { "model": "claude-opus-4-8", "content": [{"type": "text", "text": "같은 응답\r\n"}], "usage": { "input_tokens": 10 + index, "output_tokens": 2, }, }, }, ensure_ascii=False, ) + "\n", encoding="utf-8", ) candidates = load_claude_usage_candidates(root) report = match_claude_usage( [ { "id": "turn-1", "created_at": datetime(2026, 7, 1, tzinfo=timezone.utc) + timedelta(seconds=20), "text_masked": "같은 응답", } ], candidates, ) self.assertEqual(len(report.matches), 1) self.assertEqual(report.matches[0].tokens_in, 10) self.assertEqual(report.ambiguous_turns, 0) def test_rejects_multiple_candidates_in_the_same_time_window(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) path = root / "session.jsonl" lines = [] for index in range(2): lines.append( json.dumps( { "type": "assistant", "uuid": f"assistant-{index}", "timestamp": f"2026-07-01T00:00:0{index}Z", "message": { "content": [{"type": "text", "text": "반복"}], "usage": {"input_tokens": 10, "output_tokens": 1}, }, }, ensure_ascii=False, ) ) path.write_text("\n".join(lines) + "\n", encoding="utf-8") candidates = load_claude_usage_candidates(root) report = match_claude_usage( [ { "id": "turn-1", "created_at": datetime(2026, 7, 1, 0, 0, 20, tzinfo=timezone.utc), "text_masked": "반복", } ], candidates, ) self.assertEqual(report.matches, ()) self.assertEqual(report.ambiguous_turns, 1) if __name__ == "__main__": unittest.main()