Skip to content

Commit 716d0e9

Browse files
authored
fix(v7): resolve packaging, tsc path portability, and golden newline issues (#219)
* fix(v7): resolve packaging module errors, make tsc paths platform-independent, and ensure golden corpus binary line-ending stability * fix(v7): remove trailing whitespace from test files * refactor(tests): centralize local TypeScript compiler resolution
1 parent 34005d1 commit 716d0e9

10 files changed

Lines changed: 109 additions & 12 deletions

src/validation/golden_corpus.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,34 @@ def corpus_records() -> dict[str, list[dict[str, object]]]:
5151
}
5252

5353

54+
def canonical_content_hash(content: str | bytes) -> str:
55+
"""Compute the SHA-256 hash of a UTF-8 string with normalized LF line endings.
56+
57+
Newline Rule: All occurrences of CRLF (\\r\\n) are replaced with LF (\\n).
58+
Encoding: UTF-8.
59+
Invalid UTF-8: Input bytes that are not valid UTF-8 will raise a UnicodeDecodeError.
60+
"""
61+
if isinstance(content, bytes):
62+
text = content.decode("utf-8")
63+
else:
64+
text = content
65+
normalized = text.replace("\r\n", "\n")
66+
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
67+
68+
5469
def write_golden_corpus(root: Path = GOLDEN_ROOT) -> dict[str, str]:
5570
root.mkdir(parents=True, exist_ok=True)
5671
hashes: dict[str, str] = {}
5772
for filename, records in corpus_records().items():
5873
path = root / filename
5974
lines = [json.dumps(record, sort_keys=True, separators=(",", ":"), ensure_ascii=False) for record in records]
6075
content = "\n".join(lines) + "\n"
61-
if path.exists() and path.read_text(encoding="utf-8") != content:
62-
raise RuntimeError(f"golden corpus mutation detected: {path}")
63-
path.write_text(content, encoding="utf-8")
64-
hashes[filename] = hashlib.sha256(content.encode()).hexdigest()
76+
if path.exists():
77+
existing = path.read_text(encoding="utf-8").replace("\r\n", "\n")
78+
if existing != content:
79+
raise RuntimeError(f"golden corpus mutation detected: {path}")
80+
path.write_text(content, encoding="utf-8", newline="\n")
81+
hashes[filename] = canonical_content_hash(content)
6582
return hashes
6683

