codetool-shell 0.1.1__py3-none-win_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. codetool_shell/__init__.py +11 -0
  2. codetool_shell/api.py +59 -0
  3. codetool_shell/bin/windows-arm64/codetool-shell-rust.exe +0 -0
  4. codetool_shell/filters/__init__.py +14 -0
  5. codetool_shell/filters/build_compiler/__init__.py +7 -0
  6. codetool_shell/filters/build_compiler/detector.py +412 -0
  7. codetool_shell/filters/build_compiler/reducer.py +166 -0
  8. codetool_shell/filters/build_compiler/summary.py +617 -0
  9. codetool_shell/filters/ci_job_log/__init__.py +7 -0
  10. codetool_shell/filters/ci_job_log/detector.py +64 -0
  11. codetool_shell/filters/ci_job_log/reducer.py +99 -0
  12. codetool_shell/filters/ci_job_log/summary.py +243 -0
  13. codetool_shell/filters/diff/__init__.py +7 -0
  14. codetool_shell/filters/diff/detector.py +136 -0
  15. codetool_shell/filters/diff/reducer.py +308 -0
  16. codetool_shell/filters/generic_log/__init__.py +7 -0
  17. codetool_shell/filters/generic_log/detector.py +175 -0
  18. codetool_shell/filters/generic_log/reducer.py +99 -0
  19. codetool_shell/filters/generic_log/summary.py +161 -0
  20. codetool_shell/filters/git.py +514 -0
  21. codetool_shell/filters/html_cleanup/__init__.py +7 -0
  22. codetool_shell/filters/html_cleanup/detector.py +136 -0
  23. codetool_shell/filters/html_cleanup/reducer.py +27 -0
  24. codetool_shell/filters/html_cleanup/summary.py +422 -0
  25. codetool_shell/filters/json_payload/__init__.py +7 -0
  26. codetool_shell/filters/json_payload/detector.py +62 -0
  27. codetool_shell/filters/json_payload/reducer.py +81 -0
  28. codetool_shell/filters/json_payload/summary.py +233 -0
  29. codetool_shell/filters/listing/__init__.py +7 -0
  30. codetool_shell/filters/listing/detector.py +294 -0
  31. codetool_shell/filters/listing/reducer.py +30 -0
  32. codetool_shell/filters/log_template/__init__.py +7 -0
  33. codetool_shell/filters/log_template/constants.py +76 -0
  34. codetool_shell/filters/log_template/detector.py +331 -0
  35. codetool_shell/filters/log_template/reducer.py +78 -0
  36. codetool_shell/filters/log_template/template.py +280 -0
  37. codetool_shell/filters/log_template/types.py +21 -0
  38. codetool_shell/filters/opaque_payload/__init__.py +7 -0
  39. codetool_shell/filters/opaque_payload/detector.py +563 -0
  40. codetool_shell/filters/opaque_payload/reducer.py +142 -0
  41. codetool_shell/filters/opaque_payload/summary.py +61 -0
  42. codetool_shell/filters/package_manager/__init__.py +7 -0
  43. codetool_shell/filters/package_manager/detector.py +220 -0
  44. codetool_shell/filters/package_manager/reducer.py +110 -0
  45. codetool_shell/filters/package_manager/summary.py +172 -0
  46. codetool_shell/filters/pipeline.py +65 -0
  47. codetool_shell/filters/rg.py +250 -0
  48. codetool_shell/filters/system_output/__init__.py +7 -0
  49. codetool_shell/filters/system_output/detector.py +600 -0
  50. codetool_shell/filters/system_output/reducer.py +331 -0
  51. codetool_shell/filters/system_output/summary.py +164 -0
  52. codetool_shell/filters/table/__init__.py +7 -0
  53. codetool_shell/filters/table/detector.py +244 -0
  54. codetool_shell/filters/table/reducer.py +57 -0
  55. codetool_shell/filters/table/summary.py +37 -0
  56. codetool_shell/filters/test_runner/__init__.py +7 -0
  57. codetool_shell/filters/test_runner/ansi.py +80 -0
  58. codetool_shell/filters/test_runner/detector.py +409 -0
  59. codetool_shell/filters/test_runner/reducer.py +288 -0
  60. codetool_shell/filters/test_runner/summary.py +449 -0
  61. codetool_shell/filters/text.py +38 -0
  62. codetool_shell/filters/traceback/__init__.py +7 -0
  63. codetool_shell/filters/traceback/detector.py +209 -0
  64. codetool_shell/filters/traceback/reducer.py +141 -0
  65. codetool_shell/filters/traceback/summary.py +122 -0
  66. codetool_shell/filters/tree.py +59 -0
  67. codetool_shell/py.typed +0 -0
  68. codetool_shell/python_backend.py +38 -0
  69. codetool_shell/rust_backend.py +254 -0
  70. codetool_shell-0.1.1.dist-info/METADATA +152 -0
  71. codetool_shell-0.1.1.dist-info/RECORD +72 -0
  72. codetool_shell-0.1.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,409 @@
