From f8a24b935617aaf4c4a2f02cd6e47b75a581f99d Mon Sep 17 00:00:00 2001 From: Max Cruz Date: Mon, 24 Aug 2026 15:28:08 +0000 Subject: [PATCH 1/4] feat(context): add gen_test_case_guid for globally unique test case ids A test case id is only unique within a (collection, repo) pairing: ids are generated from framework-internal values, and `--no-repo` deliberately makes the same test share one id across a collection's repos. Consumers that need to address a single test case by one value -- links, APIs, webhooks -- have no id to use. Add `gen_test_case_guid(test_collection_id, repo_id, test_case_id)`, a deterministic UUIDv8 derived from that whole identity tuple, so it is unique by construction and inherits the tuple's semantics (including the `--no-repo` collapse to one id per collection). The hash contract is frozen: the three ids as canonical lowercase text, joined with `#`, SHA-256, first 16 bytes, stamped as an RFC 9562 UUIDv8. The stamp is required rather than cosmetic -- consumers validate the value with a UUID matcher that enforces the version and variant nibbles. Two golden vectors are pinned in the Rust tests and mirrored into the JS and Python binding suites, which is what keeps every copy of the contract honest. `gen_info_id` is untouched, so existing bindings and their pinned tests are unaffected. Exported through context-js and context-py alongside it. Co-Authored-By: Claude Opus 5 (1M context) --- Cargo.lock | 2 + context-js/Cargo.toml | 1 + context-js/src/lib.rs | 27 +++++++- context-js/tests/meta.test.ts | 69 +++++++++++++++++++- context-py/src/lib.rs | 25 ++++++++ context-py/tests/test_meta.py | 49 +++++++++++++- context/Cargo.toml | 3 +- context/src/meta/id.rs | 117 +++++++++++++++++++++++++++++++++- 8 files changed, 288 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9154b86..3398e904 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -967,6 +967,7 @@ dependencies = [ "regex", "serde", "serde_json", + "sha2 0.10.9", "speedate 0.14.4", "tempfile", "test_utils", @@ -988,6 +989,7 @@ dependencies = [ "js-sys", "prost", "speedate 0.17.0", + "uuid", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", diff --git a/context-js/Cargo.toml b/context-js/Cargo.toml index 0caf737b..1bfad027 100644 --- a/context-js/Cargo.toml +++ b/context-js/Cargo.toml @@ -21,3 +21,4 @@ wasm-streams = "0.4.2" prost = "0.12.6" chrono = "0.4.42" speedate = "0.17.0" +uuid = { version = "1.10.0", features = ["v5", "v8"] } diff --git a/context-js/src/lib.rs b/context-js/src/lib.rs index c9e84368..de81be2e 100644 --- a/context-js/src/lib.rs +++ b/context-js/src/lib.rs @@ -10,11 +10,13 @@ use bundle::{ use chrono::{DateTime, FixedOffset}; use context::{ env, junit, meta::id::gen_info_id as gen_info_id_impl, - meta::id::gen_info_id_base as gen_info_id_base_impl, repo, + meta::id::gen_info_id_base as gen_info_id_base_impl, + meta::id::gen_test_case_guid as gen_test_case_guid_impl, repo, }; use futures::{future::Either, io::BufReader as BufReaderAsync, stream::TryStreamExt}; use js_sys::Uint8Array; use prost::Message; +use uuid::Uuid; use wasm_bindgen::prelude::*; use wasm_streams::{readable::ReadableStream, readable::sys}; @@ -264,3 +266,26 @@ pub fn gen_info_id_base( &variant, ) } + +/// Deterministic public id for a test case in a test collection -- the hash of the +/// `(test_collection_id, repo_id, test_case_id)` tuple. The hash contract is frozen. +/// +/// Takes and returns canonical UUID text. A malformed input is an error rather than a hash of +/// the raw bytes: silently minting a valid-looking guid from garbage would plant an id that +/// resolves to nothing. Pass the nil UUID for `repo_id` to match `--no-repo` storage. +#[wasm_bindgen] +pub fn gen_test_case_guid( + test_collection_id: String, + repo_id: String, + test_case_id: String, +) -> Result { + let parse = |label: &str, raw: &str| { + Uuid::parse_str(raw).map_err(|e| JsError::new(&format!("invalid {label}: {e}"))) + }; + Ok(gen_test_case_guid_impl( + parse("test_collection_id", &test_collection_id)?, + parse("repo_id", &repo_id)?, + parse("test_case_id", &test_case_id)?, + ) + .to_string()) +} diff --git a/context-js/tests/meta.test.ts b/context-js/tests/meta.test.ts index 2599235c..574f6348 100644 --- a/context-js/tests/meta.test.ts +++ b/context-js/tests/meta.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { gen_info_id, gen_info_id_base } from "../pkg/context_js"; +import { + gen_info_id, + gen_info_id_base, + gen_test_case_guid, +} from "../pkg/context_js"; describe("context-js", () => { // These tests match the tests in context/src/meta/id.rs. @@ -213,4 +217,67 @@ describe("context-js", () => { expect(base_result).not.toBe(expected); }); }); + + // These vectors are the FROZEN gen_test_case_guid contract, pinned identically in + // context/src/meta/id.rs. Proving the binding and the Rust code agree is the point of this file. + describe("gen_test_case_guid", () => { + const COLLECTION_ID = "018f6d3a-6f2e-4c4a-9b1e-2f3a4b5c6d7e"; + const REPO_ID = "7a1f0e3d-2b4c-4d5e-8f90-123456789abc"; + const TEST_CASE_ID = "88e5353c-190c-5dce-9d06-0e66c3e062b1"; + const NIL_UUID = "00000000-0000-0000-0000-000000000000"; + + it("matches the golden vector with a repo", () => { + expect.hasAssertions(); + + const result = gen_test_case_guid(COLLECTION_ID, REPO_ID, TEST_CASE_ID); + + expect(result).toBe("bfeebcf4-72d1-887d-8bcd-788d0dec7f97"); + + // Generate again to ensure it is consistent + expect(gen_test_case_guid(COLLECTION_ID, REPO_ID, TEST_CASE_ID)).toBe( + result, + ); + }); + + // --no-repo uploads store the nil repo UUID, so one guid covers the test across the + // collection's repos. The guid inherits that collapse from the tuple. + it("matches the golden vector with the nil repo id", () => { + expect.hasAssertions(); + + expect(gen_test_case_guid(COLLECTION_ID, NIL_UUID, TEST_CASE_ID)).toBe( + "943a80af-66b0-84bb-ad01-56b3b72fe363", + ); + }); + + it("normalizes uppercase input to the same guid", () => { + expect.hasAssertions(); + + expect( + gen_test_case_guid( + COLLECTION_ID.toUpperCase(), + REPO_ID.toUpperCase(), + TEST_CASE_ID.toUpperCase(), + ), + ).toBe("bfeebcf4-72d1-887d-8bcd-788d0dec7f97"); + }); + + it("is stamped as a v8 UUID", () => { + expect.hasAssertions(); + + const result = gen_test_case_guid(COLLECTION_ID, REPO_ID, TEST_CASE_ID); + + // Version nibble, then the RFC 9562 variant nibble consumers validate on. + expect(result[14]).toBe("8"); + expect(["8", "9", "a", "b"]).toContain(result[19]); + }); + + // Hashing a malformed id would mint a valid-looking guid that resolves to nothing. + it("throws on a malformed uuid instead of hashing it", () => { + expect.hasAssertions(); + + expect(() => + gen_test_case_guid(COLLECTION_ID, "not-a-uuid", TEST_CASE_ID), + ).toThrow(); + }); + }); }); diff --git a/context-py/src/lib.rs b/context-py/src/lib.rs index 46b56e68..94616c03 100644 --- a/context-py/src/lib.rs +++ b/context-py/src/lib.rs @@ -552,6 +552,30 @@ pub fn gen_info_id_base( ) } +/// Deterministic public id for a test case in a test collection -- the hash of the +/// `(test_collection_id, repo_id, test_case_id)` tuple. The hash contract is frozen. +/// +/// Takes and returns canonical UUID text. A malformed input raises rather than hashing the raw +/// bytes: silently minting a valid-looking guid from garbage would plant an id that resolves to +/// nothing. Pass the nil UUID for `repo_id` to match `--no-repo` storage. +#[gen_stub_pyfunction] +#[pyfunction] +pub fn gen_test_case_guid( + test_collection_id: String, + repo_id: String, + test_case_id: String, +) -> PyResult { + let parse = |label: &str, raw: &str| { + Uuid::parse_str(raw).map_err(|e| PyTypeError::new_err(format!("invalid {label}: {e}"))) + }; + Ok(id::gen_test_case_guid( + parse("test_collection_id", &test_collection_id)?, + parse("repo_id", &repo_id)?, + parse("test_case_id", &test_case_id)?, + ) + .to_string()) +} + #[pymodule] fn context_py(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; @@ -609,6 +633,7 @@ fn context_py(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(parse_internal_bin_from_tarball, m)?)?; m.add_function(wrap_pyfunction!(gen_info_id, m)?)?; m.add_function(wrap_pyfunction!(gen_info_id_base, m)?)?; + m.add_function(wrap_pyfunction!(gen_test_case_guid, m)?)?; m.add_class::()?; m.add_function(wrap_pyfunction!(codeowners_parse, m)?)?; diff --git a/context-py/tests/test_meta.py b/context-py/tests/test_meta.py index 2bdab318..85d7d6b2 100644 --- a/context-py/tests/test_meta.py +++ b/context-py/tests/test_meta.py @@ -1,4 +1,5 @@ -from context_py import gen_info_id, gen_info_id_base +import pytest +from context_py import gen_info_id, gen_info_id_base, gen_test_case_guid def test_generates_id_properly_for_trunk(): @@ -191,3 +192,49 @@ def test_variant_wrapper_does_change_variant_case(): expected = "1bf61475-b542-5faf-aa85-e66a691257a3" assert result == expected assert base_result != expected + + +# These vectors are the FROZEN gen_test_case_guid contract, pinned identically in +# context/src/meta/id.rs. Proving the binding and the Rust code agree is the point of these tests. +COLLECTION_ID = "018f6d3a-6f2e-4c4a-9b1e-2f3a4b5c6d7e" +REPO_ID = "7a1f0e3d-2b4c-4d5e-8f90-123456789abc" +TEST_CASE_ID = "88e5353c-190c-5dce-9d06-0e66c3e062b1" +NIL_UUID = "00000000-0000-0000-0000-000000000000" + + +def test_gen_test_case_guid_golden_with_repo(): + result = gen_test_case_guid(COLLECTION_ID, REPO_ID, TEST_CASE_ID) + assert result == "bfeebcf4-72d1-887d-8bcd-788d0dec7f97" + + # Generate again to ensure it is consistent + assert gen_test_case_guid(COLLECTION_ID, REPO_ID, TEST_CASE_ID) == result + + +def test_gen_test_case_guid_golden_no_repo(): + # --no-repo uploads store the nil repo UUID, so one guid covers the test across the + # collection's repos. The guid inherits that collapse from the tuple. + assert ( + gen_test_case_guid(COLLECTION_ID, NIL_UUID, TEST_CASE_ID) + == "943a80af-66b0-84bb-ad01-56b3b72fe363" + ) + + +def test_gen_test_case_guid_normalizes_uppercase_input(): + assert ( + gen_test_case_guid(COLLECTION_ID.upper(), REPO_ID.upper(), TEST_CASE_ID.upper()) + == "bfeebcf4-72d1-887d-8bcd-788d0dec7f97" + ) + + +def test_gen_test_case_guid_is_stamped_v8(): + result = gen_test_case_guid(COLLECTION_ID, REPO_ID, TEST_CASE_ID) + + # Version nibble, then the RFC 9562 variant nibble consumers validate on. + assert result[14] == "8" + assert result[19] in ("8", "9", "a", "b") + + +def test_gen_test_case_guid_rejects_malformed_uuid(): + # Hashing a malformed id would mint a valid-looking guid that resolves to nothing. + with pytest.raises(TypeError): + gen_test_case_guid(COLLECTION_ID, "not-a-uuid", TEST_CASE_ID) diff --git a/context/Cargo.toml b/context/Cargo.toml index d362412a..e3fd26ce 100644 --- a/context/Cargo.toml +++ b/context/Cargo.toml @@ -35,10 +35,11 @@ quick-xml = "0.36.2" regex = { version = "1.10.3", default-features = false, features = ["std"] } serde = { version = "1.0.215", default-features = false, features = ["derive"] } serde_json = "1.0.133" +sha2 = "0.10.9" speedate = "0.14.4" thiserror = "1.0.63" tsify-next = { version = "0.5.4", optional = true } -uuid = { version = "1.10.0", features = ["v5"] } +uuid = { version = "1.10.0", features = ["v5", "v8"] } wasm-bindgen = { version = "0.2.95", optional = true } magnus = { version = "0.8.2", optional = true, default-features = false } proto = { path = "../proto" } diff --git a/context/src/meta/id.rs b/context/src/meta/id.rs index 300f0922..28de1c67 100644 --- a/context/src/meta/id.rs +++ b/context/src/meta/id.rs @@ -1,3 +1,4 @@ +use sha2::{Digest, Sha256}; use uuid::Uuid; fn generate_checksum_uuid(values: Vec<&str>) -> String { @@ -5,6 +6,33 @@ fn generate_checksum_uuid(values: Vec<&str>) -> String { Uuid::new_v5(&Uuid::NAMESPACE_URL, info_id_input.as_bytes()).to_string() } +/// Deterministic, globally-unique public id for a test case in a test collection. +/// +/// A collection test case is only unique by the `(test_collection_id, repo_id, test_case_id)` +/// tuple: `test_case_id` is opaque to the server and `--no-repo` deliberately shares one id +/// across a collection's repos (nil `repo_id`). This hashes that whole tuple into one id, so it +/// inherits the tuple's semantics wholesale -- including the `--no-repo` collapse. +/// +/// The contract is FROZEN and enforced by the pinned golden values in this file's tests. The +/// server computes the same id from the same inputs, so changing any step here changes which ids +/// exist for every test case already reported. +/// +/// 1. the three UUIDs as canonical lowercase hyphenated text (`Display`), +/// 2. joined `"{test_collection_id}#{repo_id}#{test_case_id}"` (`#` mirrors `gen_info_id`), +/// 3. SHA-256, first 16 bytes, +/// 4. stamped RFC 9562 UUIDv8 (exactly what `Uuid::new_v8` does). +/// +/// Step 4 is load-bearing, not cosmetic: consumers validate this id with a UUID matcher that +/// enforces version 1-8 plus variant `[89ab]`, which an unstamped truncated hash fails ~87.5% of +/// the time. v8 also visibly distinguishes this id from the v4/v5 ids alongside it. +pub fn gen_test_case_guid(test_collection_id: Uuid, repo_id: Uuid, test_case_id: Uuid) -> Uuid { + let msg = format!("{test_collection_id}#{repo_id}#{test_case_id}"); + let digest = Sha256::digest(msg.as_bytes()); + let mut bytes = [0u8; 16]; + bytes.copy_from_slice(&digest[..16]); + Uuid::new_v8(bytes) +} + // trunk-ignore(clippy/too_many_arguments) pub fn gen_info_id_base( org_url_slug: &str, @@ -93,7 +121,9 @@ pub fn gen_info_id( #[cfg(test)] #[cfg(feature = "bindings")] mod tests { - use crate::meta::id::{gen_info_id, gen_info_id_base}; + use uuid::Uuid; + + use crate::meta::id::{gen_info_id, gen_info_id_base, gen_test_case_guid}; #[cfg(feature = "bindings")] #[test] @@ -345,4 +375,89 @@ mod tests { ); assert_eq!(result_v4, result_again); } + + // ---------------------------------------------------------------------------------- + // gen_test_case_guid -- FROZEN contract. + // + // These two vectors ARE the contract. They are pinned in every binding's test suite, and + // server-side consumers pin them too; the golden values are what keep those copies honest. + // A change here is a change to which ids exist for every test case already reported. + // ---------------------------------------------------------------------------------- + + const GOLDEN_COLLECTION_ID: &str = "018f6d3a-6f2e-4c4a-9b1e-2f3a4b5c6d7e"; + const GOLDEN_REPO_ID: &str = "7a1f0e3d-2b4c-4d5e-8f90-123456789abc"; + const GOLDEN_TEST_CASE_ID: &str = "88e5353c-190c-5dce-9d06-0e66c3e062b1"; + + /// `--repo` (the ordinary case): a real repo UUID participates in the hash. + const GOLDEN_GUID_WITH_REPO: &str = "bfeebcf4-72d1-887d-8bcd-788d0dec7f97"; + /// `--no-repo`: the nil repo UUID, so one guid per collection across repos. + const GOLDEN_GUID_NO_REPO: &str = "943a80af-66b0-84bb-ad01-56b3b72fe363"; + + fn golden_guid(repo_id: Uuid) -> Uuid { + gen_test_case_guid( + Uuid::parse_str(GOLDEN_COLLECTION_ID).unwrap(), + repo_id, + Uuid::parse_str(GOLDEN_TEST_CASE_ID).unwrap(), + ) + } + + #[cfg(feature = "bindings")] + #[test] + fn test_gen_test_case_guid_golden_with_repo() { + let result = golden_guid(Uuid::parse_str(GOLDEN_REPO_ID).unwrap()); + assert_eq!(result.to_string(), GOLDEN_GUID_WITH_REPO); + + // Run again to ensure deterministic output + let result_again = golden_guid(Uuid::parse_str(GOLDEN_REPO_ID).unwrap()); + assert_eq!(result_again, result); + } + + #[cfg(feature = "bindings")] + #[test] + fn test_gen_test_case_guid_golden_no_repo() { + let result = golden_guid(Uuid::nil()); + assert_eq!(result.to_string(), GOLDEN_GUID_NO_REPO); + + // Run again to ensure deterministic output + let result_again = golden_guid(Uuid::nil()); + assert_eq!(result_again, result); + } + + /// The contract hashes the *canonical lowercase* rendering, so uppercase input text must + /// normalize to the same guid. `Uuid`'s `Display` is what guarantees this. + #[cfg(feature = "bindings")] + #[test] + fn test_gen_test_case_guid_normalizes_uppercase_inputs() { + let result = gen_test_case_guid( + Uuid::parse_str(&GOLDEN_COLLECTION_ID.to_uppercase()).unwrap(), + Uuid::parse_str(&GOLDEN_REPO_ID.to_uppercase()).unwrap(), + Uuid::parse_str(&GOLDEN_TEST_CASE_ID.to_uppercase()).unwrap(), + ); + assert_eq!(result.to_string(), GOLDEN_GUID_WITH_REPO); + } + + /// Consumers validate this id with a UUID matcher enforcing version 1-8 and variant `[89ab]`, + /// so an unstamped hash would be rejected ~87.5% of the time. Assert the stamp directly -- + /// far cheaper to catch here than at the API edge. + #[cfg(feature = "bindings")] + #[test] + fn test_gen_test_case_guid_is_stamped_v8() { + for repo_id in [Uuid::parse_str(GOLDEN_REPO_ID).unwrap(), Uuid::nil()] { + let guid = golden_guid(repo_id); + assert_eq!(guid.get_version_num(), 8); + // RFC 9562 variant: the high two bits of byte 8 are 0b10. + assert_eq!(guid.as_bytes()[8] & 0xC0, 0x80); + } + } + + /// The two collision vectors the tuple encodes must stay distinct: a real repo and the nil + /// repo are different tuples, so they are different guids. + #[cfg(feature = "bindings")] + #[test] + fn test_gen_test_case_guid_repo_id_changes_the_guid() { + assert_ne!( + golden_guid(Uuid::parse_str(GOLDEN_REPO_ID).unwrap()), + golden_guid(Uuid::nil()) + ); + } } From 5cf9cd97c7a2448615e5cc5d0fa4d233ffd7f979 Mon Sep 17 00:00:00 2001 From: Max Cruz Date: Mon, 24 Aug 2026 15:54:07 +0000 Subject: [PATCH 2/4] refactor(context): trim gen_test_case_guid comments The doc comments restated the algorithm the code already shows. Keep only what is not visible from reading it: why the tuple is the identity, that the contract is frozen, and that the v8 stamp is load-bearing rather than decorative. Co-Authored-By: Claude Opus 5 (1M context) --- context-js/src/lib.rs | 9 +++---- context-js/tests/meta.test.ts | 10 +++----- context-py/src/lib.rs | 9 +++---- context-py/tests/test_meta.py | 10 +++----- context/src/meta/id.rs | 46 +++++++++++------------------------ 5 files changed, 30 insertions(+), 54 deletions(-) diff --git a/context-js/src/lib.rs b/context-js/src/lib.rs index de81be2e..88b11a96 100644 --- a/context-js/src/lib.rs +++ b/context-js/src/lib.rs @@ -267,12 +267,11 @@ pub fn gen_info_id_base( ) } -/// Deterministic public id for a test case in a test collection -- the hash of the -/// `(test_collection_id, repo_id, test_case_id)` tuple. The hash contract is frozen. +/// Deterministic public id for a test case in a test collection: the frozen hash of the +/// `(test_collection_id, repo_id, test_case_id)` tuple. /// -/// Takes and returns canonical UUID text. A malformed input is an error rather than a hash of -/// the raw bytes: silently minting a valid-looking guid from garbage would plant an id that -/// resolves to nothing. Pass the nil UUID for `repo_id` to match `--no-repo` storage. +/// Takes and returns canonical UUID text. A malformed input errors rather than being hashed, which +/// would mint a valid-looking id that resolves to nothing. Nil `repo_id` matches `--no-repo`. #[wasm_bindgen] pub fn gen_test_case_guid( test_collection_id: String, diff --git a/context-js/tests/meta.test.ts b/context-js/tests/meta.test.ts index 574f6348..1a9c1c44 100644 --- a/context-js/tests/meta.test.ts +++ b/context-js/tests/meta.test.ts @@ -218,8 +218,7 @@ describe("context-js", () => { }); }); - // These vectors are the FROZEN gen_test_case_guid contract, pinned identically in - // context/src/meta/id.rs. Proving the binding and the Rust code agree is the point of this file. + // The frozen gen_test_case_guid contract, pinned identically in context/src/meta/id.rs. describe("gen_test_case_guid", () => { const COLLECTION_ID = "018f6d3a-6f2e-4c4a-9b1e-2f3a4b5c6d7e"; const REPO_ID = "7a1f0e3d-2b4c-4d5e-8f90-123456789abc"; @@ -239,8 +238,7 @@ describe("context-js", () => { ); }); - // --no-repo uploads store the nil repo UUID, so one guid covers the test across the - // collection's repos. The guid inherits that collapse from the tuple. + // --no-repo stores the nil repo UUID, collapsing to one guid per collection. it("matches the golden vector with the nil repo id", () => { expect.hasAssertions(); @@ -266,12 +264,12 @@ describe("context-js", () => { const result = gen_test_case_guid(COLLECTION_ID, REPO_ID, TEST_CASE_ID); - // Version nibble, then the RFC 9562 variant nibble consumers validate on. + // Version nibble, then the variant nibble consumers validate on. expect(result[14]).toBe("8"); expect(["8", "9", "a", "b"]).toContain(result[19]); }); - // Hashing a malformed id would mint a valid-looking guid that resolves to nothing. + // Hashing a malformed id would mint an id that resolves to nothing. it("throws on a malformed uuid instead of hashing it", () => { expect.hasAssertions(); diff --git a/context-py/src/lib.rs b/context-py/src/lib.rs index 94616c03..ce2cd2dd 100644 --- a/context-py/src/lib.rs +++ b/context-py/src/lib.rs @@ -552,12 +552,11 @@ pub fn gen_info_id_base( ) } -/// Deterministic public id for a test case in a test collection -- the hash of the -/// `(test_collection_id, repo_id, test_case_id)` tuple. The hash contract is frozen. +/// Deterministic public id for a test case in a test collection: the frozen hash of the +/// `(test_collection_id, repo_id, test_case_id)` tuple. /// -/// Takes and returns canonical UUID text. A malformed input raises rather than hashing the raw -/// bytes: silently minting a valid-looking guid from garbage would plant an id that resolves to -/// nothing. Pass the nil UUID for `repo_id` to match `--no-repo` storage. +/// Takes and returns canonical UUID text. A malformed input raises rather than being hashed, which +/// would mint a valid-looking id that resolves to nothing. Nil `repo_id` matches `--no-repo`. #[gen_stub_pyfunction] #[pyfunction] pub fn gen_test_case_guid( diff --git a/context-py/tests/test_meta.py b/context-py/tests/test_meta.py index 85d7d6b2..2bd43e6e 100644 --- a/context-py/tests/test_meta.py +++ b/context-py/tests/test_meta.py @@ -194,8 +194,7 @@ def test_variant_wrapper_does_change_variant_case(): assert base_result != expected -# These vectors are the FROZEN gen_test_case_guid contract, pinned identically in -# context/src/meta/id.rs. Proving the binding and the Rust code agree is the point of these tests. +# The frozen gen_test_case_guid contract, pinned identically in context/src/meta/id.rs. COLLECTION_ID = "018f6d3a-6f2e-4c4a-9b1e-2f3a4b5c6d7e" REPO_ID = "7a1f0e3d-2b4c-4d5e-8f90-123456789abc" TEST_CASE_ID = "88e5353c-190c-5dce-9d06-0e66c3e062b1" @@ -211,8 +210,7 @@ def test_gen_test_case_guid_golden_with_repo(): def test_gen_test_case_guid_golden_no_repo(): - # --no-repo uploads store the nil repo UUID, so one guid covers the test across the - # collection's repos. The guid inherits that collapse from the tuple. + # --no-repo stores the nil repo UUID, collapsing to one guid per collection. assert ( gen_test_case_guid(COLLECTION_ID, NIL_UUID, TEST_CASE_ID) == "943a80af-66b0-84bb-ad01-56b3b72fe363" @@ -229,12 +227,12 @@ def test_gen_test_case_guid_normalizes_uppercase_input(): def test_gen_test_case_guid_is_stamped_v8(): result = gen_test_case_guid(COLLECTION_ID, REPO_ID, TEST_CASE_ID) - # Version nibble, then the RFC 9562 variant nibble consumers validate on. + # Version nibble, then the variant nibble consumers validate on. assert result[14] == "8" assert result[19] in ("8", "9", "a", "b") def test_gen_test_case_guid_rejects_malformed_uuid(): - # Hashing a malformed id would mint a valid-looking guid that resolves to nothing. + # Hashing a malformed id would mint an id that resolves to nothing. with pytest.raises(TypeError): gen_test_case_guid(COLLECTION_ID, "not-a-uuid", TEST_CASE_ID) diff --git a/context/src/meta/id.rs b/context/src/meta/id.rs index 28de1c67..7c63eed7 100644 --- a/context/src/meta/id.rs +++ b/context/src/meta/id.rs @@ -8,23 +8,15 @@ fn generate_checksum_uuid(values: Vec<&str>) -> String { /// Deterministic, globally-unique public id for a test case in a test collection. /// -/// A collection test case is only unique by the `(test_collection_id, repo_id, test_case_id)` -/// tuple: `test_case_id` is opaque to the server and `--no-repo` deliberately shares one id -/// across a collection's repos (nil `repo_id`). This hashes that whole tuple into one id, so it -/// inherits the tuple's semantics wholesale -- including the `--no-repo` collapse. +/// A test case is unique only by the `(test_collection_id, repo_id, test_case_id)` tuple: +/// `test_case_id` is built from framework-internal values, and `--no-repo` deliberately shares it +/// across a collection's repos. Hashing the whole tuple inherits those semantics rather than +/// re-deciding them. /// -/// The contract is FROZEN and enforced by the pinned golden values in this file's tests. The -/// server computes the same id from the same inputs, so changing any step here changes which ids -/// exist for every test case already reported. -/// -/// 1. the three UUIDs as canonical lowercase hyphenated text (`Display`), -/// 2. joined `"{test_collection_id}#{repo_id}#{test_case_id}"` (`#` mirrors `gen_info_id`), -/// 3. SHA-256, first 16 bytes, -/// 4. stamped RFC 9562 UUIDv8 (exactly what `Uuid::new_v8` does). -/// -/// Step 4 is load-bearing, not cosmetic: consumers validate this id with a UUID matcher that -/// enforces version 1-8 plus variant `[89ab]`, which an unstamped truncated hash fails ~87.5% of -/// the time. v8 also visibly distinguishes this id from the v4/v5 ids alongside it. +/// The contract is FROZEN -- lowercase canonical text, `#`-joined, SHA-256, first 16 bytes, stamped +/// UUIDv8 -- and the pinned golden values in this file's tests are what hold it. The v8 stamp is +/// required, not cosmetic: consumers validate this with a UUID matcher that enforces the version +/// and variant nibbles. A change here changes which ids exist for every test case already reported. pub fn gen_test_case_guid(test_collection_id: Uuid, repo_id: Uuid, test_case_id: Uuid) -> Uuid { let msg = format!("{test_collection_id}#{repo_id}#{test_case_id}"); let digest = Sha256::digest(msg.as_bytes()); @@ -376,21 +368,15 @@ mod tests { assert_eq!(result_v4, result_again); } - // ---------------------------------------------------------------------------------- - // gen_test_case_guid -- FROZEN contract. - // - // These two vectors ARE the contract. They are pinned in every binding's test suite, and - // server-side consumers pin them too; the golden values are what keep those copies honest. - // A change here is a change to which ids exist for every test case already reported. - // ---------------------------------------------------------------------------------- + // These two vectors ARE the frozen gen_test_case_guid contract, and are pinned in the binding + // suites and by server-side consumers too. const GOLDEN_COLLECTION_ID: &str = "018f6d3a-6f2e-4c4a-9b1e-2f3a4b5c6d7e"; const GOLDEN_REPO_ID: &str = "7a1f0e3d-2b4c-4d5e-8f90-123456789abc"; const GOLDEN_TEST_CASE_ID: &str = "88e5353c-190c-5dce-9d06-0e66c3e062b1"; - /// `--repo` (the ordinary case): a real repo UUID participates in the hash. const GOLDEN_GUID_WITH_REPO: &str = "bfeebcf4-72d1-887d-8bcd-788d0dec7f97"; - /// `--no-repo`: the nil repo UUID, so one guid per collection across repos. + /// `--no-repo`: nil repo UUID, so one guid per collection across repos. const GOLDEN_GUID_NO_REPO: &str = "943a80af-66b0-84bb-ad01-56b3b72fe363"; fn golden_guid(repo_id: Uuid) -> Uuid { @@ -423,8 +409,7 @@ mod tests { assert_eq!(result_again, result); } - /// The contract hashes the *canonical lowercase* rendering, so uppercase input text must - /// normalize to the same guid. `Uuid`'s `Display` is what guarantees this. + /// The contract hashes the canonical lowercase rendering, so case must not matter. #[cfg(feature = "bindings")] #[test] fn test_gen_test_case_guid_normalizes_uppercase_inputs() { @@ -436,9 +421,7 @@ mod tests { assert_eq!(result.to_string(), GOLDEN_GUID_WITH_REPO); } - /// Consumers validate this id with a UUID matcher enforcing version 1-8 and variant `[89ab]`, - /// so an unstamped hash would be rejected ~87.5% of the time. Assert the stamp directly -- - /// far cheaper to catch here than at the API edge. + /// Consumers validate this with a UUID matcher enforcing version and variant. #[cfg(feature = "bindings")] #[test] fn test_gen_test_case_guid_is_stamped_v8() { @@ -450,8 +433,7 @@ mod tests { } } - /// The two collision vectors the tuple encodes must stay distinct: a real repo and the nil - /// repo are different tuples, so they are different guids. + /// A real repo and the nil repo are different tuples, so different guids. #[cfg(feature = "bindings")] #[test] fn test_gen_test_case_guid_repo_id_changes_the_guid() { From e04019a1315948ce310244ed6612e1171bf83030 Mon Sep 17 00:00:00 2001 From: Max Cruz Date: Mon, 24 Aug 2026 18:36:35 +0000 Subject: [PATCH 3/4] fix(context): satisfy trunk check on the new golden tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nine new lint findings, all in the two test files this PR adds: - `vitest/prefer-describe-function-title` wants the identifier when a describe title is exactly a function name, while `vitest/valid-title` rejects a non-string title — the two conflict, and trunk's autofix for the first produced a violation of the second. A string that isn't an exact match satisfies both. The `gen_info_id` describe was pre-existing but became a "new" finding once my import shifted its line. - `vitest/require-to-throw-message`: assert the message rather than bare throw. - pytest isn't resolvable in the pyright environment (nothing else here imports it), so the raises test uses try/except instead of depending on it. - `context_py`'s stub is generated rather than committed, so pyright strict can't type anything imported from it. Annotating or `str()`-wrapping just relocates the unknown, so the two call sites and the import carry targeted suppressions, matching the existing precedent in test_parse_codeowners.py. `trunk check` is clean; 113 Rust and 33 context-js tests still pass. Co-Authored-By: Claude Opus 5 (1M context) --- context-js/tests/meta.test.ts | 6 +++--- context-py/tests/test_meta.py | 10 ++++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/context-js/tests/meta.test.ts b/context-js/tests/meta.test.ts index 1a9c1c44..7410bd2f 100644 --- a/context-js/tests/meta.test.ts +++ b/context-js/tests/meta.test.ts @@ -9,7 +9,7 @@ describe("context-js", () => { // These tests match the tests in context/src/meta/id.rs. // While they don't need to match, it proves both the bindings and // rust code are generating the same IDs. - describe("gen_info_id", () => { + describe("gen_info_id()", () => { it("generates ID properly for trunk", () => { expect.hasAssertions(); @@ -219,7 +219,7 @@ describe("context-js", () => { }); // The frozen gen_test_case_guid contract, pinned identically in context/src/meta/id.rs. - describe("gen_test_case_guid", () => { + describe("gen_test_case_guid()", () => { const COLLECTION_ID = "018f6d3a-6f2e-4c4a-9b1e-2f3a4b5c6d7e"; const REPO_ID = "7a1f0e3d-2b4c-4d5e-8f90-123456789abc"; const TEST_CASE_ID = "88e5353c-190c-5dce-9d06-0e66c3e062b1"; @@ -275,7 +275,7 @@ describe("context-js", () => { expect(() => gen_test_case_guid(COLLECTION_ID, "not-a-uuid", TEST_CASE_ID), - ).toThrow(); + ).toThrowError(/invalid repo_id/); }); }); }); diff --git a/context-py/tests/test_meta.py b/context-py/tests/test_meta.py index 2bd43e6e..b4ec2925 100644 --- a/context-py/tests/test_meta.py +++ b/context-py/tests/test_meta.py @@ -1,4 +1,4 @@ -import pytest +# trunk-ignore(pyright/reportUnknownVariableType): context_py's stub is generated, not committed from context_py import gen_info_id, gen_info_id_base, gen_test_case_guid @@ -202,6 +202,7 @@ def test_variant_wrapper_does_change_variant_case(): def test_gen_test_case_guid_golden_with_repo(): + # trunk-ignore(pyright/reportUnknownVariableType) result = gen_test_case_guid(COLLECTION_ID, REPO_ID, TEST_CASE_ID) assert result == "bfeebcf4-72d1-887d-8bcd-788d0dec7f97" @@ -225,6 +226,7 @@ def test_gen_test_case_guid_normalizes_uppercase_input(): def test_gen_test_case_guid_is_stamped_v8(): + # trunk-ignore(pyright/reportUnknownVariableType) result = gen_test_case_guid(COLLECTION_ID, REPO_ID, TEST_CASE_ID) # Version nibble, then the variant nibble consumers validate on. @@ -234,5 +236,9 @@ def test_gen_test_case_guid_is_stamped_v8(): def test_gen_test_case_guid_rejects_malformed_uuid(): # Hashing a malformed id would mint an id that resolves to nothing. - with pytest.raises(TypeError): + try: gen_test_case_guid(COLLECTION_ID, "not-a-uuid", TEST_CASE_ID) + except TypeError as error: + assert "invalid repo_id" in str(error) + else: + raise AssertionError("expected a malformed uuid to raise") From 104cbd08e72156b944133c3921d7e04a18673e78 Mon Sep 17 00:00:00 2001 From: Max Cruz Date: Mon, 24 Aug 2026 19:02:03 +0000 Subject: [PATCH 4/4] fix(ci): generate Python stubs before trunk check, drop the pyright suppressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pyright type-checks context-py against context_py.pyi, which is generated by a trunk action rather than committed. Trunk actions run on git hooks, not during `trunk check`, so a developer's tree has the stub and CI never does — every symbol imported from context_py is `Unknown` there, which is most of this repo's standing pyright findings and why adding one import produced a new one. The trunk check job already builds the wasm package for exactly this reason on the JS side (eslint needs the generated pkg/ to resolve). Generating the Python stub is the missing counterpart, and the job already builds the workspace, so it is nearly free. With the stub present the suppressions are unnecessary, so they are gone and the tests keep their natural shape. Existing pyright findings across context-py drop from 70 to 47 as a side effect. The raises test still uses try/except rather than pytest.raises: pytest is not resolvable in the pyright environment either, and nothing else in this directory imports it. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/pull_request.yml | 7 +++++++ context-py/tests/test_meta.py | 3 --- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 112230d9..9be9e8a3 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -264,6 +264,13 @@ jobs: - name: Setup and build wasm uses: ./.github/actions/setup_build_wasm + # pyright type-checks context-py against context_py.pyi, which is generated rather than + # committed. Without this, every symbol imported from context_py is `Unknown` and each new + # import is a fresh pyright finding. The wasm step above exists for the same reason on the + # JS side -- eslint needs the generated pkg/ to resolve. + - name: Generate Python stubs + run: cargo run --bin stub_gen --manifest-path context-py/Cargo.toml --no-default-features + - name: Trunk Check uses: trunk-io/trunk-action@v1 with: diff --git a/context-py/tests/test_meta.py b/context-py/tests/test_meta.py index b4ec2925..402863dd 100644 --- a/context-py/tests/test_meta.py +++ b/context-py/tests/test_meta.py @@ -1,4 +1,3 @@ -# trunk-ignore(pyright/reportUnknownVariableType): context_py's stub is generated, not committed from context_py import gen_info_id, gen_info_id_base, gen_test_case_guid @@ -202,7 +201,6 @@ def test_variant_wrapper_does_change_variant_case(): def test_gen_test_case_guid_golden_with_repo(): - # trunk-ignore(pyright/reportUnknownVariableType) result = gen_test_case_guid(COLLECTION_ID, REPO_ID, TEST_CASE_ID) assert result == "bfeebcf4-72d1-887d-8bcd-788d0dec7f97" @@ -226,7 +224,6 @@ def test_gen_test_case_guid_normalizes_uppercase_input(): def test_gen_test_case_guid_is_stamped_v8(): - # trunk-ignore(pyright/reportUnknownVariableType) result = gen_test_case_guid(COLLECTION_ID, REPO_ID, TEST_CASE_ID) # Version nibble, then the variant nibble consumers validate on.