Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions python/tests/test_strix_scenario_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ def test_validate_scenario_read_errors_are_public_safe(tmp_path):
assert str(tmp_path) not in result["errors"][0]


def test_public_exception_message_redacts_windows_paths():
module = _load_module()
windows_path = "C:\\tmp\\missing.yaml"
error = FileNotFoundError(2, "No such file or directory", windows_path)

sanitized = module.public_exception_message(error, Path(windows_path))

assert windows_path not in sanitized
assert repr(windows_path)[1:-1] not in sanitized
assert "<external>/missing.yaml" in sanitized


def test_public_scenarios_satisfy_contract():
module = _load_module()
scenario_dir = Path(__file__).resolve().parents[2] / "sim" / "scenarios"
Expand Down
20 changes: 16 additions & 4 deletions scripts/strix_scenario_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import json
import re
import sys
from pathlib import Path
from pathlib import Path, PureWindowsPath
from typing import Any

try:
Expand All @@ -29,12 +29,24 @@
def public_path(path: Path) -> str:
"""Return a report-safe path without leaking local checkout layout."""

path_str = str(path)
if re.match(r"^[A-Za-z]:[\\/]", path_str) or path_str.startswith("\\\\"):
name = PureWindowsPath(path_str).name or "."
return f"<external>/{name}"
Comment on lines +33 to +35
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check root-relative paths before Windows redaction

On Windows, every absolute in-repo path (for example C:\repo\sim\scenarios\a.yaml) matches this drive-prefix branch first, so public_path() now returns <external>/a.yaml instead of the previous repo-relative path. That regresses report fidelity for normal runs on Windows and can make outputs ambiguous when consumers expect stable project-relative paths; this branch should run only after the is_relative_to(ROOT) check so in-repo files keep their relative location while external Windows paths are still redacted.

Useful? React with 👍 / 👎.

if path.is_relative_to(ROOT):
return str(path.relative_to(ROOT))
Comment on lines +33 to 37
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public_path() checks for a Windows drive/UNC prefix before path.is_relative_to(ROOT). On Windows, any absolute repo path (including ones under ROOT) will match ^[A-Za-z]:[\\/] and be treated as <external>/..., so internal scenario/report paths won’t be reported relative to the repo anymore. Consider checking is_relative_to(ROOT) first (and/or only applying the Windows-prefix heuristic when the path is not absolute on the current platform) so in-repo paths still return str(path.relative_to(ROOT)) on Windows.

Suggested change
if re.match(r"^[A-Za-z]:[\\/]", path_str) or path_str.startswith("\\\\"):
name = PureWindowsPath(path_str).name or "."
return f"<external>/{name}"
if path.is_relative_to(ROOT):
return str(path.relative_to(ROOT))
if path.is_relative_to(ROOT):
return str(path.relative_to(ROOT))
if re.match(r"^[A-Za-z]:[\\/]", path_str) or path_str.startswith("\\\\"):
name = PureWindowsPath(path_str).name or "."
return f"<external>/{name}"

Copilot uses AI. Check for mistakes.
if path.is_absolute():
name = path.name or "."
return f"<external>/{name}"
return str(path)
return path_str


def redaction_variants(value: str) -> set[str]:
variants = {value}
variants.add(repr(value)[1:-1])
variants.add(value.replace("\\", "\\\\"))
variants.add(value.replace("\\\\", "\\"))
return {variant for variant in variants if variant}


def public_exception_message(exc: Exception, path: Path) -> str:
Expand All @@ -50,8 +62,8 @@ def public_exception_message(exc: Exception, path: Path) -> str:
replacements[str(candidate_path)] = public_path(candidate_path)

for raw, safe in replacements.items():
if raw:
message = message.replace(raw, safe)
for variant in redaction_variants(raw):
message = message.replace(variant, safe)
return message


Expand Down
Loading