English | Português Brasileiro
- Run the right suite for the risk, not every browser path by default
- Browser runtime tests are slower and host-dependent
- Schema and inventory tests catch contract drift without Chrome
- Keeping categories explicit protects local iteration speed
- Prefer local validation with
cargo test, and use the e2e scripts only for paths nocargo testgate covers
- Unit and library tests in
src/(cargo test --lib) - CLI smoke tests such as
tests/doctor_cli.rsandtests/goto_smoke.rs - Envelope and schema gates such as
tests/envelope_schema.rsandtests/parity_toolref_schema.rs - Parity matrix tests (
tests/parity_matrix.rs) - Run inventory gate:
tests/parity_run_inventory.rsenforcesRUN_DISPATCHED_CMDS∪ intentional exclude (includesprint-pdf,select-option,pick) - Clap surface gate:
tests/clap_command_debug_assert.rsrunsCli::command().debug_assert() - Robots and pipe behaviour tests (
tests/robots_http.rs,tests/pipe_broken.rs) - Golden i18n and cold-start helpers (
tests/golden_i18n.rs,tests/cold_start.rs) - Optional e2e CDP event coverage when Chrome is available (
tests/e2e_cdp_events.rs) - Full 53-tool DevTools e2e script (legacy filename):
scripts/e2e_all_52_tools.sh - Live CLI inventory is 71 agent names (
commands --json) — broader than the 53 tool-ref e2e set; includes agent-inventoryselect-optionandpick(run/exec/schema, not clap), metalocaleandman, plus clapsubmitandstorage - Product gates introduced in v0.1.7 and still shipped in 0.2.0 (local, Chrome serial when required):
tests/dialog_multitab_gate.rs— multi-tab dialog isolation +dialog_settled(GAP-054)tests/option_pick_gate.rs— native selectinput+change(GAP-055)tests/wait_conditions_gate.rs—wait_timeout_msdeadline honesty (GAP-053)tests/scrape_step_gate.rs— scrapeformat/formatsin run without HTML monster (GAP-057)- Lighthouse unit fixtures:
scripts/fixtures/lighthouse/minimal_lhr.json+chrome_captured_lhr.json(real LHR-shaped scores_from_lhr parse; GAP-021 partial)
- Residual integration suite:
tests/residual_one_shot.rs(marker zero, Singleton non-growth, BORN fixture wipe, doctor residual fields) - Local residual gates:
scripts/residual-check.sh,scripts/residual-stress.sh(local maintainer scripts only) - The suite REQUIRES
--test-threads=1; nothing in the code enforces it - Measured: concurrent Chrome launches produce
SingletonLock: No such file or directory,No chromiumoxide Page for session_idandPage.navigate: Request timed out - Serial is also faster here: 101s serial against 148s parallel
- There is no
serial_testdependency and no#[serial], so a barecargo test, an IDE runner orcargo nextestall reproduce those failures - The suite also requires that NOTHING ELSE drive cargo against the same
target/while it runs - Measured on 2026-08-25: a
cargo runlaunched alongside the suite rebuilttarget/debug/browser-automation-cliwith DEFAULT features and replaced the binary in flight - Four gates in
tests/image_media_cli_e2e.rsthen failed withrebuild with the image-svg Cargo feature— a true statement about the wrong artifact, which reads as a product regression CARGO_BIN_EXE_*does NOT protect against this: it resolves at COMPILE time and guarantees which build PRODUCED the artifact, never who overwrites that shared path afterwards- A red that disappears on an isolated re-run diagnoses the measurement, never the product; re-run the failing test alone before concluding regression
- This project uses ZERO UNCONDITIONAL
#[ignore], deliberately: libtest reports an ignored test asok, which is the same false green a skip produces - The tree carries NINE
#[cfg_attr(…, ignore = …)]attributes across three test files, and exactly two of them ignore UNDER--all-features: the#[cfg_attr(feature = "…", ignore = "feature is on in this build")]oftests/image_wave6_codecs.rsandtests/video_manifest_hls_dash.rs - The other seven take the
not(feature = "…")form, which ignores nothing under--all-features; a count stated without the predicate that measures it counts what the BUILD ignores and reads as what the TREE contains, and goes false the moment a feature is added - Both assert the fail-closed behaviour of a build WITHOUT the feature, so under
--all-featuresthe assertion is unreachable by construction and the test still runs for real in the build it describes - That is the same distinction
strict-gatesdraws: unreachable by construction is excused, a missing tool is not - A test that cannot run declines through
skip_with_reasonorskip_with_remedy(tests/common/mod.rs), never through a bareeprintln! - Under
--features strict-gatesthose helperspanic!instead of returning, so a declined gate FAILS rather than reporting a pass - The local runner
scripts/ci-check.shruns with--all-features, which turnsstrict-gateson - Its test step is
cargo test --tests --all-features --quiet -- --test-threads=1, which you can run directly on any platform - Unit-side declines use
skip_unit_test(src/test_utils.rs) - Because a decline is a failure there,
strict-gatesturns every host tool a test needs into a HARD PREREQUISITE - Required on PATH for a green
--all-featuresrun: a Chrome or Chromium buildfind_chromecan resolve,ffmpeg,ffprobe,Xvfb(Linux),/bin/sh, andredis-server redis-servergatesredis_real_server_if_present(src/cache/tests.rs) only; the RESP protocol axis itself is always covered in-process byredis_roundtrip_via_resp_mock, which needs no external binary- Measured on Fedora 44:
dnf install redisresolves tovalkeyplusvalkey-compat-redis, which provides/usr/bin/redis-serverand satisfies the gate unchanged - A missing tool is deliberately NOT excused: installing it makes the assertion reachable, which is the distinction
strict-gatesexists to enforce - The opposite shape — an assertion unreachable by construction, such as the host already occupying the display the test needs free — declines with a bare
eprintln!plus a written justification (src/native/cdp/xvfb/spawn.rs)
timeout 300 cargo test --locked
timeout 300 cargo test --lib --locked
timeout 120 cargo test --lib residual:: --locked
timeout 120 cargo test --test residual_one_shot --locked
timeout 120 cargo test --test parity_run_inventory --locked
timeout 120 cargo test --test clap_command_debug_assert --locked
timeout 180 cargo test --test dialog_multitab_gate --locked
timeout 180 cargo test --test option_pick_gate --locked
timeout 180 cargo test --test wait_conditions_gate --locked
timeout 120 cargo test --test scrape_step_gate --locked
timeout 120 cargo test --lib scores_from --locked
timeout 120 cargo clippy --all-targets --locked -- -D warnings
cargo fmt --check- Run a single file with
cargo test --test doctor_cli --locked - Use
-- --nocaptureonly while debugging - Prefer library and schema gates first when iterating on contracts
- A thread stack overflow during
cargo testhas no measured remedy on this page - Re-run the failing target alone with
cargo test --test <name> --locked, keep the test name and the error output, and report it as a defect instead of skipping the suite
cargo build --release --locked
bash scripts/e2e_all_52_tools.sh- Requires a release binary at
target/release/browser-automation-cli(build withcargo build --release --lockedfirst) - Exercises DevTools-parity tools against the local fixture page under
scripts/fixtures/e2e_page/ - Writes a report under a temp workdir and prints PASS/FAIL/SKIP counts
- Maintainer evidence for v0.1.4: 53 PASS / 0 FAIL on a local host with Chrome (residual A001 closed; GAP-001…025 hard-close)
- Maintainer evidence for v0.1.5: residual-zero disk closed (RES-01…12);
cargo test --lib residual::+cargo test --test residual_one_shot+ local residual-check PASS - Maintainer evidence for v0.1.6:
TOTAL=53 PASS=52 FAIL=0 SKIP=1 - The lighthouse mock path is SKIP (CONTRACT-ONLY)
- Never claim a full e2e lighthouse parser PASS
- Lighthouse parser confidence is unit-level:
scores_from_lhragainstminimal_lhr.jsonand real sanitizedchrome_captured_lhr.json(Lighthouse 13.4.1 shape) - The 52-tool suite does not replace residual smokes for commands outside the tool-ref set
cargo build --release --locked
cargo test --lib residual:: --locked
cargo test --test residual_one_shot --locked
bash scripts/residual-check.sh
# optional stress of N one-shots:
# bash scripts/residual-stress.shresidual_one_shotcovers: CLI marker zero after goto, Chromium Singleton non-growth after print-pdf, BORN wipe of stale Singleton fixture, doctor residual fieldsresidual-check.shruns doctor (BORN GC path-light) + one-shot print-pdf + asserts zero CLI markers and doctor JSONresidualresidual-stress.shrepeats one-shot work to stress residual hygiene locally- Doctor check id under test:
residual_disk(path-light residual disk hygiene) - Doctor top-level JSON field under test:
residual(ResidualDiskReport) - Doctor residual fields under test:
cli_marker_dirs,chromium_tmp_singleton_orphans,scavenge_safe_candidates,live_cli_marker_processes(legacy),sibling_live_processes,orphan_marker_dirs,ghost_marker_processes,foreign_root_orphans,scanned_roots - Residual-zero agent contract:
residual_diskmust notfail(zeros onorphan_marker_dirs+ghost_marker_processes); after DIE alone also zerocli_marker_dirs+chromium_tmp_singleton_orphans;sibling_live_processes>0is healthy concurrency; do NOT require zerolive_cli_marker_processes - Age floor for production stale GC is 60s; tests may use zero-age library helpers for fixtures
cargo test --test dialog_multitab_gate --locked
cargo test --test option_pick_gate --locked
cargo test --test wait_conditions_gate --locked
cargo test --test scrape_step_gate --locked
# Lighthouse pure parse (no claim of e2e PASS):
cargo test --lib --locked scores_from
# Residual still required:
bash scripts/residual-check.shdialog_multitab_gate: isolation tab1 + accept owner viaPage::session_idmulti-tab gate; assertsdialog_settledwithout invented wait (GAP-054)option_pick_gate: native select events +via: native_select(GAP-055)wait_conditions_gate: deadline honorswait_timeout_ms(~2s, not silent default) (GAP-053)scrape_step_gate: run scrapeformat=textwithout HTML dump (GAP-057)- Lighthouse e2e mock remains SKIP; unit fixtures are the honest parser gate (GAP-021 partial)
grabencode: png|jpeg|webp only; AVIF removed (breaking) — residual smokes must not pass--format avif- GAP-024 intentional residual: PRD wishlist divergences stay intentional (do not claim full PRD parity)
- Do NOT treat remote orchestration dashboards as product surface; use local cargo and
scripts/*-check.shonly
- These gates cover the defects
CHANGELOG.mdlists under0.2.0 - None of them needs a real Chrome
timeout 900 cargo test --test cdp_pipe_sigpipe_gate --locked
timeout 900 cargo test --test chrome_failed_launch_group_gate --locked
timeout 900 cargo test --lib native::cdp::pipe --locked
timeout 900 cargo test --lib native::cdp::xvfb --locked
timeout 900 cargo test --lib native::cdp::chrome::args --lockedtests/cdp_pipe_sigpipe_gate.rsproves a Chrome that exits at once never kills the CLI bySIGPIPE- It sets
chrome_pathto/usr/bin/falsewithconfig setinside its own temporary XDG tree - It launches
goto about:blank20 times and fails if any run dies by signal or leaves no JSON envelope - It runs on Unix only and skips loudly without the binary or
/usr/bin/false tests/chrome_failed_launch_group_gate.rsproves a failed launch leaves no member of Chrome's process group alive- Its fake browser starts
sleep 60in its own group, writes that pid and exits before DevTools answers - It fails if that pid is still alive and not a zombie once the CLI returns
- It runs on Linux only and skips loudly without the binary or
/bin/sh
native::cdp::pipe::tests::the_port_switches_are_replaced_by_the_pipeproves the port and address switches are removed and--remote-debugging-pipeappears exactly oncenative::cdp::pipe::bridge::tests::messages_are_split_on_the_zero_byteproves messages from Chrome are split on the zero byte and the readiness probe reply is consumednative::cdp::pipe::bridge::tests::a_message_cut_by_end_of_file_is_not_forwardedproves a truncated message is droppednative::cdp::pipe::bridge::tests::written_messages_carry_the_terminatorproves every message written to Chrome ends with the zero bytenative::cdp::pipe::bridge::tests::the_bridge_refuses_a_wrong_path_and_serves_the_right_oneproves the WebSocket bridge refuses a path without the token and accepts the tokened pathnative::cdp::pipe::bridge::tests::shutdown_returns_within_its_grace_while_the_pipe_is_still_heldproves teardown returns withinCDP_PIPE_SHUTDOWN_GRACE_MSplus one second while the pipe is still held open- A host where the pipes cannot be created declines through
skip_unit_test, which fails understrict-gates
native::cdp::xvfb::auth::tests::the_entry_follows_the_xauthority_layoutproves theMIT-MAGIC-COOKIE-1entry has the 46-byte Xauthority layoutnative::cdp::xvfb::auth::tests::the_file_is_private_and_removed_with_its_ownerproves the cookie file has mode 0600 and is gone once its owner dropsnative::cdp::xvfb::auth::tests::orphaned_cookies_are_swept_and_live_ones_keptproves a cookie of a dead pid is removed while a live one and an unrelated file staynative::cdp::xvfb::display::tests::a_lock_left_by_a_dead_server_does_not_retire_the_numberproves a lock naming a dead process frees the number, while a bare socket, a live server or an unreadable lock keep it takennative::cdp::xvfb::spawn::tests::a_server_that_exits_is_reported_at_once_not_at_the_deadlineproves a server that exits is reported before the startup deadlinenative::cdp::chrome::args::tests::the_x11_pin_follows_the_display_outcomeproves--ozone-platform=x11appears once when the private display started, never when it did not, and never over a platform switch already in the argument list, a defensive branch that no flag or XDG key reaches- The cookie file and sweep tests run on Unix only
tests/holds 94 integration gate files, each run withcargo test --test <name> --locked- Each family below closes one class of defect, never one command
- Every gate is local and needs no runner beyond cargo
- A gate that cannot run its precondition SKIPs loudly instead of passing silently
tests/block_detection_gate.rs— a CAPTCHA wall must reach the envelope instead of passing as content- It requires exit
6, anerror.kindofblockedanddata.block_detectiontogether - An ordinary page is the negative control and must come back unflagged
- The browser engine must report the same block the HTTP engine reports
tests/input_trace_gate.rs— asserts what the page RECEIVED, not the resulting effect- The
humanprofile must deliver realwheeland key events with non-uniform spacing - The
directprofile is the negative control and emits no synthetic wheel or key event tests/xvfb_gate.rs— the doctor claim about Xvfb must match what the host can do- A headed Linux run must leave no display lock behind
- A host without Xvfb is a printed skip in a default build and a failure under
strict-gates tests/compression_gate.rs— every advertised content-coding must arrive decoded- No
content-encodingmay survive into the envelope, which is proof the body was decompressed
tests/clap_arg_coverage.rs— every top-level subcommand renders help and missing args error- It pins the renamed payload flags
fields-json,cookies-jsonanddetailedto their fields - It also pins that
imageexposes no text-recognition action tests/clap_global_flag_collision.rs— no local subcommand flag may shadow a global long or shorttests/help_description_gate.rs— every subcommand at every depth carries a non-empty description- Its detector is a shared function also driven against a synthetic tree, proving its own sensitivity
tests/manpage_cli.rs—manemits roff, writes atomically and rejects path traversal
tests/envelope_shape_gate.rs— norunstep may carrydataandresultwith equal content- A fixed ten-step script must stay inside a declared byte budget
tests/view_precondition_gate.rs— a blank page answers a precondition kind, neverusage--allow-emptymust succeed and a malformed argv must still answerusagetests/config_key_diagnosis_gate.rs—config setdiagnoses the KEY independently of the VALUE- An unknown key reads the same whatever the value looks like
tests/devtools_envelope_behavior.rs— envelope fields of DevTools-parity commands, offline and with Chrometests/grab_envelope_gate.rs—grabreports the width and height it just wrote- The reported size must match the file for viewport, full-page and element captures
tests/preflight_no_browser.rs— an invalid script fails before Chrome is launched- The proof is residual: no marker profile appears and the failure is far faster
tests/drag_route_gate.rs— drag must reach the page handler and reportrouteasintercepted- A page without a
dragstarthandler must report the degraded synthetic route and warn tests/submit_form_gate.rs—submitfires the form event and waits for the navigation- Pressing the look-alike button succeeds while submitting nothing, which is the defect being separated
tests/upload_cdp_e2e_gate.rs— the file really reaches a real Chrome input element- It pins
--script -reading stdin and--scripttaking a path, never inline JSON tests/extract_step_gate.rs— two selectors in one run must resolve to two different nodestests/cookie_jar_gate.rs— a cookie set is found by a laterlist, andclearreally emptiestests/dialog_if_present_gate.rs— absence is tolerated with the flag and fatal without ittests/eval_typed_gate.rs— an object return arrives as structure, andtypedreportsvalue_typetests/ref_invalidation_gate.rs— the@eNstaleness marker appears only when the tree really movedtests/assert_step_gate.rs— everyassertkind can FAIL the run and reach the exit codetests/record_gate.rs— the NDJSON thatrecordwrites replays throughrun --scriptunchanged
tests/allowed_roots_gate.rs— a local path outside the allowed roots is refused as policy- The refusal must not be classified as
usage, and--allow-outside-rootsrestores access tests/failure_dump_gate.rs— a failing run writes the captured console and network rings to disk- A successful run, or a failing run without the flag, must leave no artifact
tests/lifecycle_group_kill.rs— one signal reaps a whole process group, with a pid-tree fallback- It refuses our own group,
initand the zero group tests/lifecycle_hard_kill_gate.rs—SIGKILLon the CLI leaves no browser process from its grouptests/chrome_failed_launch_group_gate.rs— a failed Chrome launch leaves no member of Chrome's process group alive- A fake browser forks a
sleepthat stays in its group and exits before DevTools answers, and that pid must be gone once the CLI returns tests/cdp_pipe_sigpipe_gate.rs— a Chrome that exits at once must never kill the CLI bySIGPIPEthrough the DevTools pipe- It launches 20 times with
/usr/bin/falseas the browser and requires every run to end by exit code with a JSON envelope tests/signal_shutdown.rs— SIGTERM and SIGINT against a live CLI child must not hangtests/residual_report_contract.rs— pids are counted once and impostor processes are not counted- The report emits the roots it scanned and reports foreign-root orphans separately
- A live owner pid protects a concurrent invocation from collection
tests/scrape_agent_native_gate.rs— offline projection, truncation flag, path filters and format promotiontests/scrape_wave6_gate.rs—--format feed,rel=nextfollowing and near-duplicate collapse- Each behaviour is paired with its OFF control, so an unconditional implementation fails
tests/scrape_wave7_e2e_gate.rs— anchorrel=next, JSON Feed andbatch-scrape --dedup-similar- Pagination discovery must never outrank a robots
Disallow tests/crawl_plan_llms_txt_gate.rs—crawl --dry-runresolves the plan and fetches nothing- It is aimed at an unreachable host, so a success envelope IS the evidence
tests/audio_local_gate.rs— inventory, schema actions, SSRF block and conversion when ffmpeg existstests/image_wave6_codecs.rs— AVIF encode, HEIC fail-closed, SVG raster, GIF frames, resize backendtests/image_metadata_iptc_xmp.rs— IPTC IIM and XMP readers plus the SVG sanitiser threat gatetests/image_media_cli_e2e.rs— the same wave-6 surfaces driven through the real binary- A feature that is implemented but unreachable from
mainis what it catches tests/video_manifest_parse.rs— the HLS and DASH parser at library level, fetching nothingtests/video_manifest_hls_dash.rs— variant ladders, relative URI resolution and the byte ceilingtests/video_manifest_cli_gate.rs—video manifestis an advertised action reachable from argvtests/video_site_extraction_rejected.rs— a player page and a manifest get different actionable errors
tests/proptest_parsers.rs— property tests for offline parsers, robots body and envelope round triptests/tracing_local_log_schema.rs— field names of rotated JSON log lines underconfig set log_to_file- The lines are local files only; this product has no remote telemetry
- Discover it live with
browser-automation-cli commands --json
assert attr back batch-scrape click-at commands completions config console cookie
crawl devtools3p dialog doctor drag emulate eval exec extension extract feed fill-form
find-paths forward goto grab heap hover image video audio keys lighthouse locale man map mitm monitor
net page parse perf pick press print-pdf qr record reload resize run schema scrape screencast
scroll search select-option sg-rewrite sg-scan sheet-write sitemap storage submit text type
upload version view wait webmcp workflow write
pickandselect-optionare multi-step inventory names used inrunscripts- The clap product subcommand count is therefore 69, which is 71 agent names minus 2 run-only names
- After editing inventory or flat-list docs, run the local gate
bash scripts/inventory-flat-check.sh - It expects live
commands --jsonto return 71 names includingimage,video,audioandrecord - The old name
scripts/verify-inventory-flat.shstays as a thin shim that delegates to it - The local runner
scripts/ci-check.shdiscovers verifiers with the globscripts/*-check.sh - The old filename never matched that glob, so the gate never ran in the bundle
- The docs then drifted to a stale count of 67 while the runner reported green
- Run these after e2e when validating the full 71-name inventory
# print-pdf artifact (one-shot + run)
browser-automation-cli --json print-pdf --url https://example.com --path /tmp/page.pdf
# monitor baseline check
browser-automation-cli --json monitor check --url https://example.com --baseline /tmp/mon.base --write-baseline
# QR encode/decode (no Chrome)
browser-automation-cli --json qr encode --text 'hello' --format png --path /tmp/qr.png
browser-automation-cli --json qr decode --path /tmp/qr.png
# Local image pipeline (no Chrome; agent-native — no pixel base64)
browser-automation-cli --json image download 'https://www.w3.org/People/mimasa/test/imgformat/img/w3c_home.png' -o /tmp/w3c.png
browser-automation-cli --json image info --path /tmp/w3c.png --select format,width,height,sha256
browser-automation-cli --json image convert --path /tmp/w3c.png --format webp -o /tmp/w3c.webp
browser-automation-cli --json image exif --path /tmp/w3c.webp --select tags,path # alias tags→exif; EXIF only (no IPTC/XMP)
# AVIF/HEIC: magic reject (no pure-Rust encode). SVG: use --allow-non-image for raw bytes (no resvg).
# image download = single image URL (SSRF+magic) — NOT a whole-site tree download.
# Upload needs Chrome + navigated file input (dry: schema upload):
browser-automation-cli schema upload >/dev/null
# browser-automation-cli --json run --script '[{"cmd":"goto","url":"…"},{"cmd":"upload","target":"input[type=file]","path":"/tmp/w3c.webp"}]'
# Magic-parser fuzzing ships as a normal gate — no nightly, no libFuzzer, no separate binary:
cargo test --test fuzz_magic_parsers_gate
# Deterministic xorshift corpus: real container prefixes (PNG/JPEG APP1+APP13/GIF/RIFF/
# ISOBMFF ftyp/Matroska/OGG/FLAC/ID3/ADTS/WAV/AIFF), truncated and bit-flipped, then fed to
# image_local::detect_format, video_local::detect_container and audio_local::detect_container.
# The property is that they classify or return a typed error — never panic, never hang.
# The old `cargo fuzz` recipe was never runnable here: it needs nightly, it needs libFuzzer
# from LLVM (a C++ dependency in a rust-native crate), and no gate ever invoked it.
# Local video pipeline (no Chrome; needs host ffmpeg/ffprobe for convert/to-mp3/trim/thumbnail)
# Integration gate (skips convert/trim/thumbnail when ffmpeg missing):
# cargo test --test video_local_gate --locked
# ffmpeg -y -f lavfi -i testsrc=duration=0.5:size=160x120:rate=10 -c:v libx264 -pix_fmt yuv420p /tmp/in.mp4
browser-automation-cli --json video info --path /tmp/in.mp4 --select container,duration_secs,streams
browser-automation-cli --json video convert --path /tmp/in.mp4 --format webm -o /tmp/out.webm # auto re-encode when copy incompatible
browser-automation-cli --json video to-mp3 --path /tmp/in.mp4 -o /tmp/a.mp3
browser-automation-cli --json video trim --path /tmp/in.mp4 --start 0 --duration 0.2 -o /tmp/clip.mp4
browser-automation-cli --json video thumbnail --path /tmp/in.mp4 --at 0.1 -o /tmp/thumb.png
# Manifest summary needs no ffmpeg: HLS .m3u8 / DASH .mpd structure, zero media fetched
browser-automation-cli --json video manifest --path /tmp/master.m3u8
browser-automation-cli schema video >/dev/null
# find-paths (no Chrome)
browser-automation-cli --json find-paths 'Cargo.*' .
browser-automation-cli --json find-paths --glob '**/*.rs' .
# sheet-write / sg-scan / sg-rewrite (no Chrome)
printf 'a,b\n1,2\n' > /tmp/rows.csv
browser-automation-cli --json sheet-write /tmp/rows.csv -o /tmp/out.xlsx
browser-automation-cli --json sg-scan . --limit 20
browser-automation-cli --json sg-rewrite .
# run JSON array + json-steps stream (GAP-020)
cat > /tmp/demo.array.json <<'JSON'
[{"cmd":"goto","url":"https://example.com"},{"cmd":"view"}]
JSON
browser-automation-cli --timeout 60 --json run --script /tmp/demo.array.json
browser-automation-cli --timeout 60 --json --json-steps run --script /tmp/demo.array.json
# wait multi-selector / url_contains (GAP-019/024)
cat > /tmp/wait.json <<'JSON'
[
{"cmd":"goto","url":"https://example.com"},
{"cmd":"wait","selector":"h1, body","ms":3000},
{"cmd":"wait","url_contains":"example.com","ms":3000}
]
JSON
browser-automation-cli --timeout 60 --json run --script /tmp/wait.json
# pick / select-option (run-only inventory; GAP-023)
# browser-automation-cli --timeout 60 --json run --script '[{"cmd":"goto","url":"…"},{"cmd":"pick","target":"…","option":"…"}]'
# assert console kinds (GAP-025)
# browser-automation-cli --capture-console --timeout 60 --json run --script '[{"cmd":"goto","url":"https://example.com"},{"cmd":"assert","kind":"console_empty"}]'
# schema positional (GAP-022)
browser-automation-cli --json schema run
browser-automation-cli --json schema --cmd wait
# view --allow-empty (GAP-012)
browser-automation-cli --json view --allow-empty
# multi-format scrape + batch/crawl browser engine (GAP-009/010)
browser-automation-cli --json scrape https://example.com --format markdown,html,links --engine http
printf '%s\n' 'https://example.com' > /tmp/urls.txt
browser-automation-cli --json batch-scrape --urls-file /tmp/urls.txt --format text --engine http --concurrency 1
# browser-automation-cli --timeout 120 --json batch-scrape --urls-file /tmp/urls.txt --format text --engine browser --concurrency 1
# MITM capture-url + har --out (GAP-011)
browser-automation-cli --json mitm init-ca
# browser-automation-cli --json mitm capture-url https://example.com --seconds 15 --har /tmp/cap.har
# browser-automation-cli --json mitm har --out /tmp/capture.har
# browser-automation-cli --json mitm redact
# config list-keys + redis honesty (no rediss)
browser-automation-cli --json config list-keys
# browser-automation-cli --json config set cache_backend redis
# browser-automation-cli --json config set cache_redis_url redis://127.0.0.1:6379
# lighthouse binary_source (mock)
browser-automation-cli --json lighthouse https://example.com \
--lighthouse-path ./scripts/mock-lighthouse.sh | jaq '.data.binary_source // .'
# parse PDF / DOCX with optional PII redact
browser-automation-cli --json parse tests/fixtures/hello.pdf
browser-automation-cli --json parse tests/fixtures/hello.docx --redact-pii
# extract --llm fail-closed without XDG key
browser-automation-cli --json extract https://example.com --llm --question 'What is the title?'
# expect usage envelope requiring: config set openrouter_api_key
# clap JSON usage error (GAP-002)
browser-automation-cli --json not-a-real-command 2>/dev/null | jaq -e '.ok == false' || true
# dialog soft path
browser-automation-cli --json dialog accept --if-present
# console dump always []
browser-automation-cli --capture-console --json console dump --path /tmp/console.json
# beforeunload flag help surface
browser-automation-cli goto --help | rg handle-before-unload
# page isolated context
browser-automation-cli page new --help | rg isolated-context
# print-pdf in run
# cat > /tmp/pdf.run.json <<'JSON'
# [{"cmd":"goto","url":"https://example.com"},{"cmd":"print-pdf","path":"/tmp/page-from-run.pdf"}]
# JSON
# browser-automation-cli --timeout 60 --json run --script /tmp/pdf.run.json
# schema already covered
# locale / man meta + submit/storage/image/video/audio/record (inventory 71)
browser-automation-cli --json locale
browser-automation-cli --json man >/tmp/browser-automation-cli.1
browser-automation-cli --json schema submit
browser-automation-cli --json schema storage
browser-automation-cli --json config list-keys
browser-automation-cli --json config set dialog_settle_ms 2000
# residual doctor fields (v0.1.5 law still current)
browser-automation-cli doctor --offline --quick --json | jaq '.residual'- Also useful: browser format scrape,
config path,mitm start, doctor XDG, i18n--lang pt-BR - Contract tests to cite in evidence:
parity_run_inventory,clap_command_debug_assert,residual_one_shot, residual lib tests,dialog_multitab_gate,option_pick_gate,wait_conditions_gate,scrape_step_gate
browser-automation-cli --json lighthouse https://example.com \
--lighthouse-path ./scripts/mock-lighthouse.sh- Use
--lighthouse-pathor XDGlighthouse_pathto point atscripts/mock-lighthouse.shwhen a real Lighthouse install is unavailable - Resolve order: flag → XDG
lighthouse_path→ PATH - Envelope reports
binary_sourceasrealormock - The mock writes minimal HTML/JSON reports for smoke paths
- Doctor reports lighthouse presence/source as informational when the binary is missing
- v0.1.6 honesty: the e2e suite SKIPs the lighthouse mock path — never report that as a full parser PASS
- Parser confidence: unit tests on
scripts/fixtures/lighthouse/minimal_lhr.jsonandchrome_captured_lhr.json
- Run fmt, clippy, and non-browser contract tests first on your machine
- Browser-backed tests require Chrome or Chromium installed locally
- Validation runs locally with cargo and e2e scripts on the maintainer machine
- Keep crates.io publish blocked without explicit maintainer approval
- Optional pillar smokes after e2e:
run+--json-steps, residual PRD commands above, residual-check,config path,mitm capture-url, doctor XDG + residual
cargo build --release --locked
bash scripts/generate_command_schemas.sh
bash scripts/generate_command_schemas.sh --check
bash scripts/audit_bilingual_docs.shgenerate_command_schemas.shwrites onedocs/schemas/<cmd>.schema.jsonper inventory command from liveschema --cmd(meta.rs surface)--checkfails when static command schemas drift from the installed binary- Envelopes and
run-script-step.schema.jsonare preserved and not overwritten by the generator audit_bilingual_docs.shcomparesbrowser-automation-cliinvocations inside code fences for EN and.pt-BRpairs- Exit
0means fence multisets match; exit1means drift; exit2means a missing pair file
cargo build --release --locked
bash scripts/agent-ops-check.sh
cargo test --test agent_ops_cli --lockedagent-ops-check.shruns the compiled binary, never the internal functions- It asserts an impossible output ceiling reports exit
2with an envelope - It asserts a plausible ceiling emits a payload or an error, never silence
- It asserts
--fields,--sort-rowsand--dedupe-byname an unresolved path - It asserts a resolving
--fieldskeeps the envelope quiet - It asserts suggestion messages cite only global flags, in EN and pt-BR
tests/agent_ops_cli.rsadds 10 integration tests driven through argv- Coverage of the eight agent-ops flags under
tests/was previously zero scripts/ci-check.shdiscovers this gate through the globscripts/*-check.shverifier-controls-check.shcarries 1 positive control for this gate- The script resolves the binary with a PATH fallback on purpose
- The controls harness copies the tree without
target/, so lookup must not abort - A gate that aborts before its first assertion is indistinguishable from a passing gate
- Project law: a control that never fails is a verifier that does not verify
- Measured bundle state:
ci-check OK (all steps passed)with 249 PASS and 0 FAIL
cargo build --release --locked
bash scripts/doc-coverage-check.shdoc-coverage-check.shreads the live binary surface, never a transcribed list- Assertion 1: both
CONFIGURATIONdocuments cover every live XDG key - Assertion 2: no document still teaches a retired configuration key
- Assertion 3: entry-point documents name every live command
- Assertion 4: every public document has a
.pt-BRmirror - Assertion 5: no document presents a per-command flag as global
- Assertion 6: no document teaches a product environment variable
- Assertion 7: every
llmslink resolves to a real file - A local counter guards the envelope-flag line against an earlier failure
scripts/ci-check.shdiscovers this gate through the globscripts/*-check.shverifier-controls-check.shcarries 3 positive controls for this gate- The script resolves the binary with the same PATH fallback for the same reason
- KNOWN LIMITATION of this whole gate set: no documentation gate EXECUTES a fenced block
audit_bilingual_docs.shcompares the CLI invocations between the two languages and never runs them- A published recipe that exits non-zero therefore passes every documentation gate
- Measured 2026-08-28: four
run --scriptrecipes indocs/COOKBOOK.mdexited non-zero while all five gates were green - The causes were an element absent from the target page, a stealth-profile contradiction the product refuses by design, and two blocks assuming input files they never create
- NONE was a step-key error, so a static check of every documented step against
schema <command>would have found none of them - That static check was written and run over 327 documented steps: it produced six hits and all six were false positives
- Recipes are verified by EXECUTING them, and that execution is manual today
scripts/verifier-controls-check.shis the meta-gate: it proves the other verifiers can FAIL- Project law: a control that never fails is a verifier that does not verify
- Each control copies the working tree into a throwaway sandbox and mutates it
- The mutation is chosen so the verifier under test MUST report failure
- A verifier that stays green under its own mutation is blind, and the harness says so
bash scripts/verifier-controls-check.shruns it;ci-check.shalso discovers it through thescripts/*-check.shglob- The control count is derived at runtime by counting
^run_control, never frozen in prose - This is the most expensive step in the bundle: one whole tree copy per control
- The copy uses
fd -H -I, so paths the git ignore rules hide are still present in the sandbox - Without
-Ithe sandbox silently lackeddocs_prd/,gaps.mdandCLAUDE.md, which is the partial copy its own header warns against - Killing it mid-run leaves one
bac-verifier-control-*directory behind, because cleanup runs on the return paths only
scripts/holds 44 top-level.shfiles;tests/holds 94.rsgate files- Measured 2026-09-13 with
fd -d 1 -e sh . scripts/andfd -d 1 -e rs . tests/ tests/doc_measured_claims_gate.rsre-measures both, because the 2026-08-28 pair read 42 and 73 and went stale unnoticed-d 1is part of the measurement, not decoration: without it thescripts/count picks up subdirectories and reads 47bash scripts/ci-check.shis the local runner for the bundle- It auto-discovers every executable
scripts/*-check.shthrough that glob - A script whose name does not end in
-check.shnever enters the bundle and must be invoked by name - Measurement tools and generators are named that way on purpose: they report or write, they do not assert
- Prefer running the whole bundle before release, and a single verifier while iterating
scripts/interior-mutability-check.sh— rejectsstatic mutandArc<RefCell>/Rc<RefCell>shapes undersrc/scripts/memory-check.sh— RAII, ownership and allocation hygiene; rejectsstd::process::exitundersrc/scripts/ownership-check.sh— ownership, borrowing and lifetime rules, anchored on session interact plus native launch and tabs modulesscripts/macros-check.sh— declarative and built-in macro hygiene; rejectstodo!(,unimplemented!(anddbg!(left in productionscripts/json-ndjson-check.sh— JSON and NDJSON rules; assertsserde_jsonis the sole production parser and rejectssimd-json/sonic-rsscripts/network-check.sh— network rules for a one-shot agent CLI: CLI plus XDG only,no_proxy, SSRF policy and body caps, anchored on the robots and CDP discovery modulesscripts/process-check.sh— external process execution rules: timeout capture helper, BatBadBut defense, no shell spawn in productionscripts/parallelism-check.sh— bounded parallelism; requiressrc/concurrency/and rejectsBox::leak/mem::forgetinside itscripts/shutdown-check.sh— graceful shutdown aroundrun_from_args, including the SIGPIPE handler and dual flushscripts/tracing-check.sh— logging and rotation rules;--inventory-onlyruns the static half alonescripts/multiplatform-check.sh— rejects shelling out towhich/where, which must go throughplatform::which_binscripts/natives-check.sh— native crate allowlist; forbids human CLIs undersrc/outside domain binaries (chrome, lightpanda, lighthouse, ffmpeg, redis-server in tests), and forbidsopenssl(TLS stays rustls-only) andnasm-rsinCargo.lockscripts/filesize-check.sh— 300 CODE-line ceiling per production file; rustdoc prose is not code, so counting physical lines would reward deleting documentation; declared exceptions carry an expiry version and fail like any other file past itscripts/orphan-module-check.sh— everysrc/**/*.rsmust be reachable from a crate root; a file no parent declares withmodis absent from the binary while build, clippy and the whole suite stay greenscripts/reachability-check.sh—pub useitems with no call site undersrc/; thedead_codelint stops at the crate boundary, so a re-exported item nobody calls stays silentscripts/split-conservation-audit.sh— takes<original.rs> <new_dir>pairs and asserts every significant line of a pre-split file still exists under the directory that replaced it; splittingcommands/ops/lighthouse.rssilently dropped apub(crate)doc comment while build, clippy,cargo doc -D warningsand the whole suite stayed green, becausemissing_docsfires on ABSENT documentation of a public item and is blind to documentation that was DELETED from an item it does not cover
scripts/clap-schema-parity-check.sh— compares the clap PARSER against the published schema, the axisschema-drift-check.shstructurally cannot see because both of its sides derive from the same schema module; measured 29 flags accepted by clap and absent fromschema, including the requiredstorage export --pathscripts/schema-drift-check.sh— thin adapter over the generator's--check; the runtime is the source of truth anddocs/schemas/*.jsonis a derived artifact; the capability existed long before the wiring, and seven schemas had drifted while every audit reported greenscripts/config-roundtrip-check.sh— everyCONFIG_KEYSliteral must exist in the writer and in the reader (see v0.1.8 below)scripts/phantom-flag-gate.sh— bash adapter overtests/phantom_flag_gate.rs(see v0.1.8 below)
scripts/docs-check.sh— local docs.rs validation pipeline:cargo check,cargo doc --no-deps --features docs-mermaid, optional nightly doc and rustdoc JSON phases, then a coverage audit of the feature and metadata; NDJSON progress on stdout and human logs on stderr; exit0all phases passed,65documentation or metadata audit failed,70build failurescripts/i18n-check.sh—locales/en.ftlandlocales/pt-BR.ftlmessage-id parity plus the i18n unit surfacescripts/doc-coverage-check.sh— prose against the live binary surface; see the Documentation Coverage Gate section above
scripts/perf-check.sh— performance hygiene: profile inventory plus a release build smoke;--inventory-onlyskips the rebuild,--rsschains the RSS baseline,--benchruns the slowcli_parsebenchscripts/latency-check.sh— latency hygiene gates;--baselineadds wall-clock numbers,--inventory-onlykeeps it staticscripts/latency-baseline.sh— measurement tool, not a gate; reports P50, P99, P999, P9999 and max over agent meta paths so Chrome boot never masks a Rust regression; keeps outliers and always exits0after printingscripts/rss-baseline.sh— measurement tool; readsMaximum resident set sizefrom/usr/bin/time -vfor the release binaryscripts/profile-cdp.sh— local profiling of a one-shotgoto about:blank; wall time by default, with optional flamegraph or samply; no committed profiles
scripts/gen-completions.sh— freezes shell completions intotarget/completions/for distro packaging; at runtime completions still come frombrowser-automation-cli completions <shell>scripts/gen-llms-txt.sh— regenerates the machine inventory block ofllms.txtfrom livecommands --json, replacing the previous generated block and leaving the prose alonescripts/gen-flag-reconciliation.sh— reconciles the flags the PRD declares global against where the capability actually lives, classifying each asglobal,local,xdgorabsent(GAP-023);xdgis deliberately NOT a synonym forglobal, because an XDG key is per-host and cannot be varied per invocation, so folding the two would report a gap as closed while the per-invocation control stays missing- Neither script asserts anything, so neither belongs in the
scripts/*-check.shglob
scripts/config-roundtrip-check.sh— asserts every key literal inCONFIG_KEYSis present in the writersrc/xdg/config_write.rsand in the readersrc/xdg/config_io.rs; promoted policy keys are exempt because one macro table generates them- The class it closes:
config setrebuilds the wholeconfig.tomlfrom a hand-written template, so a key missing from it is dropped rather than left alone, and the reader's hand-writtenmatchdiscards it on load - The failure was silent in the worst way:
config setansweredok: truewith exit0and the next process read backnull - Measured 2026-08-09: seventeen keys missing from both halves
- Measured 2026-08-10: six more still missing —
proxy_url,proxy_bypass,proxy_username,proxy_password,stealth_seed,robots_user_agent - Two of those six are proxy credentials the documentation tells the operator to keep in configuration precisely because argv is visible in the process table; the safe channel discarded the value and left the leaky one as the only one that worked
- Why not a Rust test: a round-trip test needs a validation-passing value per key and degenerates into a hardcoded sample, which is exactly how
tests/v018_parity_gate.rsmissed the six; the invariant is a static set comparison and belongs in a static check scripts/phantom-flag-gate.sh— adapter that exposes one property oftests/phantom_flag_gate.rsto the controls runner, which drives every control withbash $script; it holds no assertion of its own and takes a libtest filter argument- It is deliberately NOT named
*-check.sh: that glob would run the same properties a second time, and a second green adds no information while costing a full test-binary link - The original scan was a 411-line Python script; it was ported to Rust and deleted, because the product is Rust end to end
scripts/natives-check.sh— pins the native crate allowlist and forbidsopensslandnasm-rs; the first would break the rustls-only TLS law, the second would make a system NASM a build requirement- A new native dependency must be added to
SYS_ALLOWLISTwith a justification or removed scripts/doc-coverage-check.sh— reads the live binary and fails when prose drifts from the live key surface or the live command surface; see its own section above for the seven assertions
- These four gates live under
tests/and no document underdocs/described them until now - Each one compares a claim written in prose against the surface the live binary publishes
- Run it with
cargo test --test doc_binary_numeral_gate --locked - A configuration key count written in prose is a claim the binary settles, and nothing was asking it
- Measured 2026-09-04: seventeen points of the published documentation claimed a key count of 206 while
config list-keysanswered 215, with every gate green scripts/doc-coverage-check.shvalidates COVERAGE, that every live key appears indocs/CONFIGURATION.md, and never reads the NUMERAL in the prose beside it- Covering the items and claiming how many items exist are two different assertions, and only one had an owner
- Run it with
cargo test --test root_doc_xdg_coverage_gate --locked - The root
llms-fulldocuments publish the XDG surface twice, once as a NUMERAL and once as an ENUMERATION, and nothing checked that the two halves of the same sentence agree with each other and with the binary - Measured 2026-09-04:
llms-full.txtannounced a family-grouped surface of 217 and then enumerated only 205 under it, andllms-full.pt-BR.txtcarried the identical defect in Portuguese - The numeral had been updated when the surface grew, and the list under it had not
- Run it with
cargo test --test bilingual_flag_parity_gate --locked - A translated pair that enumerates flags MUST enumerate the SAME flags
- The global flag
--mitm-wswas listed inREADME.mdand absent fromREADME.pt-BR.md scripts/audit_bilingual_docs.shprintedSummary: ok=18 fail=0before and after the fix, because it compares CLI INVOCATIONS between the two halves and never looks at a flag enumerated in a prose bullet- The general form is that a gate verifying the PRESENCE of an item never sees the absence of that item on the other side
- Run it with
cargo test --test schema_input_drift_gate --locked - Every
docs/schemas/<cmd>.schema.jsonMUST describe the same INPUT surface the live binary publishes inschema --cmd <cmd> - The capability already existed as
scripts/generate_command_schemas.sh --check, reached byscripts/schema-drift-check.sh - A shell script is not executed by
cargo test, so nothing in the common loop compared the two sides and the files aged in silence docs/schemas/config.schema.jsonlisted neitheruser_data_dirnorinput_typo_permille, the two keys the most recent version added
- The four were born of the same pattern: a gate that verifies the presence of one item never sees the absence of another, and a gate that reads a number never reads the list beside it
- Product logging in the CLI under test:
--verbose/--debug/-qor XDGconfig set log_level - Color defaults via
config set color - Host-specific Chrome path overrides via
config set chrome_pathwhen discovery needs it - Resolve XDG layout with
config path --json
- Doctor fails on chrome: install Chromium or Google Chrome first, or set
config set chrome_path - Timeouts in goto smoke: raise process timeout or inspect network policy
- Schema gate failures: update both code and
docs/schemas/in the same change - Command schema drift: re-run
bash scripts/generate_command_schemas.shafter changingmeta.rs - Bilingual fence drift: re-run
bash scripts/audit_bilingual_docs.shand align EN and.pt-BRcommand blocks - Inventory drift: refresh against
commands --json(71) - Residual disk leaks: re-run
cargo test --test residual_one_shotandbash scripts/residual-check.sh; inspect doctorresidual - Run inventory drift: refresh
RUN_DISPATCHED_CMDSand re-runcargo test --test parity_run_inventory - Clap assert failures: fix
GlobalOpts/ subcommand definitions then re-runcargo test --test clap_command_debug_assert - E2E script missing binary: run
cargo build --release --lockedfirst sotarget/release/browser-automation-cliexists - Lighthouse path missing: pass
--lighthouse-path ./scripts/mock-lighthouse.shor set XDGlighthouse_path - LLM extract fail-closed: expected without
config set openrouter_api_key - MITM bind issues: ensure local loopback is free and review
mitm status --json - Workflow journal confusion: inspect
workflow statusand XDGworkflow_dirfromconfig path --json