diff --git a/scripts/generate_turn_contract.py b/scripts/generate_turn_contract.py index 1ee2ae77ee..046c19e897 100644 --- a/scripts/generate_turn_contract.py +++ b/scripts/generate_turn_contract.py @@ -217,7 +217,11 @@ def require(condition, message): def build_artifacts(): contract = validate_contract(read_contract()) - digest = hashlib.sha256(CONTRACT_PATH.read_bytes()).hexdigest() + # Git may check the contract out with CRLF on Windows; hash the canonical LF + # bytes so every checkout records the same provenance digest. + digest = hashlib.sha256( + CONTRACT_PATH.read_bytes().replace(b"\r\n", b"\n") + ).hexdigest() header = f"Generated by scripts/generate_turn_contract.py; do not edit.\nSource: loopx/control_plane/turn_loop_controller_contract_v0.json\nSHA256: {digest}" py = [ '"""' + header + '"""', diff --git a/tests/architecture/test_turn_contract_generation.py b/tests/architecture/test_turn_contract_generation.py index 9bd8cf1e28..d2e4c494fe 100644 --- a/tests/architecture/test_turn_contract_generation.py +++ b/tests/architecture/test_turn_contract_generation.py @@ -105,6 +105,18 @@ def test_generator_check_is_deterministic_and_does_not_repair(tmp_path, monkeypa assert path.read_text() == "stale\n" +def test_contract_digest_ignores_checkout_line_endings(tmp_path, monkeypatch): + # A Windows checkout may still carry CRLF when the guard reads the contract. + crlf_contract = tmp_path / generator.CONTRACT_PATH.name + contract_text = generator.CONTRACT_PATH.read_text(encoding="utf-8") + crlf_contract.write_bytes(contract_text.replace("\n", "\r\n").encode("utf-8")) + monkeypatch.setattr(generator, "CONTRACT_PATH", crlf_contract) + assert generator.build_artifacts() == { + generator.PYTHON_PATH: generator.PYTHON_PATH.read_text(encoding="utf-8"), + generator.TYPESCRIPT_PATH: generator.TYPESCRIPT_PATH.read_text(encoding="utf-8"), + } + + def test_typescript_settlement_reexports_generated_result_set(): result = subprocess.run( [