Skip to content

Commit f6c02fe

Browse files
uipreligaclaude
andcommitted
fix(reports): make skipped-task JUnit names platform-independent
_skipped_suite built the testcase name with str(Path(path).with_suffix("")), which yields "tasks\opt" on Windows and "tasks/opt" on Linux. That made the JUnit testcase identity depend on the OS that generated the report, so the same logical run would split into two identities in CI history/flake tracking (and it broke the Windows smoke job). Normalize separators and parse with PurePosixPath so the emitted name is always "/"-separated. Adds a regression test feeding a Windows-style path. Also switch the action's model in the README/tutorial examples from Haiku to Sonnet (docs only; the dogfood job stays on Haiku for per-PR cost). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3613665 commit f6c02fe

4 files changed

Lines changed: 33 additions & 8 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ the job summary, and fails the step on any task/gate failure:
105105
- uses: UiPath/coder_eval@v0 # becomes @v1 once 1.0.0 ships; @vX.Y.Z pins exactly
106106
with:
107107
tasks: tests/tasks/**/*.yaml
108-
model: claude-haiku-4-5-20251001
108+
model: claude-sonnet-5
109109
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
110110
```
111111

‎docs/tutorials/02-ci-pipeline.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ job-summary + fail-on-failure into one step:
172172
- uses: UiPath/coder_eval@v0 # …then run the gate (pin @vX.Y.Z in production)
173173
with:
174174
tasks: tests/tasks/**/*.yaml
175-
model: claude-haiku-4-5-20251001
175+
model: claude-sonnet-5
176176
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
177177
```
178178

‎src/coder_eval/reports_junit.py‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import math
2525
import re
2626
import xml.etree.ElementTree as ET
27-
from pathlib import Path
27+
from pathlib import Path, PurePosixPath
2828
from typing import Any, Literal
2929

3030
from .evaluation.judge_context import truncate
@@ -235,20 +235,32 @@ def _task_case(row: dict[str, Any], run_dir: Path) -> ET.Element:
235235
return case
236236

237237

238+
def _skipped_name(path: str) -> str:
239+
"""Stable testcase name for a skipped task: suffix-stripped, ``/``-separated.
240+
241+
Uses the whole path rather than just the stem, because two skipped tasks
242+
sharing a basename (``suiteA/task.yaml``, ``suiteB/task.yaml``) would
243+
otherwise collapse into one identity that some JUnit ingesters merge.
244+
245+
Separators are normalized to ``/`` and the path is parsed with
246+
``PurePosixPath`` so the emitted name does not depend on the OS that
247+
generated the report — the same logical run must produce the same testcase
248+
identity on Windows and Linux, or CI history/flake tracking splits in two.
249+
"""
250+
return str(PurePosixPath(path.replace("\\", "/")).with_suffix(""))
251+
252+
238253
def _skipped_suite(summary: RunSummary) -> ET.Element | None:
239254
"""Build the synthetic ``skipped`` testsuite from ``RunSummary.skipped_tasks``."""
240255
if not summary.skipped_tasks:
241256
return None
242257
suite = ET.Element("testsuite", {"name": "skipped"})
243258
cases: list[ET.Element] = []
244259
for entry in summary.skipped_tasks:
245-
# Use the suffix-stripped PATH, not just the stem: two skipped tasks
246-
# sharing a basename (suiteA/task.yaml, suiteB/task.yaml) would otherwise
247-
# collapse into one identity that some JUnit ingesters merge.
248260
case = ET.SubElement(
249261
suite,
250262
"testcase",
251-
{"name": _xml_safe(str(Path(entry.path).with_suffix(""))), "classname": "skipped"},
263+
{"name": _xml_safe(_skipped_name(entry.path)), "classname": "skipped"},
252264
)
253265
ET.SubElement(case, "skipped", {"message": _xml_safe(entry.reason)})
254266
cases.append(case)

‎tests/test_reports_junit.py‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,9 +302,12 @@ def test_skipped_tasks_suite(write_run_json: Callable[..., Path], tmp_path: Path
302302
assert ts is not None
303303
skipped_cases = ts.findall("testcase")
304304
assert len(skipped_cases) == 2
305-
# Suffix-stripped full path (not just the stem) so same-basename tasks stay distinct.
305+
# Suffix-stripped full path (not just the stem) so same-basename tasks stay
306+
# distinct. Always '/'-separated: the same logical run must yield the same
307+
# testcase identity on Windows and Linux, or CI history splits in two.
306308
names = {c.get("name") for c in skipped_cases}
307309
assert names == {"tasks/broken", "tasks/opt"}
310+
assert not any("\\" in n for n in names)
308311
assert all(c.find("skipped") is not None for c in skipped_cases)
309312

310313

@@ -580,3 +583,13 @@ def test_skipped_names_unique_for_same_stem(write_run_json: Callable[..., Path],
580583
ts = _find_testsuite(root, "skipped")
581584
names = {c.get("name") for c in ts.findall("testcase")}
582585
assert len(names) == 2, f"skipped testcase names collided: {names}"
586+
587+
588+
def test_skipped_name_is_platform_independent(write_run_json: Callable[..., Path], tmp_path: Path) -> None:
589+
"""A Windows-style path recorded in run.json must still emit a '/'-separated
590+
name, so a report generated on Windows matches one generated on Linux."""
591+
run_dir = tmp_path / "run"
592+
write_run_json(run_dir, [], skipped=[(r"tasks\win\task.yaml", "skip: true")])
593+
root = fromstring(generate_junit_xml(run_dir))
594+
name = _find_testsuite(root, "skipped").find("testcase").get("name")
595+
assert name == "tasks/win/task"

0 commit comments

Comments
 (0)