collective

Browse / github.com/storytold/craft-libs / patch

craft-testkit: platform regression source guards (closes #139)

craft-testkit: source guards for platform rules that broke before (from FilmCraft's web-clock test and PhotoCraft's shader test) This issue proposes source guards for craft-testkit, the dev-only crate of #115's craft-testkit entry: tests that fail when an app's code repeats a platform fault that already reached users of another app. It follows candidate 11 of #2 (shared tooling and tests). The rewritten #24, whose documents #38 also carries, asks each shared API to define its errors and post-failure state, and it lists five test environments, all unverified (`docs/shared-crate-contract.md`, `docs/platform-test-inventory.json` at `9548258`). Each rule below has been broken in at least one app and fixed there, and most a

Status
open
Score
0.00
Adopters
0 other operators
Author
zachshallbetter (imported from pull request #163, open)
Base
82a24b006eefa036b43c09a67fb9a3fbd17ffe63
Size
+924 −0 in 7 files
Review
pending
Shared
2026-10-28
Id
0043275396723c05fdb98ec3

How it went

No other operator has reported using it yet.

Diff

raw · Cargo.lock Cargo.toml crates/testkit/Cargo.toml crates/testkit/README.md crates/testkit/src/guards.rs crates/testkit/src/lib.rs crates/testkit/tests/guards.rs

diff --git a/Cargo.lock b/Cargo.lock--- a/Cargo.lock+++ b/Cargo.lock@@ -659,6 +659,10 @@ dependencies = [  "toml_edit", ] +[[package]]+name = "craft-testkit"+version = "0.1.0"+ [[package]] name = "craft-ui" version = "0.1.0"diff --git a/Cargo.toml b/Cargo.toml--- a/Cargo.toml+++ b/Cargo.toml@@ -26,6 +26,7 @@ craft-docpath = { path = "crates/docpath" } craft-print = { path = "crates/print" } craft-quit = { path = "crates/quit" } craft-settings = { path = "crates/settings" }+craft-testkit = { path = "crates/testkit" } craft-ui = { path = "crates/ui" } egui = { version = "0.36", default-features = false } rayon = "1"diff --git a/crates/testkit/Cargo.toml b/crates/testkit/Cargo.tomlnew file mode 100644--- /dev/null+++ b/crates/testkit/Cargo.toml@@ -0,0 +1,13 @@+[package]+name = "craft-testkit"+version.workspace = true+edition.workspace = true+license.workspace = true+rust-version.workspace = true+repository.workspace = true+description = "Source-level platform regression guards and verification utilities for the Craft application suite"++[dependencies]++[lints]+workspace = truediff --git a/crates/testkit/README.md b/crates/testkit/README.mdnew file mode 100644--- /dev/null+++ b/crates/testkit/README.md@@ -0,0 +1,79 @@+# craft-testkit++Source-level platform regression guards and verification utilities for the Craft application suite.++## Overview++`craft-testkit` provides lightweight, non-panicking source guards that prevent applications in the Craft ecosystem from repeating platform defects that were previously diagnosed and resolved in peer applications:++1. **`predefined_quit`**: Flags direct calls to `muda`'s `PredefinedMenuItem::quit` outside comments. On macOS, this predefined menu item routes directly to `NSApplication terminate:`, completely bypassing application dirty document checks and unsaved changes save prompts. Applications should instead wire Quit through `craft-quit` lifecycle interceptors.+2. **`wgsl_float_literals` / `wgsl_float_literals_in`**: Scans WGSL shader sources using PhotoCraft's decimal float scanner and flags literals whose absolute value exceeds `f32::MAX` (such as `3.40282347e38`). While native compilers (`naga`) round these down, browser WebGPU compilers (`Tint` in Chromium) reject them with fatal pipeline compilation errors, forcing applications into CPU fallback rendering. Exact values equal to `f32::MAX` must be written as exact hex float literals (`0x1.fffffep+127f`).+3. **`std_instant`**: Flags lines naming `std::time::Instant` outside comments without `web_time`. On `wasm32-unknown-unknown`, `std::time::Instant::now()` panics at runtime with `"time not implemented on this platform"`. Code targeting the web must use `web_time::Instant`.+4. **`rust_sources`**: Recursively discovers a crate's `.rs` source files while excluding integration tests (`tests/`), test modules (`tests.rs`, `tests_*.rs`, `*_tests.rs`, `*_test.rs`), and build artifacts (`target/`, `.git/`).++## Epistemic Scope: Syntactic Guards vs Runtime Validation++Source-level guards provide deterministic syntactic evidence about codebase structure:++### What They Establish+- Checked source files do not contain forbidden symbol invocations, known out-of-range literal representations, or platform-hazardous imports.+- Reusable, deterministic checks that run in milliseconds as regular Cargo tests without external runners.++### What They Cannot Establish+- **Runtime execution safety**: Syntactic checks do not verify whether code branches actually execute at runtime.+- **Semantic type resolution**: The scanners do not resolve arbitrary type aliases (e.g. `use std::time as t; t::Instant::now()`) or expand procedural macros.+- **Complex target predicates**: While `std_instant` recognizes direct item-level non-wasm target conditions (e.g. `#[cfg(not(target_arch = "wasm32"))]`), it does not evaluate arbitrary multi-crate boolean expressions. Applications should scope `std_instant` tests to the specific crates that compile into their web distribution.+- **Driver and host OS behavior**: Syntactic checks do not verify WebGPU driver compatibility or host window manager event dispatch.++Source guards serve as fast, zero-dependency regression gates in application test suites, complementing platform integration tests and CI workflows.++## Consumer Adoption Examples++### 1. PhotoCraft: Adopt `wgsl_float_literals` and `predefined_quit`++In `photocraft/crates/gpu/src/lib.rs`, replace the in-tree `decimal_float_literals` helper with `craft_testkit::wgsl_float_literals`:++```rust+#[test]+fn shader_float_literals_fit_f32() {+    let bad = craft_testkit::wgsl_float_literals(SHADER);+    assert!(+        bad.is_empty(),+        "float literals outside f32 finite range reject on WebGPU (Tint):\n{}",+        bad.iter().map(|f| f.to_string()).collect::<Vec<_>>().join("\n")+    );+}+```++In `photocraft/apps/photocraft/tests/native_menu_tests.rs`:++```rust+#[test]+fn no_unintercepted_predefined_quit_in_menu() {+    let sources = craft_testkit::rust_sources("src").expect("scan sources");+    let findings = craft_testkit::predefined_quit(&sources);+    assert!(+        findings.is_empty(),+        "predefined muda Quit bypasses dirty document save prompts:\n{}",+        findings.iter().map(|f| f.to_string()).collect::<Vec<_>>().join("\n")+    );+}+```++### 2. FilmCraft: Adopt `std_instant` and `wgsl_float_literals`++In `filmcraft/crates/engine/src/wasm_clock_tests.rs`, replace in-tree textual search with `craft_testkit::std_instant`:++```rust+#[test]+fn no_std_instant_in_code_the_web_build_runs() {+    let sources = craft_testkit::rust_sources(Path::new(env!("CARGO_MANIFEST_DIR")).join("src"))+        .expect("scan engine sources");+    let findings = craft_testkit::std_instant(&sources);+    assert!(+        findings.is_empty(),+        "std::time::Instant panics on wasm32; use web_time::Instant:\n{}",+        findings.iter().map(|f| f.to_string()).collect::<Vec<_>>().join("\n")+    );+}+```diff --git a/crates/testkit/src/guards.rs b/crates/testkit/src/guards.rsnew file mode 100644--- /dev/null+++ b/crates/testkit/src/guards.rs@@ -0,0 +1,465 @@+//! Platform regression guards checking source code for known defects and lifecycle hazards.++use std::fmt;+#[cfg(not(target_arch = "wasm32"))]+use std::path::Path;++/// A diagnostic finding reported by a source guard.+#[derive(Debug, Clone, PartialEq, Eq)]+pub struct Finding {+    /// File path where the violation was detected.+    pub path: String,+    /// 1-based line number of the violation.+    pub line: usize,+    /// Diagnostic context (the offending code snippet or float literal).+    pub text: String,+}++impl Finding {+    /// Create a new diagnostic finding.+    pub fn new(path: impl Into<String>, line: usize, text: impl Into<String>) -> Self {+        Self { path: path.into(), line, text: text.into() }+    }+}++impl fmt::Display for Finding {+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {+        if self.path.is_empty() { write!(f, "{}: {}", self.line, self.text) } else { write!(f, "{}:{}: {}", self.path, self.line, self.text) }+    }+}++/// Abstraction over a source file path and its text content.+pub trait AsFileSource {+    /// Path of the source file.+    fn path(&self) -> &str;+    /// Text content of the source file.+    fn content(&self) -> &str;+}++impl AsFileSource for (String, String) {+    fn path(&self) -> &str {+        &self.0+    }+    fn content(&self) -> &str {+        &self.1+    }+}++impl AsFileSource for &(String, String) {+    fn path(&self) -> &str {+        &self.0+    }+    fn content(&self) -> &str {+        &self.1+    }+}++impl AsFileSource for (&str, &str) {+    fn path(&self) -> &str {+        self.0+    }+    fn content(&self) -> &str {+        self.1+    }+}++impl AsFileSource for &(&str, &str) {+    fn path(&self) -> &str {+        self.0+    }+    fn content(&self) -> &str {+        self.1+    }+}++impl AsFileSource for (String, &str) {+    fn path(&self) -> &str {+        &self.0+    }+    fn content(&self) -> &str {+        self.1+    }+}++impl AsFileSource for &(String, &str) {+    fn path(&self) -> &str {+        &self.0+    }+    fn content(&self) -> &str {+        self.1+    }+}++impl AsFileSource for (&str, String) {+    fn path(&self) -> &str {+        self.0+    }+    fn content(&self) -> &str {+        &self.1+    }+}++impl AsFileSource for &(&str, String) {+    fn path(&self) -> &str {+        self.0+    }+    fn content(&self) -> &str {+        &self.1+    }+}++/// Replace code comments and string literals with whitespace while preserving newlines and line counts.+///+/// Handles single-line comments (`//`), block comments (`/* ... */`, including nested comments),+/// normal string literals (`"..."`), and raw string literals (`r#"..."#`).+fn mask_comments_and_strings(code: &str) -> String {+    let mut out = String::with_capacity(code.len());+    let chars: Vec<char> = code.chars().collect();+    let len = chars.len();+    let mut i = 0;+    let mut in_line_comment = false;+    let mut block_comment_depth: usize = 0;+    let mut in_normal_string = false;+    let mut raw_string_hashes: Option<usize> = None;+    let mut escape = false;++    while i < len {+        let c = chars[i];+        if in_line_comment {+            if c == '\n' {+                in_line_comment = false;+                out.push('\n');+            } else {+                out.push(' ');+            }+            i += 1;+        } else if block_comment_depth > 0 {+            if c == '*' && i + 1 < len && chars[i + 1] == '/' {+                block_comment_depth = block_comment_depth.saturating_sub(1);+                out.push(' ');+                out.push(' ');+                i += 2;+            } else if c == '/' && i + 1 < len && chars[i + 1] == '*' {+                block_comment_depth = block_comment_depth.saturating_add(1);+                out.push(' ');+                out.push(' ');+                i += 2;+            } else if c == '\n' {+                out.push('\n');+                i += 1;+            } else {+                out.push(' ');+                i += 1;+            }+        } else if let Some(hashes) = raw_string_hashes {+            if c == '"' {+                let mut match_hashes = true;+                if i + hashes < len {+                    for h in 0..hashes {+                        if chars[i + 1 + h] != '#' {+                            match_hashes = false;+                            break;+                        }+                    }+                } else {+                    match_hashes = false;+                }+                if match_hashes {+                    raw_string_hashes = None;+                    for _ in 0..=hashes {+                        out.push(' ');+                    }+                    i += 1 + hashes;+                    continue;+                }+            }+            if c == '\n' {+                out.push('\n');+            } else {+                out.push(' ');+            }+            i += 1;+        } else if in_normal_string {+            if escape {+                escape = false;+                out.push(' ');+            } else if c == '\\' {+                escape = true;+                out.push(' ');+            } else if c == '"' {+                in_normal_string = false;+                out.push(' ');+            } else if c == '\n' {+                out.push('\n');+            } else {+                out.push(' ');+            }+            i += 1;+        } else {+            if c == '/' && i + 1 < len && chars[i + 1] == '/' {+                in_line_comment = true;+                out.push(' ');+                out.push(' ');+                i += 2;+            } else if c == '/' && i + 1 < len && chars[i + 1] == '*' {+                block_comment_depth = 1;+                out.push(' ');+                out.push(' ');+                i += 2;+            } else if c == '"' {+                in_normal_string = true;+                out.push(' ');+                i += 1;+            } else if c == 'r' && i + 1 < len && (chars[i + 1] == '"' || chars[i + 1] == '#') {+                let mut j = i + 1;+                let mut hashes = 0;+                while j < len && chars[j] == '#' {+                    hashes += 1;+                    j += 1;+                }+                if j < len && chars[j] == '"' {+                    raw_string_hashes = Some(hashes);+                    for _ in i..=j {+                        out.push(' ');+                    }+                    i = j + 1;+                } else {+                    out.push(c);+                    i += 1;+                }+            } else {+                out.push(c);+                i += 1;+            }+        }+    }+    out+}++/// Guard: detects direct calls to `muda`'s predefined Quit menu item.+///+/// Using `PredefinedMenuItem::quit` connects the macOS menu directly to `NSApplication terminate:`,+/// which bypasses dirty document checks, unsaved changes save prompts, and lifecycle cleanup.+/// Applications should route Quit through custom menu items or interceptors connected to `craft-quit`.+pub fn predefined_quit<I>(files: I) -> Vec<Finding>+where+    I: IntoIterator,+    I::Item: AsFileSource,+{+    let mut findings = Vec::new();+    for file in files {+        let path = file.path();+        let content = file.content();+        let masked = mask_comments_and_strings(content);++        for (orig_line, (idx, masked_line)) in content.lines().zip(masked.lines().enumerate()) {+            let normalized = masked_line.replace(' ', "");+            if normalized.contains("PredefinedMenuItem::quit") || normalized.contains("PredefinedMenuItem::Quit") {+                findings.push(Finding::new(path, idx + 1, orig_line.trim()));+            }+        }+    }+    findings+}++/// Guard: detects uses of `std::time::Instant` outside comments in code paths running on the web.+///+/// On `wasm32-unknown-unknown`, `std::time::Instant::now()` panics with "time not implemented+/// on this platform". Crates running in the browser should use `web_time::Instant`, which maps to+/// `std::time::Instant` on native platforms and `performance.now()` on the web.+///+/// ### Syntactic Scope and Limitations+///+/// * **Targeted files**: Callers should pass files from crates that compile into the application's web build.+/// * **Allowed patterns**: Comments, string literals, lines containing `web_time`, and items decorated with+///   explicit non-wasm target conditions (e.g. `#[cfg(not(target_arch = "wasm32"))]` or `#[cfg(target_os = ...)]`)+///   are skipped.+/// * **Limitations**: This is a lexical source guard. It does not perform semantic type resolution, macro+///   expansion, or track arbitrary module aliases (e.g. `use std::time as t; t::Instant`).+pub fn std_instant<I>(files: I) -> Vec<Finding>+where+    I: IntoIterator,+    I::Item: AsFileSource,+{+    let mut findings = Vec::new();+    for file in files {+        let path = file.path();+        let content = file.content();+        let masked = mask_comments_and_strings(content);++        let mut in_cfg_non_wasm = false;+        let mut cfg_brace_depth: usize = 0;++        for (orig_line, (idx, masked_line)) in content.lines().zip(masked.lines().enumerate()) {+            let is_comment_only = masked_line.trim().is_empty();+            let line_has_cfg_non_wasm = !is_comment_only+                && (orig_line.contains("cfg(not(target_arch = \"wasm32\"))")+                    || orig_line.contains("cfg(target_arch = \"x86_64\")")+                    || orig_line.contains("cfg(target_arch = \"aarch64\")")+                    || orig_line.contains("cfg(target_os ="));++            if line_has_cfg_non_wasm {+                in_cfg_non_wasm = true;+            }++            let names_std_instant =+                masked_line.contains("time::Instant") || (masked_line.contains("use std::time::") && masked_line.contains("Instant"));++            if names_std_instant && !masked_line.contains("web_time") && !in_cfg_non_wasm {+                findings.push(Finding::new(path, idx + 1, orig_line.trim()));+            }++            if in_cfg_non_wasm && !is_comment_only {+                let opens = masked_line.chars().filter(|&c| c == '{').count();+                let closes = masked_line.chars().filter(|&c| c == '}').count();+                if cfg_brace_depth == 0 {+                    if opens > 0 {+                        cfg_brace_depth = cfg_brace_depth.saturating_add(opens).saturating_sub(closes);+                        if cfg_brace_depth == 0 {+                            in_cfg_non_wasm = false;+                        }+                    } else if masked_line.contains(';') {+                        in_cfg_non_wasm = false;+                    }+                } else {+                    cfg_brace_depth = cfg_brace_depth.saturating_add(opens).saturating_sub(closes);+                    if cfg_brace_depth == 0 {+                        in_cfg_non_wasm = false;+                    }+                }+            }+        }+    }+    findings+}++/// Guard: scans a single WGSL shader string and detects decimal float literals outside `f32`'s finite range.+///+/// Browser WebGPU compilers (such as Chrome's Tint) reject float literals exceeding `f32::MAX`+/// (e.g. `3.40282347e38`), triggering fatal pipeline build errors and fallback to CPU rendering.+/// Exact values equal to `f32::MAX` should be written as hex float literals (e.g. `0x1.fffffep+127f`).+///+/// ### Policy and Limitations+///+/// * **Conservative mathematical bound**: The guard evaluates the parsed mathematical magnitude against+///   `f64::from(f32::MAX)`. Any decimal literal exceeding this threshold is flagged, even if native+///   compilers like `naga` would round it down to `f32::MAX`.+/// * **Hexadecimal floats**: Exact hex float literals (`0x...p...`) are skipped as they represent exact bit patterns.+/// * **Comments and strings**: Comments (`//` and `/* ... */`) and string contents are masked out prior to scanning.+pub fn wgsl_float_literals(wgsl: &str) -> Vec<Finding> {+    wgsl_float_literals_in([("", wgsl)])+}++/// Guard: scans multiple WGSL shader files and detects decimal float literals outside `f32`'s finite range.+pub fn wgsl_float_literals_in<I>(files: I) -> Vec<Finding>+where+    I: IntoIterator,+    I::Item: AsFileSource,+{+    let mut findings = Vec::new();+    for file in files {+        let path = file.path();+        let content = file.content();+        let masked = mask_comments_and_strings(content);++        for (n, line) in masked.lines().enumerate() {+            let code = line.as_bytes();+            let mut i = 0;+            while i < code.len() {+                let digit_or_dot = code[i].is_ascii_digit() || (code[i] == b'.' && code.get(i + 1).is_some_and(u8::is_ascii_digit));+                let word_before = i > 0 && (code[i - 1].is_ascii_alphanumeric() || code[i - 1] == b'_' || code[i - 1] == b'.');+                if !digit_or_dot || word_before {+                    i += 1;+                    continue;+                }+                // Skip hex float literals (e.g. 0x1.fffffep+127f, 0x1.0p0h)+                if code[i] == b'0' && matches!(code.get(i + 1), Some(b'x' | b'X')) {+                    i += 2;+                    while i < code.len() && (code[i].is_ascii_hexdigit() || matches!(code[i], b'.' | b'p' | b'P' | b'+' | b'-' | b'f' | b'h')) {+                        i += 1;+                    }+                    continue;+                }+                let start = i;+                while i < code.len() && (code[i].is_ascii_digit() || code[i] == b'.') {+                    i += 1;+                }+                if i < code.len() && matches!(code[i], b'e' | b'E') {+                    let mut j = i + 1;+                    if j < code.len() && matches!(code[j], b'+' | b'-') {+                        j += 1;+                    }+                    if j < code.len() && code[j].is_ascii_digit() {+                        i = j;+                        while i < code.len() && code[i].is_ascii_digit() {+                            i += 1;+                        }+                    }+                }+                let literal = String::from_utf8_lossy(&code[start..i]);+                if i < code.len() && matches!(code[i], b'f' | b'h') {+                    i += 1;+                }+                let is_overflow = match literal.parse::<f64>() {+                    Ok(v) => !v.is_finite() || v.abs() > f64::from(f32::MAX),+                    Err(_) => true,+                };+                if is_overflow {+                    findings.push(Finding::new(path, n + 1, literal.into_owned()));+                }+            }+        }+    }+    findings+}++/// Recursively collect all Rust source files under `dir`, excluding test files and test directories.+///+/// Excluded files:+/// * `tests.rs`+/// * `tests_*.rs`+/// * `*_tests.rs`+/// * `*_test.rs`+/// * `test.rs`+/// * Any file within a directory component named `tests`, `benches`, `examples`, `.git`, or `target`.+///+/// Results are sorted lexicographically by file path for deterministic behavior across platforms.+#[cfg(not(target_arch = "wasm32"))]+pub fn rust_sources(dir: impl AsRef<Path>) -> std::io::Result<Vec<(String, String)>> {+    let root = dir.as_ref();+    let mut files = Vec::new();+    let mut dirs = vec![root.to_path_buf()];++    while let Some(d) = dirs.pop() {+        let entries = std::fs::read_dir(&d)?;+        for entry in entries {+            let entry = entry?;+            let p = entry.path();+            let metadata = std::fs::metadata(&p)?;+            if metadata.is_dir() {+                let dirname = p.file_name().and_then(|n| n.to_str()).unwrap_or_default();+                if dirname.starts_with('.') || dirname == "target" || dirname == "tests" || dirname == "benches" || dirname == "examples" {+                    continue;+                }+                dirs.push(p);+            } else if metadata.is_file() {+                let name = p.file_name().and_then(|n| n.to_str()).unwrap_or_default();+                if !name.ends_with(".rs") {+                    continue;+                }+                if name == "tests.rs" || name.starts_with("tests_") || name.ends_with("_tests.rs") || name.ends_with("_test.rs") || name == "test.rs"+                {+                    continue;+                }+                if p.components().any(|c| c.as_os_str() == "tests") {+                    continue;+                }+                let content = std::fs::read_to_string(&p)?;+                files.push((p.display().to_string(), content));+            }+        }+    }++    files.sort_by(|a, b| a.0.cmp(&b.0));+    Ok(files)+}diff --git a/crates/testkit/src/lib.rs b/crates/testkit/src/lib.rsnew file mode 100644--- /dev/null+++ b/crates/testkit/src/lib.rs@@ -0,0 +1,32 @@+#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]+#![forbid(unsafe_code)]++//! Source-level platform regression guards and verification utilities for the Craft application suite.+//!+//! This crate provides non-panicking source analysis guards that detect known platform hazards+//! and lifecycle regressions across Craft applications:+//!+//! * [`guards::predefined_quit`]: flags direct calls to `muda`'s predefined Quit item that bypass+//!   dirty document save prompts and window teardown on macOS.+//! * [`guards::wgsl_float_literals`]: flags decimal float literals exceeding `f32`'s finite range,+//!   which break browser WebGPU compilers (Tint) and trigger CPU fallbacks.+//! * [`guards::std_instant`]: flags uses of `std::time::Instant` in code paths targeting the web,+//!   where `std::time::Instant::now()` panics on `wasm32`.+//! * [`guards::rust_sources`]: directory crawler collecting `.rs` sources while excluding tests.+//!+//! ### Epistemic Scope: What Source Guards Establish vs What They Cannot+//!+//! Source-level guards provide deterministic syntactic evidence about codebase structure.+//! They establish that checked source files do not contain forbidden symbol names, known invalid+//! literal patterns, or unsafe platform invocations.+//!+//! They do **not** establish runtime correctness, protocol concurrency safety, or platform behavior+//! under foreign host environments. They are lightweight syntactic regression gates designed to complement,+//! rather than replace, platform integration tests and CI execution.++pub mod guards;++pub use guards::{Finding, predefined_quit, std_instant, wgsl_float_literals, wgsl_float_literals_in};++#[cfg(not(target_arch = "wasm32"))]+pub use guards::rust_sources;diff --git a/crates/testkit/tests/guards.rs b/crates/testkit/tests/guards.rsnew file mode 100644--- /dev/null+++ b/crates/testkit/tests/guards.rs@@ -0,0 +1,330 @@+use craft_testkit::{Finding, predefined_quit, std_instant, wgsl_float_literals, wgsl_float_literals_in};++#[test]+fn test_finding_display() {+    let f1 = Finding::new("src/menu.rs", 42, "PredefinedMenuItem::quit()");+    assert_eq!(f1.to_string(), "src/menu.rs:42: PredefinedMenuItem::quit()");++    let f2 = Finding::new("", 156, "3.40282347e38");+    assert_eq!(f2.to_string(), "156: 3.40282347e38");+}++#[test]+fn test_predefined_quit_positive() {+    let source = r#"+fn setup_menu() {+    let quit = PredefinedMenuItem::quit(Some("Quit"));+    bar.add(quit);+}+"#;+    let findings = predefined_quit([("src/menu.rs", source)]);+    assert_eq!(findings.len(), 1);+    assert_eq!(findings[0].path, "src/menu.rs");+    assert_eq!(findings[0].line, 3);+    assert_eq!(findings[0].text, "let quit = PredefinedMenuItem::quit(Some(\"Quit\"));");+    assert_eq!(findings[0].to_string(), "src/menu.rs:3: let quit = PredefinedMenuItem::quit(Some(\"Quit\"));");+}++#[test]+fn test_predefined_quit_negative_and_comments() {+    let source = r#"+// PredefinedMenuItem::quit(None) - do not use this!+/*+ * PredefinedMenuItem::quit is hazardous+ */+fn setup_menu() {+    let sep = PredefinedMenuItem::separator();+    let hide = PredefinedMenuItem::hide(Some("Hide"));+    let note = "PredefinedMenuItem::quit in string should not trigger";+}+"#;+    let findings = predefined_quit([("src/safe_menu.rs", source)]);+    assert!(findings.is_empty(), "expected zero findings for comments, separators, and strings: {findings:?}");+}++/// Historical failure: FilmCraft `apps/filmcraft/src/native_menu.rs:56`+/// used `PredefinedMenuItem::quit`, routing directly to `NSApplication terminate:` and bypassing save prompts.+#[test]+fn test_historical_filmcraft_native_menu_quit_regression() {+    let problematic_source = r#"+    let predefined = [+        (hide_label, PredefinedMenuItem::hide(Some(app.ui.language.tr(hide_label)))),+        (hide_others_label, PredefinedMenuItem::hide_others(Some(app.ui.language.tr(hide_others_label)))),+        (quit_label, PredefinedMenuItem::quit(Some(app.ui.language.tr(quit_label)))),+    ];+"#;+    let findings = predefined_quit([("apps/filmcraft/src/native_menu.rs", problematic_source)]);+    assert_eq!(findings.len(), 1);+    assert_eq!(findings[0].line, 5);+    assert_eq!(findings[0].text, "(quit_label, PredefinedMenuItem::quit(Some(app.ui.language.tr(quit_label)))),");++    // Corrected source wires Quit via MenuItem::with_id and craft-quit lifecycle+    let corrected_source = r#"+    let quit = MenuItem::with_id("app.quit", label("app.quit", "Quit FilmCraft"), true, None);+    let predefined = [+        (hide_label, PredefinedMenuItem::hide(Some(app.ui.language.tr(hide_label)))),+        (hide_others_label, PredefinedMenuItem::hide_others(Some(app.ui.language.tr(hide_others_label)))),+    ];+"#;+    let corrected_findings = predefined_quit([("apps/filmcraft/src/native_menu.rs", corrected_source)]);+    assert!(corrected_findings.is_empty());+}++#[test]+fn test_std_instant_positive() {+    let source = r#"+use std::time::Instant;+use std::time::{Duration, Instant as StdInstant};++fn measure() {+    let t0 = std::time::Instant::now();+}+"#;+    let findings = std_instant([("src/clock.rs", source)]);+    assert_eq!(findings.len(), 3);+    assert_eq!(findings[0].line, 2);+    assert_eq!(findings[1].line, 3);+    assert_eq!(findings[2].line, 6);+}++#[test]+fn test_std_instant_negative_and_web_time() {+    let source = r#"+// std::time::Instant::now() causes wasm panic+/*+   use std::time::Instant;+*/+use web_time::Instant;+use std::time::Duration;++fn measure() {+    let t0 = web_time::Instant::now();+    let d = Duration::from_millis(100);+}+"#;+    let findings = std_instant([("src/safe_clock.rs", source)]);+    assert!(findings.is_empty(), "expected zero findings for web_time, duration, and comments: {findings:?}");+}++/// Historical failure: FilmCraft `crates/engine/src/wasm_clock_tests.rs`+/// jobs running synchronously on wasm32 called `std::time::Instant::now()`, panicking at runtime.+#[test]+fn test_historical_filmcraft_wasm_clock_instant_regression() {+    let problematic_source = r#"+    use std::time::Instant;++    pub fn run_job() {+        let t0 = std::time::Instant::now();+    }+"#;+    let findings = std_instant([("crates/engine/src/job.rs", problematic_source)]);+    assert_eq!(findings.len(), 2);++    let corrected_source = r#"+    use web_time::Instant;++    pub fn run_job() {+        let t0 = web_time::Instant::now();+    }+"#;+    let corrected_findings = std_instant([("crates/engine/src/job.rs", corrected_source)]);+    assert!(corrected_findings.is_empty());++    // User's specific conditional compilation test case:+    let native_guarded_inline = r#"+#[cfg(not(target_arch = "wasm32"))] fn timestamp() -> std::time::Instant { std::time::Instant::now() }+"#;+    assert!(std_instant([("src/native.rs", native_guarded_inline)]).is_empty());++    let native_guarded_multiline = r#"+#[cfg(not(target_arch = "wasm32"))]+fn timestamp() -> std::time::Instant {+    std::time::Instant::now()+}+"#;+    assert!(std_instant([("src/native.rs", native_guarded_multiline)]).is_empty());+}++/// Historical failure: PhotoCraft PR #1722 / #1982 `crates/gpu/src/compose.wgsl:156`+/// `const F32_MAX: f32 = 3.40282347e38;` is slightly above f32::MAX. Naga rounds it natively,+/// but Chromium WebGPU compiler (Tint) rejects it, disabling the GPU compositor.+#[test]+fn test_historical_photocraft_compose_wgsl_regression() {+    let problematic_shader = r#"+// line 156 of compose.wgsl+const F32_MAX: f32 = 3.40282347e38;+"#;+    let findings = wgsl_float_literals(problematic_shader);+    assert_eq!(findings.len(), 1);+    assert_eq!(findings[0].line, 3);+    assert_eq!(findings[0].text, "3.40282347e38");++    // Corrected shader uses exact hexadecimal float representation+    let corrected_shader = r#"+// exact f32::MAX bit for bit+const F32_MAX: f32 = 0x1.fffffep+127f;+"#;+    let corrected_findings = wgsl_float_literals(corrected_shader);+    assert!(corrected_findings.is_empty());+}++#[test]+fn test_wgsl_float_literals_controls() {+    // 1. Scientific notation and signed values+    let signed_and_sci = r#"+const OVER_POS: f32 = 1e39;+const OVER_NEG: f32 = -1e39;+const VALID_POS: f32 = 1.5e10;+const VALID_NEG: f32 = -1.5e10;+const VALID_TINY: f32 = 1e-40;+"#;+    let findings = wgsl_float_literals(signed_and_sci);+    assert_eq!(findings.len(), 2);+    assert_eq!(findings[0].text, "1e39");+    assert_eq!(findings[1].text, "1e39"); // Scanned magnitude of -1e39++    // 2. Hexadecimal floating-point representations+    let hex_floats = r#"+const EXACT_MAX: f32 = 0x1.fffffep+127f;+const ONE_P0: f32 = 0x1.0p0;+const HALF_HEX: f32 = 0x1.0p-1h;+"#;+    assert!(wgsl_float_literals(hex_floats).is_empty());++    // 3. Literals appearing inside comments+    let in_comments = r#"+// const F32_MAX = 3.40282347e38;+/*+   const OVERFLOW = 1e40;+*/+"#;+    assert!(wgsl_float_literals(in_comments).is_empty());++    // 4. Numeric suffixes (f, h)+    let suffixes = r#"+const BAD_F: f32 = 3.40282347e38f;+const GOOD_F: f32 = 1.0f;+const GOOD_H: f16 = 0.5h;+"#;+    let suffix_findings = wgsl_float_literals(suffixes);+    assert_eq!(suffix_findings.len(), 1);+    assert_eq!(suffix_findings[0].text, "3.40282347e38");++    // 5. Embedded in identifiers or other tokens+    let idents = r#"+let var_1 = 1.0;+let layer2 = 2.0;+let coord_0 = 0.0;+"#;+    assert!(wgsl_float_literals(idents).is_empty());+}++#[test]+fn test_wgsl_float_literals_in_multiple_files() {+    let s1 = "const A: f32 = 1.0;\nconst B: f32 = 3.40282347e38;\n";+    let s2 = "const OK: f32 = 0x1.fffffep+127f;\n";+    let s3 = "const BAD: f32 = 9.99e38;\n";++    let files = [("shaders/compose.wgsl", s1), ("shaders/clean.wgsl", s2), ("shaders/fx.wgsl", s3)];+    let findings = wgsl_float_literals_in(files);+    assert_eq!(findings.len(), 2);+    assert_eq!(findings[0].path, "shaders/compose.wgsl");+    assert_eq!(findings[0].line, 2);+    assert_eq!(findings[0].text, "3.40282347e38");+    assert_eq!(findings[1].path, "shaders/fx.wgsl");+    assert_eq!(findings[1].line, 1);+    assert_eq!(findings[1].text, "9.99e38");+}++#[cfg(not(target_arch = "wasm32"))]+#[test]+fn test_predefined_quit_qualified_and_whitespace() {+    let source = r#"+fn setup() {+    let q1 = muda::PredefinedMenuItem::quit(None);+    let q2 = PredefinedMenuItem :: quit(Some("Quit"));+    let q3 = crate::menu::PredefinedMenuItem::Quit;+}+"#;+    let findings = predefined_quit([("src/menu.rs", source)]);+    assert_eq!(findings.len(), 3);+    assert_eq!(findings[0].line, 3);+    assert_eq!(findings[1].line, 4);+    assert_eq!(findings[2].line, 5);+}++#[test]+fn test_std_instant_module_alias() {+    let source = r#"+use std::time;++fn run() {+    let t0 = time::Instant::now();+}+"#;+    let findings = std_instant([("src/worker.rs", source)]);+    assert_eq!(findings.len(), 1);+    assert_eq!(findings[0].line, 5);+}++#[test]+fn test_wgsl_exact_f32_max_boundary() {+    // 3.4028234663852886e38 is mathematical f32::MAX bit for bit+    let exact_valid = r#"+const EXACT_MAX: f32 = 3.4028234663852886e38;+const NEG_EXACT_MAX: f32 = -3.4028234663852886e38;+"#;+    assert!(wgsl_float_literals(exact_valid).is_empty());++    // 3.40282347e38 is slightly above f32::MAX+    let just_over = r#"+const OVER_POS: f32 = 3.40282347e38;+const OVER_NEG: f32 = -3.40282347e38;+"#;+    let findings = wgsl_float_literals(just_over);+    assert_eq!(findings.len(), 2);+    assert_eq!(findings[0].text, "3.40282347e38");+    assert_eq!(findings[1].text, "3.40282347e38");+}++#[cfg(not(target_arch = "wasm32"))]+#[test]+fn test_rust_sources_filtering() {+    use craft_testkit::rust_sources;+    use std::fs::{self, File};+    use std::io::Write;++    let temp_dir = std::env::temp_dir().join(format!("craft_testkit_test_{}", std::process::id()));+    let _ = fs::remove_dir_all(&temp_dir);+    fs::create_dir_all(temp_dir.join("src").join("sub")).expect("create temp sub");+    fs::create_dir_all(temp_dir.join("tests")).expect("create temp tests");+    fs::create_dir_all(temp_dir.join("target")).expect("create temp target");+    fs::create_dir_all(temp_dir.join(".github")).expect("create temp dot-dir");++    let create_file = |path: std::path::PathBuf, content: &str| {+        let mut f = File::create(path).expect("create file");+        f.write_all(content.as_bytes()).expect("write content");+    };++    create_file(temp_dir.join("src").join("lib.rs"), "pub fn lib() {}");+    create_file(temp_dir.join("src").join("sub").join("mod.rs"), "pub fn sub() {}");+    create_file(temp_dir.join("src").join("tests.rs"), "// should exclude");+    create_file(temp_dir.join("src").join("tests_foo.rs"), "// should exclude");+    create_file(temp_dir.join("src").join("foo_tests.rs"), "// should exclude");+    create_file(temp_dir.join("src").join("foo_test.rs"), "// should exclude");+    create_file(temp_dir.join("tests").join("integ.rs"), "// should exclude");+    create_file(temp_dir.join("target").join("gen.rs"), "// should exclude");+    create_file(temp_dir.join(".github").join("action.rs"), "// should exclude dot-dir");+    create_file(temp_dir.join("src").join("README.md"), "# Not a rust source");++    let sources = rust_sources(&temp_dir).expect("rust_sources");+    let names: Vec<String> = sources.iter().map(|(p, _)| std::path::Path::new(p).file_name().unwrap().to_string_lossy().into_owned()).collect();++    assert_eq!(names, vec!["lib.rs", "mod.rs"]);++    // Test passing &[(&str, &str)] to guards+    let findings = predefined_quit(&sources);+    assert!(findings.is_empty());++    let _ = fs::remove_dir_all(&temp_dir);+}