mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-16 08:06:24 +03:00
The engine no longer lives in a separate repo. `crates/` is a snapshot of the
open crates (foundation, core, common, context, live, hook, skills, comp,
comp-verbs, html, browser, detect, cli) plus `Cargo.lock`, taken as a git
archive of the engine repo at the commit that finished the boundary split.
None of that repo's history comes with it, and none of it should: the closed
half stays private.
The closed half is the rule engine. It ships as a prebuilt native archive per
target, `libimpeccable_detector.a`, published as a `detector-v<X>` GitHub
Release on this repo. `crates/core/build.rs` resolves and links it three ways:
`IMPECCABLE_DETECTOR_LIB=<dir>` for a local detector build, else the
`~/.impeccable/detector/<version>/<target>/` cache, else a download verified
against its `.sha256` sidecar. `crates/core` is a thin shim over a three-symbol
C ABI; nothing above it knows the boundary exists.
What changed versus the engine repo copy:
- Every crate manifest moves from `license-file.workspace` to
`license.workspace` (this workspace declares Apache-2.0), and the workspace
gains the `postcard` dependency the boundary encoding needs.
- The launcher contract test reads `skill/scripts/impeccable{,.cmd}` instead of
a sibling `launcher/` dir, and `engine_binary` downloads from
`github.com/pbakaus/impeccable/releases/download/engine-v<version>/` instead
of the retired dist repo. No oracle golden carried the old URL, so no
re-recording was owed.
- The tests that hunted for a public repo through `IMPECCABLE_PUBLIC_REPO`,
`../impeccable-second` or a hardcoded home directory now resolve the root as
`CARGO_MANIFEST_DIR/../..`, because they are in it. The env var stays as an
override for an out-of-tree checkout.
- The in-page bundle (`detect-antipatterns-browser.js`, 2 MB of generated wasm
glue) is no longer tracked. `crates/core/build.rs` resolves it beside the
archive, hands the path to `impeccable_core::browser::IN_PAGE_BUNDLE_JS`, and
live mode serves that. `scripts/check-detector-release.mjs` now requires it
and its `.sha256` in a detector release.
- The live crate embeds `skill/scripts/live-browser*.js` and
`modern-screenshot.umd.js` directly rather than through vendored copies, so
the binary and the installed skill cannot drift.
- `crates/browser/assets/` (an unused second copy of the bundle) is gone.
- `tests/lib/engine-bin.mjs` also accepts `target/release/impeccable`, so a
plain `cargo build --release -p impeccable` is enough to run `bun run test`.
Verified with the archive from a local detector build: `cargo test --workspace`
267 pass, oracle 795 pass / 0 fail / 0 missing, `bun run build` clean, the
default suite green, and the launcher's `engine-probe` handshake answering
through `skill/scripts/impeccable`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
247 lines
9.6 KiB
Rust
247 lines
9.6 KiB
Rust
//! The boundary itself, exercised end to end.
|
|
//!
|
|
//! `ffi.rs`'s unit tests pin the ABI number and the function table. These pin
|
|
//! the two callback paths, which nothing else in the open workspace reaches:
|
|
//! a `&dyn StyleMap` argument routed through the host vtable, and a whole
|
|
//! `&dyn Dom` walked by the closed driver one method at a time.
|
|
|
|
use impeccable_core::browser::fake_dom::FakeDom;
|
|
use impeccable_core::browser::{driver, page_checks, BrowserConfig, Dom, ElId};
|
|
use impeccable_core::checks::{measures, text_rules};
|
|
use std::collections::HashMap;
|
|
|
|
fn style(pairs: &[(&str, &str)]) -> HashMap<String, String> {
|
|
pairs
|
|
.iter()
|
|
.map(|(k, v)| (k.to_string(), v.to_string()))
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn style_map_callback_round_trips() {
|
|
// positioned_style_implies_escape reads eleven inset properties off the
|
|
// style map; each read is one host call.
|
|
assert!(measures::positioned_style_implies_escape(&style(&[
|
|
("position", "absolute"),
|
|
("left", "-40px"),
|
|
])));
|
|
assert!(measures::positioned_style_implies_escape(&style(&[(
|
|
"inset", "0 0 0 100%"
|
|
)])));
|
|
assert!(!measures::positioned_style_implies_escape(&style(&[
|
|
("top", "0px"),
|
|
("left", "8px"),
|
|
])));
|
|
}
|
|
|
|
#[test]
|
|
fn optional_style_map_absent_and_present() {
|
|
assert!(!measures::is_opaque_decorated_box(None));
|
|
assert!(measures::is_opaque_decorated_box(Some(&style(&[(
|
|
"backgroundColor",
|
|
"rgb(255, 255, 255)"
|
|
)]))));
|
|
assert!(!measures::is_opaque_decorated_box(Some(&style(&[(
|
|
"backgroundColor",
|
|
"rgba(0, 0, 0, 0.2)"
|
|
)]))));
|
|
|
|
assert!(!text_rules::is_repeated_text_container(None));
|
|
assert!(text_rules::is_repeated_text_container(Some(&style(&[
|
|
("boxShadow", "rgba(0, 0, 0, 0.1) 0px 2px 4px 0px"),
|
|
("borderRadius", "8px"),
|
|
("backgroundColor", "rgb(255, 255, 255)"),
|
|
]))));
|
|
}
|
|
|
|
/// A page whose text is mostly hidden at rest.
|
|
fn hidden_text_page() -> FakeDom {
|
|
let mut d = FakeDom::new();
|
|
let (html, body) = d.with_page();
|
|
for el in [html, body] {
|
|
d.set_styles(
|
|
el,
|
|
&[
|
|
("display", "block"),
|
|
("opacity", "1"),
|
|
("visibility", "visible"),
|
|
],
|
|
);
|
|
}
|
|
let visible = d.add(Some(body), "p");
|
|
d.add_text(visible, &"v".repeat(100));
|
|
d.set_styles(
|
|
visible,
|
|
&[
|
|
("display", "block"),
|
|
("opacity", "1"),
|
|
("visibility", "visible"),
|
|
],
|
|
);
|
|
let hidden = d.add(Some(body), "p");
|
|
d.add_text(hidden, &"h".repeat(300));
|
|
d.set_styles(
|
|
hidden,
|
|
&[
|
|
("display", "block"),
|
|
("opacity", "0"),
|
|
("visibility", "visible"),
|
|
],
|
|
);
|
|
let n = d.els.len() as ElId;
|
|
for id in 1..n {
|
|
if id != body && d.contains(body, id) {
|
|
d.add_selector(id, "body *");
|
|
}
|
|
}
|
|
d
|
|
}
|
|
|
|
#[test]
|
|
fn dom_callback_walks_a_whole_page() {
|
|
let d = hidden_text_page();
|
|
let m = page_checks::measure_hidden_text_dom(&d);
|
|
assert_eq!(m.total_chars, 400.0);
|
|
assert_eq!(m.hidden_chars, 300.0);
|
|
assert_eq!(m.hidden_samples.len(), 1);
|
|
assert!(m.hidden_samples[0].starts_with("hhh"));
|
|
|
|
// The same walk, through the driver: hundreds of host calls, then a
|
|
// findings tree that serializes back as JSON.
|
|
let out = driver::collect_browser_findings(&d, &BrowserConfig::default());
|
|
let json = driver::serialize_findings(&d, &out.groups);
|
|
assert!(json.is_array(), "{json}");
|
|
}
|
|
|
|
#[test]
|
|
fn browser_config_json_fields_survive() {
|
|
let mut d = FakeDom::new();
|
|
d.with_page();
|
|
let config = BrowserConfig {
|
|
extension_mode: true,
|
|
skip_scan: true,
|
|
..Default::default()
|
|
};
|
|
// skip_scan waives the page wholesale, so every stage answers empty:
|
|
// proof the config crossed intact.
|
|
let out = driver::collect_browser_findings(&d, &config);
|
|
assert!(out.groups.is_empty());
|
|
assert!(out.page_level.is_empty());
|
|
}
|
|
|
|
/// The visual-contrast subsystem is the widest set of shapes on the boundary:
|
|
/// tuples of `f64`, plans that mix a `serde_json::Value` with postcard fields,
|
|
/// and three `Result`s whose error half is a `Value`. Nothing else in this
|
|
/// workspace exercises it offline (it drives the in-page bundle), so this
|
|
/// walks every wire shape once and requires each to decode.
|
|
#[test]
|
|
fn every_visual_wire_shape_round_trips() {
|
|
use impeccable_core::browser::visual as v;
|
|
use impeccable_core::color::Rgba;
|
|
use serde_json::json;
|
|
|
|
let plan = v::raster_plan(1200.0, 900.0);
|
|
assert!(plan.width > 0.0 && plan.scale_x > 0.0);
|
|
let (px, py) = v::raster_pixel(&plan, 10.0, 20.0);
|
|
assert!(px.is_finite() && py.is_finite());
|
|
|
|
let sample = v::pixel_sample(10.0, 20.0, 30.0, 255.0);
|
|
assert_eq!(sample["status"], json!("sampled"));
|
|
assert!(v::sample_is_opaque(&sample));
|
|
assert!(v::alpha_composite(sample.clone(), &sample).is_object());
|
|
assert!(v::img_finish(sample.clone()).is_object());
|
|
assert!(v::css_url_finish(sample.clone()).is_object());
|
|
|
|
assert!(!v::raster_error_reason("SecurityError: tainted").is_empty());
|
|
assert!(v::raster_failure_sample("tainted").is_object());
|
|
assert!(v::raster_no_context_sample().is_object());
|
|
assert!(v::css_url_no_image().is_object());
|
|
assert!(v::unresolved_from_reasons(&["blend mode".to_string()]).is_object());
|
|
assert!(!v::needs_scroll_retry(&json!({ "status": "sampled" })));
|
|
|
|
let painted = v::PaintedRect {
|
|
left: 0.0,
|
|
top: 0.0,
|
|
width: 100.0,
|
|
height: 50.0,
|
|
intrinsic_width: 200.0,
|
|
intrinsic_height: 100.0,
|
|
};
|
|
assert!(v::img_loaded_source_point(&painted, 200.0, 100.0, 10.0, 10.0).is_some());
|
|
|
|
let text_color = Rgba::new(0.0, 0.0, 0.0, 1.0);
|
|
let candidate = json!({ "selector": "p", "reasons": [], "textColor": { "r": 0, "g": 0, "b": 0 } });
|
|
let verdict = v::finish_analysis(&candidate, &text_color, &[sample], 1);
|
|
assert!(verdict.is_object());
|
|
|
|
// The dom-taking half. A FakeDom with no hit-test points answers "no
|
|
// stack", which is a perfectly good decode.
|
|
let d = hidden_text_page();
|
|
let el = d.query_one(None, "body *").unwrap().unwrap();
|
|
let _ = v::collect_visual_contrast_reasons(&d, el);
|
|
let _ = v::collect_visual_contrast_candidates(&d, &json!({}));
|
|
let _ = v::stack_nodes(&d, el, 1.0, 1.0, 0.0);
|
|
let _ = v::img_source_point(&d, el, 100.0, 50.0, 1.0, 1.0);
|
|
let _ = v::raster_source_point(&d, el, 100.0, 50.0, 1.0, 1.0);
|
|
let _ = v::raster_finish(&d, el, v::pixel_sample(1.0, 2.0, 3.0, 255.0));
|
|
let _ = v::css_plan(&d, el, Some(&text_color));
|
|
let _ = v::css_url_source_point(&d, el, 100.0, 50.0, "cover", "50% 50%", 1.0, 1.0);
|
|
let _ = v::prepare_analysis(&d, &candidate);
|
|
}
|
|
|
|
/// The shims with no caller in this workspace: the oracle and the vector
|
|
/// replay never reach them, so nothing else would notice their wire shape
|
|
/// drifting. With this test every id in `fn_id::TABLE_ENTRIES` is crossed at
|
|
/// least once by `cargo test --workspace`.
|
|
#[test]
|
|
fn shims_without_an_open_caller_still_cross() {
|
|
use impeccable_core::browser::element_checks;
|
|
use impeccable_core::checks::{css_scan, html_patterns, rules};
|
|
use impeccable_core::color::Rgba;
|
|
|
|
assert!(rules::is_accent_color("rgb(99, 102, 241)"));
|
|
assert!(!rules::is_accent_color("rgb(17, 17, 17)"));
|
|
assert!(rules::resolve_serif(Some("Georgia, serif")).is_serif);
|
|
assert!(!rules::resolve_serif(Some("Inter, sans-serif")).is_serif);
|
|
|
|
assert!(text_rules::check_em_dash_overuse(Some("plain text")).is_empty());
|
|
|
|
// Reached in production only behind a gate the oracle corpus does not
|
|
// trip (a hover style, a page-level CSS analyzer run, the browser crate).
|
|
assert!(rules::check_hover_contrast(&rules::HoverContrastOpts::default()).is_empty());
|
|
let hidden = measures::check_content_hidden_at_rest(&measures::ContentHiddenInput {
|
|
total_chars: 1000.0,
|
|
hidden_chars: 800.0,
|
|
hidden_samples: vec!["lorem ipsum".to_string()],
|
|
});
|
|
assert_eq!(hidden.len(), 1);
|
|
assert_eq!(hidden[0].id, "content-hidden-at-rest");
|
|
assert!(css_scan::scan_css_text_for_glow("").is_empty());
|
|
assert!(css_scan::scan_css_text_for_radial_halo("").is_empty());
|
|
assert!(css_scan::scan_css_text_for_organic_clip_path("").is_empty());
|
|
assert!(css_scan::scan_css_text_for_marquee("", None).is_empty());
|
|
|
|
let props = css_scan::collect_css_custom_props(":root { --bg: #0b0b0f; }");
|
|
let _ = css_scan::css_text_has_dark_root_bg(":root { background: var(--bg); }", &props);
|
|
assert!(css_scan::scan_css_text_for_inset_stripe("").is_empty());
|
|
assert!(css_scan::scan_css_text_for_buried_raster("").is_empty());
|
|
assert!(css_scan::scan_css_text_for_pulsing_dot("", None).is_empty());
|
|
assert!(css_scan::is_round_dot_radius("50%", 8.0, 8.0));
|
|
assert!(!css_scan::is_round_dot_radius("2px", 8.0, 8.0));
|
|
|
|
assert!(html_patterns::scan_html_for_shape_assembled_illustration("<p>hi</p>").is_empty());
|
|
let corpora = html_patterns::build_html_pattern_corpora("<div class=\"card\">hi</div>");
|
|
let _ = html_patterns::check_html_patterns("<div class=\"card\">hi</div>", Some(&corpora));
|
|
|
|
let d = hidden_text_page();
|
|
let el = d.query_one(None, "body *").unwrap().unwrap();
|
|
assert!(!driver::scoped_ignore_active(&d, el, "ai-slop-glow"));
|
|
let _ = element_checks::is_rendered_for_browser_rule(&d, el);
|
|
|
|
// Rgba crosses in both directions (its JSON shape and its wire shape
|
|
// differ; see `impeccable_foundation::color::Rgba`).
|
|
assert!(measures::is_cream_color(Some(&Rgba::new(253.0, 250.0, 240.0, 1.0))));
|
|
assert!(!measures::is_cream_color(Some(&Rgba::new(0.0, 0.0, 0.0, 1.0))));
|
|
assert!(!measures::is_cream_color(None));
|
|
}
|