6784

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Test package for CompTextv7."""

tests/test_compression_signals_ts.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import textwrap
44
from pathlib import Path
55

6+
from tests.utils import resolve_tsc_executable
7+
68
REPO_ROOT = Path(__file__).resolve().parents[1]
79
DASHBOARD_APP = REPO_ROOT / "dashboard" / "app"
8-
TSC = DASHBOARD_APP / "node_modules" / ".bin" / "tsc"
10+
TSC = resolve_tsc_executable()
911

1012

1113
def run_compression_script(tmp_path: Path, script: str):

tests/test_core_foundation_ts.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import textwrap
44
from pathlib import Path
55

6+
from tests.utils import resolve_tsc_executable
7+
68
REPO_ROOT = Path(__file__).resolve().parents[1]
79
DASHBOARD_APP = REPO_ROOT / "dashboard" / "app"
8-
TSC = DASHBOARD_APP / "node_modules" / ".bin" / "tsc"
10+
TSC = resolve_tsc_executable()
911

1012

1113
def run_foundation_script(tmp_path: Path, script: str):
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
from src.validation.golden_corpus import canonical_content_hash
5+
6+
def test_canonical_content_hash_line_endings() -> None:
7+
# 1. LF line endings
8+
content_lf = "line1\nline2\nline3\n"
9+
hash_lf = canonical_content_hash(content_lf)
10+
11+
# 2. CRLF line endings
12+
content_crlf = "line1\r\nline2\r\nline3\r\n"
13+
hash_crlf = canonical_content_hash(content_crlf)
14+
15+
# 3. Mixed line endings
16+
content_mixed = "line1\r\nline2\nline3\r\n"
17+
hash_mixed = canonical_content_hash(content_mixed)
18+
19+
# All three must yield the identical hash
20+
assert hash_lf == hash_crlf
21+
assert hash_lf == hash_mixed
22+
23+
def test_canonical_content_hash_detects_mutations() -> None:
24+
# Ensure byte changes other than line endings are detected
25+
content_orig = "line1\nline2\nline3\n"
26+
content_mutated = "line1\nline2_altered\nline3\n"
27+
28+
assert canonical_content_hash(content_orig) != canonical_content_hash(content_mutated)
29+
30+
def test_canonical_content_hash_bytes_support() -> None:
31+
content_str = "line1\nline2\r\n"
32+
content_bytes = b"line1\nline2\r\n"
33+
34+
assert canonical_content_hash(content_str) == canonical_content_hash(content_bytes)
35+
36+
def test_canonical_content_hash_invalid_utf8_raises_error() -> None:
37+
# Invalid UTF-8 bytes (e.g. 0xff) must raise a UnicodeDecodeError
38+
invalid_bytes = b"line1\n\xff\n"
39+
with pytest.raises(UnicodeDecodeError):
40+
canonical_content_hash(invalid_bytes)

tests/test_reference_index_event_fingerprints_ts.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import textwrap
44
from pathlib import Path
55

6+
from tests.utils import resolve_tsc_executable
7+
68
REPO_ROOT = Path(__file__).resolve().parents[1]
79
DASHBOARD_APP = REPO_ROOT / "dashboard" / "app"
8-
TSC = DASHBOARD_APP / "node_modules" / ".bin" / "tsc"
10+
TSC = resolve_tsc_executable()
911

1012
def run_foundation_script(tmp_path: Path, script: str):
1113
out_dir = tmp_path / "compiled"

tests/test_replay_artifact_writer_ts.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import textwrap
44
from pathlib import Path
55

6+
from tests.utils import resolve_tsc_executable
7+
68
REPO_ROOT = Path(__file__).resolve().parents[1]
79
DASHBOARD_APP = REPO_ROOT / "dashboard" / "app"
8-
TSC = DASHBOARD_APP / "node_modules" / ".bin" / "tsc"
10+
TSC = resolve_tsc_executable()
911

1012
def run_foundation_script(tmp_path: Path, script: str):
1113
out_dir = tmp_path / "compiled"

tests/test_shared_stable_hashing_ts.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import textwrap
44
from pathlib import Path
55

6+
from tests.utils import resolve_tsc_executable
7+
68
REPO_ROOT = Path(__file__).resolve().parents[1]
79
DASHBOARD_APP = REPO_ROOT / "dashboard" / "app"
8-
TSC = DASHBOARD_APP / "node_modules" / ".bin" / "tsc"
10+
TSC = resolve_tsc_executable()
911

1012

1113
def run_foundation_script(tmp_path: Path, script: str):

tests/test_validation_hardening.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
from src.validation.token_telemetry import SUPPORTED_ENCODINGS, count_tokens, drift_fingerprint
1010

1111

12-
def test_golden_corpus_hashes_are_stable() -> None:
12+
from src.validation.golden_corpus import canonical_content_hash
13+
14+
def test_golden_corpus_canonical_content_hashes_are_stable() -> None:
1315
hashes = write_golden_corpus()
1416

1517
assert set(hashes) == {
@@ -19,8 +21,8 @@ def test_golden_corpus_hashes_are_stable() -> None:
1921
"mixed_incident_reference.jsonl",
2022
}
2123
for filename, digest in hashes.items():
22-
content = Path("datasets/golden", filename).read_bytes()
23-
assert hashlib.sha256(content).hexdigest() == digest
24+
content_bytes = Path("datasets/golden", filename).read_bytes()
25+
assert canonical_content_hash(content_bytes) == digest
2426

2527

2628
def test_token_telemetry_supports_required_encodings_deterministically() -> None:

tests/utils/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import os
2+
import shutil
3+
from pathlib import Path
4+
5+
def resolve_tsc_executable() -> Path:
6+
"""Resolve the path to the TypeScript compiler (tsc).
7+
8+
Prefers the local Node dependency in the dashboard application,
9+
falling back to system PATH. Raises FileNotFoundError if missing.
10+
"""
11+
repo_root = Path(__file__).resolve().parents[2]
12+
dashboard_app = repo_root / "dashboard" / "app"
13+
14+
tsc_bin = "tsc.cmd" if os.name == "nt" else "tsc"
15+
local_tsc = dashboard_app / "node_modules" / ".bin" / tsc_bin
16+
17+
if local_tsc.exists():
18+
return local_tsc
19+
20+
system_tsc = shutil.which(tsc_bin)
21+
if system_tsc:
22+
return Path(system_tsc)
23+
24+
raise FileNotFoundError(
25+
"TypeScript compiler (tsc) not found. Please run 'npm install' "
26+
"in dashboard/app or install TypeScript globally."
27+
)

0 commit comments

Comments
 (0)