From 2d7412da53d88270e29fbbae717c099045efcebf Mon Sep 17 00:00:00 2001 From: Lauri Gates Date: Sun, 14 Jun 2026 17:17:51 +0300 Subject: [PATCH 1/2] test(core): golden hash determinism guard across languages and versions (#4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(core): golden hash determinism guard across languages and versions The stored anchor hash is the only value every consumer compares against, yet nothing pinned its exact output. A tree-sitter grammar bump (the grammars are caret-pinned and only frozen by Cargo.lock, which Dependabot bumps) can rename a node kind and silently change every stored hash — a wave of false DIVERGED downstream, or worse, new collisions — with green CI. Add goldens for Rust/TS/TSX/Python/Go plus invariants: cosmetic edits (reformatting, consistent rename) keep the hash; logic edits (operator flip, swapped operands) move it. Runs on Linux and macOS in CI, so it also catches cross-platform drift between the release target triples. https://claude.ai/code/session_01T3Z35bgg6cvWzBDfyh6pQ8 * style: rustfmt golden_hash test (cherry picked from commit 7bb7a59f5531d1791ea30ea256e2900298485ed5) --- surf-core/tests/golden_hash.rs | 109 +++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 surf-core/tests/golden_hash.rs diff --git a/surf-core/tests/golden_hash.rs b/surf-core/tests/golden_hash.rs new file mode 100644 index 0000000..4786950 --- /dev/null +++ b/surf-core/tests/golden_hash.rs @@ -0,0 +1,109 @@ +//! Golden, cross-version determinism guard for the AST-canonical hash (§6.1). +//! +//! The stored anchor hash is the *only* thing every Surface consumer compares against. If the +//! canonical token stream changes for an unchanged symbol, every stored hash in every downstream +//! repo silently breaks (a wave of false DIVERGED) or, worse, two spans that should differ start +//! colliding. That can happen without anyone touching `hash.rs`: +//! +//! * a tree-sitter **grammar bump** renames a node kind (`binary_expression` → …) or reshapes +//! the tree — the grammars are caret-pinned in `Cargo.toml` (`tree-sitter-rust = "0.24.2"` +//! ⇒ `^0.24.2`) and only frozen by `Cargo.lock`, which Dependabot bumps on a schedule; +//! * a `tree-sitter` core bump changes traversal; +//! * a refactor of the canonicalization itself. +//! +//! These goldens pin the exact hash of representative symbols in every supported family. A diff +//! here is a loud, intentional signal: the canonical form moved, so either revert the bump or +//! ship the hash change deliberately (and tell consumers to re-verify). Because CI runs this +//! suite on both Linux and macOS, it also catches any cross-platform drift between the three +//! release target triples. +//! +//! If a *deliberate* change updates these values, update CHANGELOG and treat it as a +//! hash-format break for downstreams. + +use surf_core::{hash_anchor, parse_anchor, Lang}; + +fn h(src: &str, lang: Lang, anchor: &str) -> String { + hash_anchor(src, lang, &parse_anchor(anchor).unwrap()).unwrap() +} + +#[test] +fn golden_hashes_are_stable_per_language() { + // Each snippet carries a comment and non-canonical whitespace on purpose, so the golden + // already encodes the "comments + formatting are ignored" guarantee. + let rust = "pub fn add(a: i64, b: i64) -> i64 {\n // sum them\n a + b\n}\n"; + assert_eq!(h(rust, Lang::Rust, "x.rs > add"), "f1075e760a17"); + + let ts = + "export class Svc {\n rotate(tok: string): string {\n return tok + tok; // c\n }\n}\n"; + assert_eq!( + h(ts, Lang::TypeScript, "x.ts > Svc > rotate"), + "afa4514b5c89" + ); + + let tsx = "export function App(): JSX.Element {\n return
{1 + 2}
;\n}\n"; + assert_eq!(h(tsx, Lang::Tsx, "x.tsx > App"), "97e0de58725d"); + + let py = "def add(a, b):\n # comment\n return a + b\n"; + assert_eq!(h(py, Lang::Python, "x.py > add"), "879b76118966"); + + let go = "func Add(a int, b int) int {\n\t// sum\n\treturn a + b\n}\n"; + assert_eq!(h(go, Lang::Go, "x.go > Add"), "942af2641116"); +} + +#[test] +fn cosmetic_edits_do_not_change_the_hash() { + let canonical = h( + "pub fn add(a: i64, b: i64) -> i64 {\n // sum them\n a + b\n}\n", + Lang::Rust, + "x.rs > add", + ); + + // Whitespace collapsed and the comment removed: pure reformatting. + assert_eq!( + h( + "pub fn add(a:i64,b:i64)->i64{a+b}\n", + Lang::Rust, + "x.rs > add" + ), + canonical, + "reformatting must not move the hash", + ); + + // Consistent local rename (a→x, b→y) — alpha-renaming makes it invisible. + assert_eq!( + h( + "pub fn add(x: i64, y: i64) -> i64 { x + y }\n", + Lang::Rust, + "x.rs > add", + ), + canonical, + "a consistent rename must not move the hash", + ); +} + +#[test] +fn logic_edits_change_the_hash() { + let canonical = h( + "pub fn add(a: i64, b: i64) -> i64 { a + b }\n", + Lang::Rust, + "x.rs > add", + ); + + // Flipped operator. + let op_flip = h( + "pub fn add(a: i64, b: i64) -> i64 { a - b }\n", + Lang::Rust, + "x.rs > add", + ); + assert_ne!(op_flip, canonical, "an operator flip must move the hash"); + + // Swapped operands without a consistent rename — a real semantic change, distinct from the + // operator flip above (guards against the alpha-rename collapsing genuinely different code). + let swapped = h( + "pub fn add(a: i64, b: i64) -> i64 { b - a }\n", + Lang::Rust, + "x.rs > add", + ); + assert_ne!(swapped, canonical); + assert_ne!(swapped, op_flip, "b - a must not collide with a - b"); +} From ffadf772a52fccb7a6e9a7644c7a4acacd53498e Mon Sep 17 00:00:00 2001 From: Lauri Gates Date: Mon, 15 Jun 2026 14:56:02 +0300 Subject: [PATCH 2/2] test(core): drop redundant cosmetic_edits golden test The cosmetic_edits_do_not_change_the_hash test re-verified Rust hash invariance under reformatting and consistent rename, which rust_quiet_on_reformat_and_rename (surf-core/tests/hash.rs:126) already covers. Per upstream review feedback, remove the duplicate; the unique value of golden_hash.rs is the pinned per-language hash values. Co-Authored-By: Claude Opus 4.8 (1M context) --- surf-core/tests/golden_hash.rs | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/surf-core/tests/golden_hash.rs b/surf-core/tests/golden_hash.rs index 4786950..5f2ae71 100644 --- a/surf-core/tests/golden_hash.rs +++ b/surf-core/tests/golden_hash.rs @@ -50,37 +50,6 @@ fn golden_hashes_are_stable_per_language() { assert_eq!(h(go, Lang::Go, "x.go > Add"), "942af2641116"); } -#[test] -fn cosmetic_edits_do_not_change_the_hash() { - let canonical = h( - "pub fn add(a: i64, b: i64) -> i64 {\n // sum them\n a + b\n}\n", - Lang::Rust, - "x.rs > add", - ); - - // Whitespace collapsed and the comment removed: pure reformatting. - assert_eq!( - h( - "pub fn add(a:i64,b:i64)->i64{a+b}\n", - Lang::Rust, - "x.rs > add" - ), - canonical, - "reformatting must not move the hash", - ); - - // Consistent local rename (a→x, b→y) — alpha-renaming makes it invisible. - assert_eq!( - h( - "pub fn add(x: i64, y: i64) -> i64 { x + y }\n", - Lang::Rust, - "x.rs > add", - ), - canonical, - "a consistent rename must not move the hash", - ); -} - #[test] fn logic_edits_change_the_hash() { let canonical = h(