1
+ """Detect supported test-runner output shapes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from enum import Enum
8
+
9
+
10
+ class TestFramework(str, Enum):
11
+ """Supported test-runner families."""
12
+
13
+ PYTEST = "pytest"
14
+ UNITTEST = "unittest"
15
+ VITEST = "vitest"
16
+ PLAYWRIGHT = "playwright"
17
+ CARGO = "cargo"
18
+ GO = "go"
19
+ JEST = "jest"
20
+ MAVEN_SUREFIRE = "maven-surefire"
21
+ GRADLE_TEST = "gradle-test"
22
+ DOTNET_TEST = "dotnet-test"
23
+ RSPEC = "rspec"
24
+ MINITEST = "minitest"
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class TestRunSignal:
29
+ """Detected test-runner family and outcome."""
30
+
31
+ framework: TestFramework
32
+ failed: bool
33
+
34
+
35
+ def detect_test_run(lines: list[str]) -> TestRunSignal | None:
36
+ """Return a strong test-runner signal, or ``None`` for uncertain text."""
37
+
38
+ framework = _detect_framework(lines)
39
+ if framework is None:
40
+ return None
41
+ return TestRunSignal(framework=framework, failed=_has_failure(lines, framework))
42
+
43
+
44
+ def _detect_framework(lines: list[str]) -> TestFramework | None:
45
+ text = "\n".join(lines)
46
+ stripped = [line.strip() for line in lines]
47
+
48
+ if _has_pytest_shape(text, stripped):
49
+ return TestFramework.PYTEST
50
+
51
+ if _has_unittest_shape(stripped):
52
+ return TestFramework.UNITTEST
53
+
54
+ if any(line.lstrip().startswith("RUN ") for line in lines) and any(
55
+ line.startswith(("Test Files", "Tests", "FAIL ")) for line in stripped
56
+ ):
57
+ return TestFramework.VITEST
58
+
59
+ if _has_playwright_shape(stripped):
60
+ return TestFramework.PLAYWRIGHT
61
+
62
+ if any(_is_cargo_running_line(line) for line in stripped) and any(
63
+ line.startswith("test result:") for line in stripped
64
+ ):
65
+ return TestFramework.CARGO
66
+
67
+ if _has_go_test_shape(stripped):
68
+ return TestFramework.GO
69
+
70
+ if _has_jest_shape(stripped):
71
+ return TestFramework.JEST
72
+
73
+ if _has_maven_surefire_shape(stripped):
74
+ return TestFramework.MAVEN_SUREFIRE
75
+
76
+ if _has_gradle_test_shape(stripped):
77
+ return TestFramework.GRADLE_TEST
78
+
79
+ if _has_dotnet_test_shape(stripped):
80
+ return TestFramework.DOTNET_TEST
81
+
82
+ if _has_rspec_shape(stripped):
83
+ return TestFramework.RSPEC
84
+
85
+ if _has_minitest_shape(stripped):
86
+ return TestFramework.MINITEST
87
+
88
+ return None
89
+
90
+
91
+ def _has_failure(lines: list[str], framework: TestFramework) -> bool:
92
+ if any(_line_has_failure(line) for line in lines):
93
+ return True
94
+ if framework is TestFramework.PYTEST:
95
+ return _has_pytest_error(lines)
96
+ if framework is TestFramework.PLAYWRIGHT:
97
+ return _has_playwright_failure(lines)
98
+ if framework in {
99
+ TestFramework.GO,
100
+ TestFramework.JEST,
101
+ TestFramework.MAVEN_SUREFIRE,
102
+ TestFramework.GRADLE_TEST,
103
+ TestFramework.DOTNET_TEST,
104
+ TestFramework.RSPEC,
105
+ TestFramework.MINITEST,
106
+ }:
107
+ return _has_extended_failure(lines)
108
+ return False
109
+
110
+
111
+ def _has_pytest_error(lines: list[str]) -> bool:
112
+ return any(_NONZERO_PYTEST_ERROR_RE.search(line.strip()) is not None for line in lines)
113
+
114
+
115
+ def _has_pytest_shape(text: str, lines: list[str]) -> bool:
116
+ if "test session starts" in text and any(
117
+ line.startswith("collected ") or _is_pytest_terminal_summary(line)
118
+ for line in lines
119
+ ):
120
+ return True
121
+ return _has_pytest_quiet_shape(lines)
122
+
123
+
124
+ def _has_pytest_quiet_shape(lines: list[str]) -> bool:
125
+ return any(_is_pytest_progress_line(line) for line in lines) and any(
126
+ _is_pytest_quiet_summary(line)
127
+ or line.startswith(("FAILED ", "ERROR "))
128
+ or "short test summary info" in line.lower()
129
+ for line in lines
130
+ )
131
+
132
+
133
+ def _line_has_failure(line: str) -> bool:
134
+ stripped = line.strip()
135
+ return (
136
+ stripped.startswith(("FAILED ", "FAILED (", "FAIL ", "FAIL:", "ERROR:", "failures:"))
137
+ or stripped.startswith("test result: FAILED")
138
+ or " FAILURES " in stripped
139
+ or _is_unittest_verbose_failure_line(stripped)
140
+ or _NONZERO_FAILED_RE.search(stripped) is not None
141
+ )
142
+
143
+
144
+ def _is_pytest_terminal_summary(line: str) -> bool:
145
+ return bool(
146
+ line.startswith("=")
147
+ and " in " in line
148
+ and any(
149
+ token in line
150
+ for token in (" passed", " failed", " error", " skipped", " xfailed", " xpassed")
151
+ )
152
+ )
153
+
154
+
155
+ def _is_pytest_quiet_summary(line: str) -> bool:
156
+ return _PYTEST_QUIET_SUMMARY_RE.match(line) is not None
157
+
158
+
159
+ def _is_pytest_progress_line(line: str) -> bool:
160
+ return _PYTEST_PROGRESS_RE.match(line) is not None
161
+
162
+
163
+ def _is_cargo_running_line(line: str) -> bool:
164
+ match = re.match(r"^running (\d+) tests?$", line)
165
+ return match is not None
166
+
167
+
168
+ def _has_unittest_shape(lines: list[str]) -> bool:
169
+ return (
170
+ any(_is_unittest_ran_line(line) for line in lines)
171
+ and any(_is_unittest_terminal_outcome_line(line) for line in lines)
172
+ and any(_is_unittest_shape_marker(line) for line in lines)
173
+ )
174
+
175
+
176
+ def _has_playwright_shape(lines: list[str]) -> bool:
177
+ return any(_is_playwright_run_line(line) for line in lines) and any(
178
+ _is_playwright_summary_line(line) for line in lines
179
+ )
180
+
181
+
182
+ def _has_playwright_failure(lines: list[str]) -> bool:
183
+ return any(_is_playwright_failure_line(line.strip()) for line in lines)
184
+
185
+
186
+ def _has_extended_failure(lines: list[str]) -> bool:
187
+ return any(_is_extended_failure_line(line.strip()) for line in lines)
188
+
189
+
190
+ def _is_playwright_run_line(line: str) -> bool:
191
+ return _PLAYWRIGHT_RUN_RE.match(line) is not None
192
+
193
+
194
+ def _is_playwright_summary_line(line: str) -> bool:
195
+ return _PLAYWRIGHT_SUMMARY_RE.match(line) is not None
196
+
197
+
198
+ def _is_playwright_failure_line(line: str) -> bool:
199
+ return bool(
200
+ _PLAYWRIGHT_NONZERO_FAILURE_RE.match(line)
201
+ or _PLAYWRIGHT_NUMBERED_FAILURE_RE.match(line)
202
+ or ((line.startswith("✘") or line.startswith("×")) and "›" in line)
203
+ or line.startswith(("Test timeout", "TimeoutError:"))
204
+ )
205
+
206
+
207
+ def _has_go_test_shape(lines: list[str]) -> bool:
208
+ has_run = any(_GO_RUN_RE.match(line) is not None for line in lines)
209
+ has_test_result = any(_GO_TEST_RESULT_RE.match(line) is not None for line in lines)
210
+ has_package_result = any(_looks_like_go_package_result(line) for line in lines)
211
+ return (has_run and has_test_result) or has_package_result
212
+
213
+
214
+ def _looks_like_go_package_result(line: str) -> bool:
215
+ match = _GO_PACKAGE_RESULT_RE.match(line)
216
+ if match is None:
217
+ return False
218
+ parts = line.split()
219
+ if len(parts) < 2:
220
+ return False
221
+ package = parts[1]
222
+ return "/" in package or "." in package or package.startswith(("./", "../"))
223
+
224
+
225
+ def _has_jest_shape(lines: list[str]) -> bool:
226
+ has_suite = any(line.startswith(("PASS ", "FAIL ")) for line in lines)
227
+ has_summary = any(
228
+ line.startswith(("Test Suites:", "Tests:", "Snapshots:", "Time:"))
229
+ for line in lines
230
+ )
231
+ return has_suite and has_summary
232
+
233
+
234
+ def _has_maven_surefire_shape(lines: list[str]) -> bool:
235
+ has_surefire = any(
236
+ "surefire" in line.lower()
237
+ or "maven-surefire-plugin" in line.lower()
238
+ or line.startswith("Tests run:")
239
+ or _MAVEN_SUREFIRE_SUMMARY_RE.match(line) is not None
240
+ for line in lines
241
+ )
242
+ has_summary = any(_MAVEN_SUREFIRE_SUMMARY_RE.match(line) is not None for line in lines)
243
+ return has_surefire and has_summary
244
+
245
+
246
+ def _has_gradle_test_shape(lines: list[str]) -> bool:
247
+ has_gradle = any(
248
+ line.startswith(("> Task ", "Gradle Test Executor", "Test worker"))
249
+ or "test report" in line.lower()
250
+ for line in lines
251
+ )
252
+ has_summary = any(_GRADLE_TEST_SUMMARY_RE.match(line) is not None for line in lines)
253
+ return has_gradle and has_summary
254
+
255
+
256
+ def _has_dotnet_test_shape(lines: list[str]) -> bool:
257
+ has_vstest = any(
258
+ line.startswith(("Test run for ", "Starting test execution"))
259
+ or "vstest" in line.lower()
260
+ for line in lines
261
+ )
262
+ has_summary = any(
263
+ line.startswith(("Passed!", "Failed!", "Total tests:"))
264
+ or _DOTNET_TEST_SUMMARY_RE.match(line) is not None
265
+ for line in lines
266
+ )
267
+ return has_vstest and has_summary
268
+
269
+
270
+ def _has_rspec_shape(lines: list[str]) -> bool:
271
+ has_summary = any(_RSPEC_SUMMARY_RE.match(line) is not None for line in lines)
272
+ has_marker = any(
273
+ line.startswith(("Failures:", "Pending:", "Finished in "))
274
+ or _RSPEC_FAILURE_NUMBER_RE.match(line) is not None
275
+ for line in lines
276
+ )
277
+ return has_summary and has_marker
278
+
279
+
280
+ def _has_minitest_shape(lines: list[str]) -> bool:
281
+ has_run_marker = any(line.startswith("Run options:") for line in lines)
282
+ has_summary = any(_MINITEST_SUMMARY_RE.match(line) is not None for line in lines)
283
+ has_progress = any(_MINITEST_PROGRESS_RE.match(line) is not None for line in lines)
284
+ return has_summary and (has_run_marker or has_progress)
285
+
286
+
287
+ def _is_extended_failure_line(line: str) -> bool:
288
+ return (
289
+ line.startswith(
290
+ (
291
+ "--- FAIL:",
292
+ "FAIL",
293
+ "FAIL ",
294
+ "Failed!",
295
+ "Failed tests:",
296
+ "Failures:",
297
+ "Test Failed.",
298
+ "[ERROR]",
299
+ )
300
+ )
301
+ or " FAILED" in line
302
+ or " failed" in line
303
+ or "Failures: " in line
304
+ or "Errors: " in line
305
+ or _GO_PACKAGE_FAIL_RE.match(line) is not None
306
+ or _MAVEN_SUREFIRE_NONZERO_RE.search(line) is not None
307
+ or _DOTNET_FAILED_SUMMARY_RE.search(line) is not None
308
+ or _RSPEC_NONZERO_FAILURE_RE.match(line) is not None
309
+ or _MINITEST_NONZERO_FAILURE_RE.search(line) is not None
310
+ )
311
+
312
+
313
+ def _is_unittest_shape_marker(line: str) -> bool:
314
+ return (
315
+ _is_unittest_separator(line)
316
+ or _is_unittest_progress_line(line)
317
+ or _is_unittest_verbose_result_line(line)
318
+ or line.startswith(("FAIL:", "ERROR:"))
319
+ )
320
+
321
+
322
+ def _is_unittest_ran_line(line: str) -> bool:
323
+ return _UNITTEST_RAN_RE.match(line) is not None
324
+
325
+
326
+ def _is_unittest_terminal_outcome_line(line: str) -> bool:
327
+ return line == "OK" or line.startswith(("OK (", "FAILED ("))
328
+
329
+
330
+ def _is_unittest_separator(line: str) -> bool:
331
+ return len(line) >= 5 and set(line) == {"-"}
332
+
333
+
334
+ def _is_unittest_progress_line(line: str) -> bool:
335
+ return len(line) >= 3 and all(char in _UNITTEST_PROGRESS_CHARS for char in line)
336
+
337
+
338
+ def _is_unittest_verbose_result_line(line: str) -> bool:
339
+ return _UNITTEST_VERBOSE_RESULT_RE.match(line) is not None
340
+
341
+
342
+ def _is_unittest_verbose_failure_line(line: str) -> bool:
343
+ return bool(
344
+ _UNITTEST_VERBOSE_RESULT_RE.match(line)
345
+ and any(line.endswith(f" ... {result}") for result in ("FAIL", "ERROR"))
346
+ )
347
+
348
+
349
+ _NONZERO_FAILED_RE = re.compile(r"\b[1-9]\d* failed\b")
350
+ _NONZERO_PYTEST_ERROR_RE = re.compile(r"\b[1-9]\d* errors?\b")
351
+ _PYTEST_PROGRESS_RE = re.compile(r"^[.FEfsxX]+(?:\s+\[\s*\d+%\])?$")
352
+ _PYTEST_QUIET_SUMMARY_RE = re.compile(
353
+ r"^(?:\d+ (?:passed|failed|errors?|skipped|xfailed|xpassed|warnings?)"
354
+ r"(?:, \d+ (?:passed|failed|errors?|skipped|xfailed|xpassed|warnings?))*"
355
+ r"|no tests ran) in [0-9.]+s$"
356
+ )
357
+ _UNITTEST_RAN_RE = re.compile(r"^Ran \d+ tests? in [0-9.]+s$")
358
+ _UNITTEST_VERBOSE_RESULT_RE = re.compile(
359
+ r"^[A-Za-z_][\w.<>-]*\s+\(.+\)\s+\.\.\.\s+"
360
+ r"(?:ok|FAIL|ERROR|skipped|expected failure|unexpected success)(?:\s+.*)?$"
361
+ )
362
+ _UNITTEST_PROGRESS_CHARS = frozenset(".FEsxXuUS")
363
+ _PLAYWRIGHT_RUN_RE = re.compile(r"^Running \d+ tests? using \d+ workers?$")
364
+ _PLAYWRIGHT_SUMMARY_RE = re.compile(
365
+ r"^\d+ (?:passed|failed|flaky|skipped|timed out|did not run|interrupted)"
366
+ r"(?: \([^)]*\))?$"
367
+ )
368
+ _PLAYWRIGHT_NONZERO_FAILURE_RE = re.compile(
369
+ r"^[1-9]\d* (?:failed|timed out|interrupted)(?: \([^)]*\))?$"
370
+ )
371
+ _PLAYWRIGHT_NUMBERED_FAILURE_RE = re.compile(r"^\d+\) .+›")
372
+ _GO_RUN_RE = re.compile(r"^=== RUN\s+\S+")
373
+ _GO_TEST_RESULT_RE = re.compile(r"^--- (?:PASS|FAIL|SKIP):\s+\S+\s+\([0-9.]+s\)$")
374
+ _GO_PACKAGE_RESULT_RE = re.compile(
375
+ r"^(?:ok|FAIL|\?)\s+\S+(?:\s+[0-9.]+s|\s+\[no test files\])$"
376
+ )
377
+ _GO_PACKAGE_FAIL_RE = re.compile(r"^FAIL\s+\S+(?:\s+[0-9.]+s)?$")
378
+ _MAVEN_SUREFIRE_SUMMARY_RE = re.compile(
379
+ r"^(?:\[ERROR\]\s+)?Tests run:\s+\d+,\s+Failures:\s+\d+,\s+Errors:\s+\d+,\s+Skipped:\s+\d+",
380
+ re.IGNORECASE,
381
+ )
382
+ _MAVEN_SUREFIRE_NONZERO_RE = re.compile(
383
+ r"(?:Failures|Errors):\s*[1-9]\d*",
384
+ re.IGNORECASE,
385
+ )
386
+ _GRADLE_TEST_SUMMARY_RE = re.compile(
387
+ r"^\d+\s+tests? completed(?:,\s+\d+\s+failed)?(?:,\s+\d+\s+skipped)?$",
388
+ re.IGNORECASE,
389
+ )
390
+ _DOTNET_TEST_SUMMARY_RE = re.compile(
391
+ r"^(?:Passed|Failed|Skipped):\s+\d+",
392
+ re.IGNORECASE,
393
+ )
394
+ _DOTNET_FAILED_SUMMARY_RE = re.compile(r"Failed:\s*[1-9]\d*", re.IGNORECASE)
395
+ _RSPEC_SUMMARY_RE = re.compile(
396
+ r"^\d+\s+examples?,\s+\d+\s+failures?(?:,\s+\d+\s+pending)?$",
397
+ re.IGNORECASE,
398
+ )
399
+ _RSPEC_FAILURE_NUMBER_RE = re.compile(r"^\d+\)\s+")
400
+ _RSPEC_NONZERO_FAILURE_RE = re.compile(r"^\d+\s+examples?,\s+[1-9]\d*\s+failures?", re.I)
401
+ _MINITEST_SUMMARY_RE = re.compile(
402
+ r"^\d+\s+runs?,\s+\d+\s+assertions?,\s+\d+\s+failures?,\s+\d+\s+errors?",
403
+ re.IGNORECASE,
404
+ )
405
+ _MINITEST_PROGRESS_RE = re.compile(r"^[.FESE]+$")
406
+ _MINITEST_NONZERO_FAILURE_RE = re.compile(
407
+ r"[1-9]\d*\s+(?:failures?|errors?)",
408
+ re.IGNORECASE,
409
+ )
@@ -0,0 +1,288 @@
1
+ """Conservative reducers for supported test-runner output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from ..text import join_preserving_final_newline, score, split_preserving_final_newline
9
+ from .ansi import strip_ansi_control_sequences
10
+ from .detector import TestFramework, TestRunSignal, detect_test_run
11
+ from .summary import (
12
+ collapse_unittest_progress_line,
13
+ extract_playwright_project,
14
+ is_failure_detail_line,
15
+ is_jest_source_context_line,
16
+ is_playwright_attention_line,
17
+ is_playwright_run_line,
18
+ is_run_marker_line,
19
+ is_summary_line,
20
+ is_unittest_progress_line,
21
+ normalize_selected_line,
22
+ )
23
+
24
+
25
+ def compress_test_runner_output(text: str) -> str:
26
+ """Compress clear test-runner output while preserving diagnostics."""
27
+
28
+ sanitized = strip_ansi_control_sequences(text)
29
+ lines, final_newline = split_preserving_final_newline(sanitized)
30
+ go_jsonl_candidate = _reduce_go_jsonl_lines(lines)
31
+ if go_jsonl_candidate is not None:
32
+ candidate = join_preserving_final_newline(go_jsonl_candidate, final_newline)
33
+ if score(candidate) < score(text):
34
+ return candidate
35
+
36
+ signal = detect_test_run(lines)
37
+ if signal is None:
38
+ return text
39
+
40
+ candidate_lines = _reduce_lines(lines, signal)
41
+ if len(candidate_lines) < 2:
42
+ return text
43
+
44
+ candidate = join_preserving_final_newline(candidate_lines, final_newline)
45
+ if score(candidate) < score(text):
46
+ return candidate
47
+ return text
48
+
49
+
50
+ def _reduce_lines(lines: list[str], signal: TestRunSignal) -> list[str]:
51
+ if signal.framework is TestFramework.PLAYWRIGHT:
52
+ return _reduce_playwright_lines(lines, signal)
53
+
54
+ outcome = "failure" if signal.failed else "success"
55
+ selected = [f"test output: {signal.framework.value} {outcome}"]
56
+
57
+ for line in lines:
58
+ if (
59
+ signal.framework is TestFramework.UNITTEST
60
+ and not signal.failed
61
+ and is_unittest_progress_line(line)
62
+ ):
63
+ selected.append(collapse_unittest_progress_line(line))
64
+ continue
65
+
66
+ if signal.failed:
67
+ keep = is_summary_line(line) or is_failure_detail_line(line)
68
+ if signal.framework is TestFramework.JEST and is_jest_source_context_line(line):
69
+ keep = True
70
+ else:
71
+ keep = is_summary_line(line) or is_run_marker_line(line)
72
+ if keep:
73
+ selected.append(normalize_selected_line(line))
74
+
75
+ return _drop_blank_and_adjacent_duplicates(selected)
76
+
77
+
78
+ def _reduce_playwright_lines(lines: list[str], signal: TestRunSignal) -> list[str]:
79
+ outcome = "failure" if signal.failed else "success"
80
+ selected = [f"test output: {signal.framework.value} {outcome}"]
81
+ projects = _playwright_projects(lines)
82
+ projects_added = False
83
+
84
+ for line in lines:
85
+ keep = False
86
+ if is_summary_line(line):
87
+ keep = True
88
+ elif signal.failed:
89
+ keep = is_failure_detail_line(line)
90
+ else:
91
+ keep = is_playwright_attention_line(line)
92
+
93
+ if not keep:
94
+ continue
95
+
96
+ selected.append(normalize_selected_line(line))
97
+ if (
98
+ not projects_added
99
+ and projects
100
+ and is_playwright_run_line(line)
101
+ ):
102
+ selected.append(f"projects: {', '.join(projects)}")
103
+ projects_added = True
104
+
105
+ return _drop_blank_and_adjacent_duplicates(selected)
106
+
107
+
108
+ def _reduce_go_jsonl_lines(lines: list[str]) -> list[str] | None:
109
+ records = _parse_go_jsonl_records(lines)
110
+ if records is None:
111
+ return None
112
+
113
+ failed = any(record.get("Action") == "fail" for record in records)
114
+ failed_tests = _go_jsonl_tests_by_action(records, "fail")
115
+ skipped_tests = _go_jsonl_tests_by_action(records, "skip")
116
+ selected = [f"test output: go {'failure' if failed else 'success'}"]
117
+
118
+ for record in records:
119
+ action = record.get("Action")
120
+ package = _string_field(record, "Package")
121
+ test = _string_field(record, "Test")
122
+
123
+ if action == "output":
124
+ output = _string_field(record, "Output")
125
+ if output is None:
126
+ continue
127
+ detail = output.rstrip("\n").strip()
128
+ if not detail:
129
+ continue
130
+ key = (package or "", test or "")
131
+ if (
132
+ key in failed_tests
133
+ and _is_go_jsonl_relevant_output(detail, failed=True)
134
+ ) or (
135
+ key in skipped_tests
136
+ and _is_go_jsonl_relevant_output(detail, failed=False)
137
+ ) or _is_go_jsonl_global_output(detail):
138
+ selected.append(detail)
139
+ continue
140
+
141
+ if action in {"fail", "skip"} and test is not None:
142
+ selected.append(_format_go_jsonl_test_event(action, package, test, record))
143
+ continue
144
+
145
+ if action in {"pass", "fail", "skip"} and package is not None and test is None:
146
+ selected.append(_format_go_jsonl_package_event(action, package, record))
147
+
148
+ return _drop_blank_and_adjacent_duplicates(selected)
149
+
150
+
151
+ def _parse_go_jsonl_records(lines: list[str]) -> list[dict[str, Any]] | None:
152
+ records: list[dict[str, Any]] = []
153
+ for line in lines:
154
+ stripped = line.strip()
155
+ if not stripped:
156
+ continue
157
+ try:
158
+ parsed = json.loads(stripped)
159
+ except json.JSONDecodeError:
160
+ return None
161
+ if not isinstance(parsed, dict):
162
+ return None
163
+ action = parsed.get("Action")
164
+ if action not in _GO_JSONL_ACTIONS:
165
+ return None
166
+ if not isinstance(parsed.get("Package"), str) and not isinstance(
167
+ parsed.get("Test"), str
168
+ ):
169
+ return None
170
+ records.append(parsed)
171
+
172
+ if len(records) < 3 or not _has_go_jsonl_test_signal(records):
173
+ return None
174
+ return records
175
+
176
+
177
+ def _has_go_jsonl_test_signal(records: list[dict[str, Any]]) -> bool:
178
+ has_terminal = any(record.get("Action") in {"pass", "fail", "skip"} for record in records)
179
+ has_output_marker = any(
180
+ record.get("Action") == "output"
181
+ and isinstance(record.get("Output"), str)
182
+ and record["Output"].strip().startswith(("=== RUN", "--- PASS:", "--- FAIL:", "--- SKIP:"))
183
+ for record in records
184
+ )
185
+ has_go_test_name = any(
186
+ isinstance(record.get("Test"), str)
187
+ and record["Test"].startswith(("Test", "Benchmark", "Example"))
188
+ for record in records
189
+ )
190
+ has_go_package = any(
191
+ isinstance(record.get("Package"), str)
192
+ and ("/" in record["Package"] or "." in record["Package"])
193
+ for record in records
194
+ )
195
+ return has_terminal and has_output_marker and (has_go_test_name or has_go_package)
196
+
197
+
198
+ def _go_jsonl_tests_by_action(
199
+ records: list[dict[str, Any]], action: str
200
+ ) -> set[tuple[str, str]]:
201
+ tests: set[tuple[str, str]] = set()
202
+ for record in records:
203
+ package = _string_field(record, "Package")
204
+ test = _string_field(record, "Test")
205
+ if record.get("Action") == action and test is not None:
206
+ tests.add((package or "", test))
207
+ return tests
208
+
209
+
210
+ def _format_go_jsonl_test_event(
211
+ action: Any, package: str | None, test: str, record: dict[str, Any]
212
+ ) -> str:
213
+ label = "FAIL" if action == "fail" else "SKIP"
214
+ package_prefix = f"{package} " if package else ""
215
+ elapsed = _format_go_jsonl_elapsed(record)
216
+ return f"{label} {package_prefix}{test}{elapsed}"
217
+
218
+
219
+ def _format_go_jsonl_package_event(
220
+ action: Any, package: str, record: dict[str, Any]
221
+ ) -> str:
222
+ label = {"pass": "ok", "fail": "FAIL", "skip": "SKIP"}[action]
223
+ return f"{label} {package}{_format_go_jsonl_elapsed(record)}"
224
+
225
+
226
+ def _format_go_jsonl_elapsed(record: dict[str, Any]) -> str:
227
+ elapsed = record.get("Elapsed")
228
+ if isinstance(elapsed, (int, float)):
229
+ return f" ({elapsed:g}s)"
230
+ return ""
231
+
232
+
233
+ def _is_go_jsonl_relevant_output(detail: str, *, failed: bool) -> bool:
234
+ if detail.startswith(("=== RUN", "=== PAUSE", "=== CONT", "--- PASS:")):
235
+ return False
236
+ if detail.startswith(("--- FAIL:", "--- SKIP:", "FAIL", "panic:", "fatal error:")):
237
+ return True
238
+ if detail.startswith(
239
+ (
240
+ "Error Trace:",
241
+ "Error:",
242
+ "Messages:",
243
+ "Expected:",
244
+ "Actual:",
245
+ "Received:",
246
+ "Not equal:",
247
+ )
248
+ ):
249
+ return True
250
+ if ".go:" in detail:
251
+ return True
252
+ if not failed:
253
+ return "skip" in detail.lower()
254
+ return len(detail) <= 240
255
+
256
+
257
+ def _is_go_jsonl_global_output(detail: str) -> bool:
258
+ return detail == "FAIL" or detail.startswith(("FAIL\t", "ok \t", "? \t"))
259
+
260
+
261
+ def _string_field(record: dict[str, Any], field: str) -> str | None:
262
+ value = record.get(field)
263
+ return value if isinstance(value, str) else None
264
+
265
+
266
+ def _playwright_projects(lines: list[str]) -> list[str]:
267
+ projects: list[str] = []
268
+ for line in lines:
269
+ project = extract_playwright_project(line)
270
+ if project is not None and project not in projects:
271
+ projects.append(project)
272
+ return projects
273
+
274
+
275
+ def _drop_blank_and_adjacent_duplicates(lines: list[str]) -> list[str]:
276
+ output: list[str] = []
277
+ for line in lines:
278
+ if not line:
279
+ continue
280
+ if output and output[-1] == line:
281
+ continue
282
+ output.append(line)
283
+ return output
284
+
285
+
286
+ _GO_JSONL_ACTIONS = frozenset(
287
+ {"start", "run", "pause", "cont", "pass", "bench", "fail", "output", "skip"}
288
+ )