diff --git a/architecture/notes/scripts/ci/2026-09-23-prohibited-name-range-check.md b/architecture/notes/scripts/ci/2026-09-23-prohibited-name-range-check.md index 836923b..126a155 100644 --- a/architecture/notes/scripts/ci/2026-09-23-prohibited-name-range-check.md +++ b/architecture/notes/scripts/ci/2026-09-23-prohibited-name-range-check.md @@ -14,6 +14,10 @@ The repository already used an ignored `scripts/check_prohibited_names.py` in st - The local checker already owns the prohibited-name list and hook behavior; duplicating the list in workflow YAML would create a second policy source. - Inline copyright/third-party attribution can contain a project name, while Cargo git/source references and protocol/external-format identifiers include key-reporting constants, multiplexer escape sequences, and theme-format variants. +- macOS run 35950152526 rejected a malformed UTF-8 filename before the checker + could run. The fixture now writes a Git tree directly, preserving the same + invalid-path regression without depending on filesystem filename support. + ## Decision - Publish the existing checker as the single policy implementation and add a fail-closed range mode. @@ -34,7 +38,7 @@ New PR/merge-group/main-push text and commit subjects are checked by the existin ## Validation -`scripts/tests/test_prohibited_names.py` covers ordinary comparison failure, inline attribution positive/negative paths, protocol identifiers, Cargo dependency references, GitHub CLI text, range text, range commit messages, diverged related bases, unrelated/missing bases, old-history exclusion, added-then-deleted text, and merge-resolution additions. The checker also covers malformed UTF-8 paths/text and normal Unicode/control-character paths; the raw-byte path fixture runs on POSIX, where that filename can exist. +`scripts/tests/test_prohibited_names.py` covers ordinary comparison failure, inline attribution positive/negative paths, protocol identifiers, Cargo dependency references, GitHub CLI text, range text, range commit messages, diverged related bases, unrelated/missing bases, old-history exclusion, added-then-deleted text, and merge-resolution additions. The checker also covers malformed UTF-8 paths/text and normal Unicode/control-character paths; the malformed-path fixture constructs Git tree objects without checking them out, so it also runs on filesystems that reject those bytes. ## Supersedes diff --git a/scripts/tests/test_prohibited_names.py b/scripts/tests/test_prohibited_names.py index 451d740..fae08ae 100644 --- a/scripts/tests/test_prohibited_names.py +++ b/scripts/tests/test_prohibited_names.py @@ -27,15 +27,35 @@ class ProhibitedNamesTests(unittest.TestCase): env={**os.environ, "PYTHONUTF8": "1"}, ) - @unittest.skipIf(os.name == "nt", "Windows filenames cannot contain raw invalid UTF-8 bytes") def test_invalid_utf8_path_cannot_turn_into_an_empty_diff(self) -> None: with tempfile.TemporaryDirectory() as directory: repository = Path(directory) run_git(repository, "init", "-q") - with open(os.fsencode(repository) + b"/bad-\xff.rs", "wb") as output: - output.write(b"Ghostty\n") - run_git(repository, "add", "--all") - result = self.check_staged(repository) + run_git(repository, "config", "user.name", "CI test") + run_git(repository, "config", "user.email", "ci@example.invalid") + # Git trees can contain these bytes even when the host filesystem + # cannot. Never require a checkout or create an invalid OS filename. + def write_object(kind, contents): + return subprocess.check_output( + ["git", "hash-object", "-w", "-t", kind, "--stdin"], + input=contents, cwd=repository, + ).decode("ascii").strip() + + empty_tree = write_object("tree", b"") + base = subprocess.check_output( + ["git", "commit-tree", empty_tree], input=b"baseline\n", cwd=repository, + ).decode("ascii").strip() + blob = write_object("blob", b"Ghostty\n") + tree = write_object("tree", b"100644 bad-\xff.rs\0" + bytes.fromhex(blob)) + head = subprocess.check_output( + ["git", "commit-tree", tree, "-p", base], + input=b"invalid path fixture\n", cwd=repository, + ).decode("ascii").strip() + result = subprocess.run( + [sys.executable, str(CHECKER), "range", "--base", base, "--head", head], + cwd=repository, text=True, encoding="utf-8", capture_output=True, + env={**os.environ, "PYTHONUTF8": "1"}, + ) self.assertNotEqual(result.returncode, 0) self.assertIn("utf-8", result.stderr.lower())