mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 08:52:45 +00:00
fix(tests): stabilize contract digests across line endings
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
/** Portable digest helpers for governed UTF-8 text contracts. */
|
||||
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
|
||||
export function stableTextDigest(filePath) {
|
||||
// IMPORTANT: normalize text newlines; raw hashing breaks frozen contracts after Windows checkout.
|
||||
const normalized = fs.readFileSync(filePath, "utf8").replace(/\r\n?/g, "\n");
|
||||
return crypto.createHash("sha256").update(normalized, "utf8").digest("hex");
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Portable digest and write helpers for governed text contracts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def normalize_text_newlines(payload: bytes) -> bytes:
|
||||
"""Return text bytes with CRLF and lone CR represented as LF."""
|
||||
# IMPORTANT: normalize text newlines; raw hashing breaks frozen contracts after Windows checkout.
|
||||
return payload.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
|
||||
|
||||
|
||||
def stable_text_digest(path: Path) -> str:
|
||||
"""Hash governed text independently of checkout newline representation."""
|
||||
return hashlib.sha256(normalize_text_newlines(path.read_bytes())).hexdigest()
|
||||
|
||||
|
||||
def write_text_lf(path: Path, text: str) -> None:
|
||||
"""Write UTF-8 contract text with explicit LF newlines on every platform."""
|
||||
with path.open("w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write(text)
|
||||
@@ -14,6 +14,8 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from scripts.contract_digest import stable_text_digest, write_text_lf # noqa: E402
|
||||
|
||||
CONTRACT_PATH = ROOT / "tests" / "api_config_contract_r221.json"
|
||||
|
||||
|
||||
@@ -126,10 +128,8 @@ def build_contract() -> dict[str, Any]:
|
||||
"tests.test_r219_exception_boundary_phase2",
|
||||
],
|
||||
},
|
||||
"r220_route_contract_sha256": hashlib.sha256(
|
||||
route_contract.read_bytes()
|
||||
).hexdigest(),
|
||||
"openapi_sha256": hashlib.sha256(openapi.read_bytes()).hexdigest(),
|
||||
"r220_route_contract_sha256": stable_text_digest(route_contract),
|
||||
"openapi_sha256": stable_text_digest(openapi),
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ def main() -> int:
|
||||
args = parser.parse_args()
|
||||
actual = build_contract()
|
||||
if args.write_baseline:
|
||||
CONTRACT_PATH.write_text(_canonical_json(actual), encoding="utf-8")
|
||||
write_text_lf(CONTRACT_PATH, _canonical_json(actual))
|
||||
print(f"API-CONFIG-CONTRACT-WRITTEN: {CONTRACT_PATH}")
|
||||
return 0
|
||||
expected = json.loads(CONTRACT_PATH.read_text(encoding="utf-8"))
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import sys
|
||||
@@ -15,6 +14,8 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from scripts.contract_digest import stable_text_digest, write_text_lf # noqa: E402
|
||||
|
||||
CONTRACT_PATH = ROOT / "tests" / "api_route_contract_r220.json"
|
||||
|
||||
|
||||
@@ -122,7 +123,6 @@ def build_contract() -> dict[str, Any]:
|
||||
"trace_handler",
|
||||
)
|
||||
}
|
||||
openapi_bytes = (ROOT / "docs" / "openapi.yaml").read_bytes()
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"registration_order": [
|
||||
@@ -149,7 +149,7 @@ def build_contract() -> dict[str, Any]:
|
||||
"families": families,
|
||||
"facade_signatures": facade,
|
||||
"facade_metadata": metadata,
|
||||
"openapi_sha256": hashlib.sha256(openapi_bytes).hexdigest(),
|
||||
"openapi_sha256": stable_text_digest(ROOT / "docs" / "openapi.yaml"),
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ def main() -> int:
|
||||
args = parser.parse_args()
|
||||
actual = build_contract()
|
||||
if args.write_baseline:
|
||||
CONTRACT_PATH.write_text(_canonical_json(actual), encoding="utf-8")
|
||||
write_text_lf(CONTRACT_PATH, _canonical_json(actual))
|
||||
print(f"API-ROUTE-CONTRACT-WRITTEN: {CONTRACT_PATH}")
|
||||
return 0
|
||||
expected = json.loads(CONTRACT_PATH.read_text(encoding="utf-8"))
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import sys
|
||||
@@ -15,6 +14,9 @@ from typing import Any
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from scripts.contract_digest import stable_text_digest, write_text_lf # noqa: E402
|
||||
|
||||
CONTRACT_PATH = ROOT / "tests" / "connector_router_contract_r222.json"
|
||||
|
||||
|
||||
@@ -75,7 +77,7 @@ def build_contract() -> dict[str, Any]:
|
||||
"api_route_contract_r220.json",
|
||||
):
|
||||
path = ROOT / "tests" / filename
|
||||
digests[filename] = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
digests[filename] = stable_text_digest(path)
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"constructor_signature": str(inspect.signature(CommandRouter)),
|
||||
@@ -116,7 +118,7 @@ def main() -> int:
|
||||
args = parser.parse_args()
|
||||
actual = build_contract()
|
||||
if args.write_baseline:
|
||||
CONTRACT_PATH.write_text(_canonical_json(actual), encoding="utf-8")
|
||||
write_text_lf(CONTRACT_PATH, _canonical_json(actual))
|
||||
print(f"CONNECTOR-ROUTER-CONTRACT-WRITTEN: {CONTRACT_PATH}")
|
||||
return 0
|
||||
expected = json.loads(CONTRACT_PATH.read_text(encoding="utf-8"))
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/** Verify the frozen R224 Settings/API frontend contract. */
|
||||
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { stableTextDigest } from "./contract_digest.mjs";
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const CONTRACT_PATH = path.join(ROOT, "web", "tests", "fixtures", "frontend_decomposition_contract_r224.json");
|
||||
@@ -42,7 +42,7 @@ function methodSignatures(source) {
|
||||
}
|
||||
|
||||
function digest(relativePath) {
|
||||
return crypto.createHash("sha256").update(fs.readFileSync(path.join(ROOT, relativePath))).digest("hex");
|
||||
return stableTextDigest(path.join(ROOT, relativePath));
|
||||
}
|
||||
|
||||
export function buildContract() {
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import sys
|
||||
@@ -15,6 +14,9 @@ from typing import Any
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from scripts.contract_digest import stable_text_digest, write_text_lf # noqa: E402
|
||||
|
||||
CONTRACT_PATH = ROOT / "tests" / "platform_adapter_contract_r223.json"
|
||||
|
||||
|
||||
@@ -131,9 +133,7 @@ def build_contract() -> dict[str, Any]:
|
||||
"tests.test_f74_reply_visibility_policy",
|
||||
"tests.security.test_s80_connector_ingress",
|
||||
],
|
||||
"router_contract_digest": hashlib.sha256(
|
||||
router_contract.read_bytes()
|
||||
).hexdigest(),
|
||||
"router_contract_digest": stable_text_digest(router_contract),
|
||||
}
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ def main() -> int:
|
||||
args = parser.parse_args()
|
||||
actual = build_contract()
|
||||
if args.write_baseline:
|
||||
CONTRACT_PATH.write_text(_canonical_json(actual), encoding="utf-8")
|
||||
write_text_lf(CONTRACT_PATH, _canonical_json(actual))
|
||||
print(f"PLATFORM-ADAPTER-CONTRACT-WRITTEN: {CONTRACT_PATH}")
|
||||
return 0
|
||||
expected = json.loads(CONTRACT_PATH.read_text(encoding="utf-8"))
|
||||
|
||||
@@ -199,7 +199,7 @@
|
||||
"requires_key": true
|
||||
}
|
||||
],
|
||||
"r220_route_contract_sha256": "17c804dc80f35ddf774e5a941ab4fd4404f55f1e37a8cafd3b3a9238a5e12c9e",
|
||||
"r220_route_contract_sha256": "947979f978f3541dce6b005c38255177322234a24b47ee0dd22a49e234eeed27",
|
||||
"schema_version": 1,
|
||||
"settings_schema_sha256": "e129472bd8b4fb81181c2a3169ed6177757cb54050276646eea70052007bd10b"
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
],
|
||||
"schema_version": 1,
|
||||
"upstream_contract_digests": {
|
||||
"api_config_contract_r221.json": "496063d3a838c5cfa884952215049054cdf6ff030c98a5b02802ae7f64f2d57e",
|
||||
"api_route_contract_r220.json": "17c804dc80f35ddf774e5a941ab4fd4404f55f1e37a8cafd3b3a9238a5e12c9e"
|
||||
"api_config_contract_r221.json": "a0a4aec0ba68692bf8bfac411fb951f95589006ba3d5d6ad463569d24dfc1245",
|
||||
"api_route_contract_r220.json": "947979f978f3541dce6b005c38255177322234a24b47ee0dd22a49e234eeed27"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
"tests.test_f74_reply_visibility_policy",
|
||||
"tests.security.test_s80_connector_ingress"
|
||||
],
|
||||
"router_contract_digest": "78360537c9129f1671d1c62b663b1ea8a53d5b29fe067e2d73ab7c6bad5824aa",
|
||||
"router_contract_digest": "1ba1ba79dba58571f120a244923ef957ce367ddde44f74d690611f6306a6dde7",
|
||||
"schema_version": 1,
|
||||
"slack": {
|
||||
"class_constants": {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ContractDigestPortabilityTests(unittest.TestCase):
|
||||
def test_text_digest_is_newline_invariant_but_content_sensitive(self) -> None:
|
||||
spec = importlib.util.find_spec("scripts.contract_digest")
|
||||
self.assertIsNotNone(spec, "shared contract digest helper must exist")
|
||||
from scripts.contract_digest import stable_text_digest
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
variants = {
|
||||
"lf.txt": b"alpha\nbeta\n",
|
||||
"crlf.txt": b"alpha\r\nbeta\r\n",
|
||||
"cr.txt": b"alpha\rbeta\r",
|
||||
}
|
||||
for name, payload in variants.items():
|
||||
(root / name).write_bytes(payload)
|
||||
(root / "changed.txt").write_bytes(b"alpha\ngamma\n")
|
||||
|
||||
normalized = {stable_text_digest(root / name) for name in variants}
|
||||
self.assertEqual(len(normalized), 1)
|
||||
self.assertNotEqual(
|
||||
stable_text_digest(root / "lf.txt"),
|
||||
stable_text_digest(root / "changed.txt"),
|
||||
)
|
||||
|
||||
def test_text_writer_emits_utf8_lf_bytes(self) -> None:
|
||||
spec = importlib.util.find_spec("scripts.contract_digest")
|
||||
self.assertIsNotNone(spec, "shared contract digest helper must exist")
|
||||
from scripts.contract_digest import write_text_lf
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
target = Path(temp_dir) / "contract.json"
|
||||
write_text_lf(target, '{\n "label": "測試"\n}\n')
|
||||
payload = target.read_bytes()
|
||||
self.assertNotIn(b"\r", payload)
|
||||
self.assertEqual(payload.decode("utf-8"), '{\n "label": "測試"\n}\n')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -173,8 +173,8 @@
|
||||
]
|
||||
},
|
||||
"upstream_contract_digests": {
|
||||
"tests/api_route_contract_r220.json": "17c804dc80f35ddf774e5a941ab4fd4404f55f1e37a8cafd3b3a9238a5e12c9e",
|
||||
"tests/api_config_contract_r221.json": "496063d3a838c5cfa884952215049054cdf6ff030c98a5b02802ae7f64f2d57e",
|
||||
"tests/platform_adapter_contract_r223.json": "a23402432a51a57ad13a5de777f3c34b6d58743179524c09a90b004f98f3dd2c"
|
||||
"tests/api_route_contract_r220.json": "947979f978f3541dce6b005c38255177322234a24b47ee0dd22a49e234eeed27",
|
||||
"tests/api_config_contract_r221.json": "a0a4aec0ba68692bf8bfac411fb951f95589006ba3d5d6ad463569d24dfc1245",
|
||||
"tests/platform_adapter_contract_r223.json": "fb72a77ad8b0ce693c27b34d757f5d47d5be0ebcdd39732c34a1ee535f899b6a"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
const helperPath = path.resolve(process.cwd(), "scripts", "contract_digest.mjs");
|
||||
const temporaryDirectories = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const directory of temporaryDirectories.splice(0)) {
|
||||
fs.rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("contract digest portability", () => {
|
||||
it("normalizes text newlines without hiding content changes", async () => {
|
||||
expect(fs.existsSync(helperPath), "shared contract digest helper must exist").toBe(true);
|
||||
const { stableTextDigest } = await import(helperPath);
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-contract-digest-"));
|
||||
temporaryDirectories.push(root);
|
||||
const variants = {
|
||||
"lf.txt": "alpha\nbeta\n",
|
||||
"crlf.txt": "alpha\r\nbeta\r\n",
|
||||
"cr.txt": "alpha\rbeta\r",
|
||||
};
|
||||
for (const [name, content] of Object.entries(variants)) {
|
||||
fs.writeFileSync(path.join(root, name), content, "utf8");
|
||||
}
|
||||
fs.writeFileSync(path.join(root, "changed.txt"), "alpha\ngamma\n", "utf8");
|
||||
|
||||
const normalized = new Set(
|
||||
Object.keys(variants).map((name) => stableTextDigest(path.join(root, name))),
|
||||
);
|
||||
expect(normalized.size).toBe(1);
|
||||
expect(stableTextDigest(path.join(root, "changed.txt"))).not.toBe(
|
||||
stableTextDigest(path.join(root, "lf.txt")),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user