87
self.assertFalse(record["estimated"])
88
89
90
+class ParseCopilotTranscriptTests(unittest.TestCase):
91
+ def test_parses_input_output_tokens_pattern(self) -> None:
92
+ tests_root = Path(__file__).resolve().parent
93
+ with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
94
+ transcript = Path(tmpdir) / "transcript.md"
95
+ transcript.write_text(
96
+ "# Copilot Session\n\nSome content here.\n\n"
97
+ "---\nInput tokens: 1500\nOutput tokens: 800\n",
98
+ encoding="utf-8",
99
+ )
100
+ result = track_token_usage.parse_copilot_transcript(transcript)
101
+ self.assertEqual(result, (1500, 800))
102
+
103
+ def test_parses_prompt_completion_tokens_pattern(self) -> None:
104
+ tests_root = Path(__file__).resolve().parent
105
+ with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
106
+ transcript = Path(tmpdir) / "transcript.md"
107
+ transcript.write_text(
108
+ "```json\n{\"prompt_tokens\": 2000, \"completion_tokens\": 950}\n```\n",
109
+ encoding="utf-8",
110
+ )
111
+ result = track_token_usage.parse_copilot_transcript(transcript)
112
+ self.assertEqual(result, (2000, 950))
113
+
114
+ def test_parses_tokens_used_combined_pattern(self) -> None:
115
+ tests_root = Path(__file__).resolve().parent
116
+ with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
117
+ transcript = Path(tmpdir) / "transcript.md"
118
+ transcript.write_text(
119
+ "## Summary\nTokens used: 3000 input, 1200 output\n",
120
+ encoding="utf-8",
121
+ )
122
+ result = track_token_usage.parse_copilot_transcript(transcript)
123
+ self.assertEqual(result, (3000, 1200))
124
+
125
+ def test_parses_usage_slash_pattern(self) -> None:
126
+ tests_root = Path(__file__).resolve().parent
127
+ with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
128
+ transcript = Path(tmpdir) / "transcript.md"
129
+ transcript.write_text(
130
+ "Usage: 500/200 tokens (input/output)\n",
131
+ encoding="utf-8",
132
+ )
133
+ result = track_token_usage.parse_copilot_transcript(transcript)
134
+ self.assertEqual(result, (500, 200))
135
+
136
+ def test_returns_none_when_no_pattern_found(self) -> None:
137
+ tests_root = Path(__file__).resolve().parent
138
+ with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
139
+ transcript = Path(tmpdir) / "transcript.md"
140
+ transcript.write_text("# Just a normal transcript\nNo usage info here.\n", encoding="utf-8")
141
+ result = track_token_usage.parse_copilot_transcript(transcript)
142
+ self.assertIsNone(result)
143
+
144
+ def test_returns_none_for_missing_file(self) -> None:
145
+ result = track_token_usage.parse_copilot_transcript(Path("/nonexistent/path.md"))
146
+ self.assertIsNone(result)
147
+
148
+
149
+class ParseApiResponseTests(unittest.TestCase):
150
+ def test_parses_openai_compatible_usage(self) -> None:
151
+ tests_root = Path(__file__).resolve().parent
152
+ with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
153
+ response_file = Path(tmpdir) / "response.json"
154
+ response_file.write_text(
155
+ json.dumps({
156
+ "id": "chatcmpl-abc123",
157
+ "choices": [{"message": {"content": "Hello"}}],
158
+ "usage": {"prompt_tokens": 450, "completion_tokens": 120, "total_tokens": 570},
159
+ }),
160
+ encoding="utf-8",
161
+ )
162
+ result = track_token_usage.parse_api_response(response_file)
163
+ self.assertEqual(result, (450, 120))
164
+
165
+ def test_returns_none_for_missing_usage(self) -> None:
166
+ tests_root = Path(__file__).resolve().parent
167
+ with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
168
+ response_file = Path(tmpdir) / "response.json"
169
+ response_file.write_text(json.dumps({"choices": []}), encoding="utf-8")
170
+ result = track_token_usage.parse_api_response(response_file)
171
+ self.assertIsNone(result)
172
+
173
+ def test_returns_none_for_invalid_json(self) -> None:
174
+ tests_root = Path(__file__).resolve().parent
175
+ with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
176
+ response_file = Path(tmpdir) / "response.json"
177
+ response_file.write_text("not json at all", encoding="utf-8")
178
+ result = track_token_usage.parse_api_response(response_file)
179
+ self.assertIsNone(result)
180
+
181
+ def test_returns_none_for_missing_file(self) -> None:
182
+ result = track_token_usage.parse_api_response(Path("/nonexistent/response.json"))
183
+ self.assertIsNone(result)
184
+
185
+
186
+class TokenSourcePriorityTests(unittest.TestCase):
187
+ """Test the priority ordering: explicit > transcript/api > file-size estimate."""
188
+
189
+ def test_transcript_overrides_file_estimate(self) -> None:
190
+ tests_root = Path(__file__).resolve().parent
191
+ with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
192
+ base = Path(tmpdir)
193
+ usage_file = base / "token-usage.jsonl"
194
+ prompt_path = base / "prompt.txt"
195
+ output_path = base / "output.md"
196
+ transcript = base / "transcript.md"
197
+ prompt_path.write_text("x" * 400, encoding="utf-8")
198
+ output_path.write_text("y" * 200, encoding="utf-8")
199
+ transcript.write_text("Input tokens: 5000\nOutput tokens: 2500\n", encoding="utf-8")
200
+
201
+ exit_code = track_token_usage.main(
202
+ [
203
+ "--stage", "analysis",
204
+ "--source", "copilot-cli",
205
+ "--model", "claude-sonnet-4",
206
+ "--current-datetime", "2026-05-19T08:00:00Z",
207
+ "--prompt-file", str(prompt_path),
208
+ "--output-file", str(output_path),
209
+ "--transcript", str(transcript),
210
+ "--usage-file", str(usage_file),
211
+ ]
212
+ )
213
+
214
+ self.assertEqual(exit_code, 0)
215
+ record = json.loads(usage_file.read_text(encoding="utf-8").strip())
216
+ self.assertEqual(record["input_tokens"], 5000)
217
+ self.assertEqual(record["output_tokens"], 2500)
218
+ self.assertFalse(record["estimated"])
219
+
220
+ def test_api_response_overrides_file_estimate(self) -> None:
221
+ tests_root = Path(__file__).resolve().parent
222
+ with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
223
+ base = Path(tmpdir)
224
+ usage_file = base / "token-usage.jsonl"
225
+ prompt_path = base / "prompt.txt"
226
+ api_response = base / "response.json"
227
+ prompt_path.write_text("x" * 400, encoding="utf-8")
228
+ api_response.write_text(
229
+ json.dumps({"usage": {"prompt_tokens": 800, "completion_tokens": 300, "total_tokens": 1100}}),
230
+ encoding="utf-8",
231
+ )
232
+
233
+ exit_code = track_token_usage.main(
234
+ [
235
+ "--stage", "reskill",
236
+ "--source", "github-models",
237
+ "--model", "gpt-4.1",
238
+ "--current-datetime", "2026-05-19T08:00:00Z",
239
+ "--prompt-file", str(prompt_path),
240
+ "--api-response", str(api_response),
241
+ "--usage-file", str(usage_file),
242
+ ]
243
+ )
244
+
245
+ self.assertEqual(exit_code, 0)
246
+ record = json.loads(usage_file.read_text(encoding="utf-8").strip())
247
+ self.assertEqual(record["input_tokens"], 800)
248
+ self.assertEqual(record["output_tokens"], 300)
249
+ self.assertFalse(record["estimated"])
250
+
251
+ def test_explicit_tokens_override_transcript(self) -> None:
252
+ tests_root = Path(__file__).resolve().parent
253
+ with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
254
+ base = Path(tmpdir)
255
+ usage_file = base / "token-usage.jsonl"
256
+ transcript = base / "transcript.md"
257
+ transcript.write_text("Input tokens: 5000\nOutput tokens: 2500\n", encoding="utf-8")
258
+
259
+ exit_code = track_token_usage.main(
260
+ [
261
+ "--stage", "analysis",
262
+ "--source", "copilot-cli",
263
+ "--model", "claude-sonnet-4",
264
+ "--current-datetime", "2026-05-19T08:00:00Z",
265
+ "--input-tokens", "9999",
266
+ "--output-tokens", "4444",
267
+ "--transcript", str(transcript),
268
+ "--usage-file", str(usage_file),
269
+ ]
270
+ )
271
+
272
+ self.assertEqual(exit_code, 0)
273
+ record = json.loads(usage_file.read_text(encoding="utf-8").strip())
274
+ self.assertEqual(record["input_tokens"], 9999)
275
+ self.assertEqual(record["output_tokens"], 4444)
276
+ self.assertFalse(record["estimated"])
277
+
278
+ def test_fallback_to_estimate_when_transcript_has_no_usage(self) -> None:
279
+ tests_root = Path(__file__).resolve().parent
280
+ with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
281
+ base = Path(tmpdir)
282
+ usage_file = base / "token-usage.jsonl"
283
+ prompt_path = base / "prompt.txt"
284
+ output_path = base / "output.md"
285
+ transcript = base / "transcript.md"
286
+ prompt_path.write_text("x" * 40, encoding="utf-8")
287
+ output_path.write_text("y" * 20, encoding="utf-8")
288
+ transcript.write_text("# No usage info here\n", encoding="utf-8")
289
+
290
+ exit_code = track_token_usage.main(
291
+ [
292
+ "--stage", "analysis",
293
+ "--source", "copilot-cli",
294
+ "--model", "claude-sonnet-4",
295
+ "--current-datetime", "2026-05-19T08:00:00Z",
296
+ "--prompt-file", str(prompt_path),
297
+ "--output-file", str(output_path),
298
+ "--transcript", str(transcript),
299
+ "--usage-file", str(usage_file),
300
+ ]
301
+ )
302
+
303
+ self.assertEqual(exit_code, 0)
304
+ record = json.loads(usage_file.read_text(encoding="utf-8").strip())
305
+ self.assertEqual(record["input_tokens"], 10)
306
+ self.assertEqual(record["output_tokens"], 5)
307
+ self.assertTrue(record["estimated"])
308
+
309
+
310
if __name__ == "__main__":
311
unittest.main()