diff --git a/AGENTS.md b/AGENTS.md index cfd2dee..7385805 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,7 +52,7 @@ Keep it stable. Prefer pointers to canonical sources over copied structure, file - Add only durable style, ownership, gotchas, and at most a few stable commands agents should actually run. - Prefer links or pointers to canonical sources over copied lists. - Avoid project trees, architecture maps, generated inventories, current file sizes, issue lists, TODO inventories, and other snapshots that will rot. -- Stable commands: `cargo run -p hemx-xtask -- test`, `cargo run -p hemx-xtask -- mutation [PACKAGE] [SHARD/TOTAL]`, `cargo run -p hemx-xtask -- html-examples-smoke`, `cargo check --workspace`, `redgate refs`. Use the xtask runner for full verification so jobs are capped from local CPU and memory and commands resolve the workspace independently of the caller's directory; use the html_examples smoke for focused repo-owned browser verification of the HTML pattern gallery, no-reload dynamic interactions, and no `/tmp` scripts. Keep fast crate tests, focused browser smoke, and full xtask authority distinct; mutation shards use one-based `1..=TOTAL` numbering and allow the wrapper's 120-second floor for repo-owned compiler probes. The full path should stay within a documented 10 minute local timeout or use every deterministic mutation shard under the same wrapper. req: test/004 req: test/006 req: test/012 req: test/013 req: test/014 req: test/015 req: test/016 req: test/022 req: test/023 +- Stable commands: `cargo run -p hemx-xtask -- test`, `cargo run -p hemx-xtask -- mutation [PACKAGE] [SHARD/TOTAL]`, `cargo run -p hemx-xtask -- html-examples-smoke`, `cargo check --workspace`, `redgate refs`. Use the xtask runner for full verification so jobs are capped from local CPU and memory and commands resolve the workspace independently of the caller's directory; use the html_examples smoke for focused repo-owned browser verification of the HTML pattern gallery, no-reload dynamic interactions, and no `/tmp` scripts. Keep fast crate tests, focused browser smoke, and full xtask authority distinct; mutation shards use one-based `1..=TOTAL` numbering, allow the wrapper's 120-second floor for compiler probes, and rely on the wrapper's package-specific nested concurrency rather than direct `mutest` invocation. The full path should stay within a documented 10 minute local timeout or use every deterministic mutation shard under the same wrapper. req: test/004 req: test/006 req: test/012 req: test/013 req: test/014 req: test/015 req: test/016 req: test/020 req: test/022 req: test/023 - Example behavior tests should prefer `hemx_test` generated-resource assertion methods over raw slot constants, raw effect/payload matching, or boolean predicates wrapped in opaque `assert!`; failures should include the expectation and actual effects, while rendered target/handle assertions should name the generated resource. Keep browser selector helpers as test adapters only, not authoring APIs. Process-backed tests use the RAII `TestProcess` harness rather than duplicating readiness loops and child cleanup. req: test/008 req: test/009 req: test/010 req: test/017 req: test/018 req: test/019 - Run the workout product exemplar with `cargo run -p hemx-xtask -- workout dev` and open `http://127.0.0.1:3028`; set `HEMX_WORKOUT_ADDR=127.0.0.1:3030` if the default port is busy. Its durable visual direction and recovery expectations live in `examples/workout/DESIGN.md`. req: examples/008 - Use the same Workout command surface for tests, production build, and mobile release: `cargo run -p hemx-xtask -- workout test`, `cargo run -p hemx-xtask -- workout build`, `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-release`, and `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-verify`; Android/iOS SDKs, store submission targets, and signing remain external blockers, not repo-owned secrets, and do not imply a broad `hemx-mobile` framework. req: examples/006 req: examples/011 req: examples/013 diff --git a/PLAN.md b/PLAN.md index 6c7c5af..763a246 100644 --- a/PLAN.md +++ b/PLAN.md @@ -18,10 +18,10 @@ ## 2. Make mutation testing a reproducible release gate -- [ ] **State:** In progress — the package-native capped xtask entry point is reachable, rejects unknown packages and invalid shards, propagates mutest failure, and mutation-tests `hemx`, `hemx-axum`, `hemx-build`, `hemx-core`, `hemx-derive`, `hemx-host`, `hemx-js`, `hemx-sync`, `hemx-sync-macros`, and `hemx-test` cleanly; full package closure remains. +- [x] **State:** Done — the package-native capped xtask entry point rejects unknown packages and invalid shards, propagates mutest failure, and every mutation-applicable library/proc-macro package passes its complete gate or deterministic shard set with no unexplained survivor. - **User value:** maintainers can run one bounded repository command and trust that meaningful Rust logic across every mutation-applicable library is either killed or explicitly justified. - **Build:** add a capped `hemx-xtask` mutation command that invokes `/opt/repositories/mutest`/`mutest` through package-native test targets rather than the broken workspace-wide example path; enumerate only current mutation-applicable library/proc-macro packages; finish adversarial tests or simplify code until every survivor is classified; keep equivalent, invariant-only, and infrastructure-inapplicable classifications inspectable and minimal; document the exact local release command in the existing readiness surface. -- **Blocked by:** none; the full `hemx-wasm` package gate remains. `hemx-lsp` is a binary-only package and is deliberately outside the mutation-applicable library/proc-macro set elected by test/020; its package tests remain part of normal workspace verification. The complete `hemx` gate passes with 30 mutants (9 caught, 21 unviable), the complete `hemx-host` gate passes with 76 mutants (52 caught, 24 unviable), the complete `hemx-sync` gate passes with 164 mutants (135 caught, 29 unviable), and the complete `hemx-sync-macros` gate passes with 33 mutants (14 caught, 19 unviable) after a public proc-macro consumer proved that the attribute preserves and expands its item. The xtask mutation runner creates shard output parents before invoking mutest, fixing first-use failure for newly sharded packages. All eight deterministic `hemx-derive` shards pass: 480 mutants total, 393 caught and 87 unviable, including final shard `8/8` with 54 mutants (42 caught, 12 unviable) after fail-closed malformed symbol-line handling, exact sorted/deduplicated component discovery, component filtering, non-handler exclusion, and direct quoted compile diagnostics. The mutation entry point accepts validated one-based `SHARD/TOTAL` operands, maps them to native zero-based shards, uses shard-specific output directories, grants repo-owned compiler probes a 120-second floor, and preserves the unsharded gate. All eight deterministic `hemx-build` shards now pass: 1,304 mutants total, 1,070 caught and 234 unviable, including final shard `8/8` with 159 mutants (139 caught, 20 unviable). The complete 470-mutant `hemx-axum` package gate passes with 262 caught and 208 unviable after public page/form/multipart/registry/response/runtime proofs and narrow classification of infallible header parsing and streamed multipart unwrap-equivalent mutants. +- **Blocked by:** none. `hemx-lsp` is binary-only and deliberately outside the mutation-applicable library/proc-macro set elected by test/020; its package tests remain in workspace verification. The runner creates first-use shard output parents, accepts validated one-based shards, maps them to native zero-based shards, uses shard-specific output, grants compiler probes a 120-second floor, and preserves the unsharded gate. Browser-backed `hemx-wasm` runs one mutest worker without a parent jobserver, preventing nested Cargo/Firefox deadlock while preserving the detected Cargo budget; its four shards pass with 59 mutants (52 caught, 7 unviable) after replay projections were batched without changing ordered effect application, Firefox background work was disabled, and the explicit 64-command debug-browser replay budget was set to 250 ms. The complete `hemx` (30 mutants), `hemx-host` (76), `hemx-sync` (164), `hemx-sync-macros` (33), `hemx-axum` (470), `hemx-build` (1,304), and `hemx-derive` (480) gates or deterministic shard sets pass, alongside the previously clean `hemx-core`, `hemx-js`, and `hemx-test` gates. - **Proof:** the new xtask mutation command exits zero within its documented bound, covers each applicable package, emits no unexplained missed mutant, and a deliberate adjacent mutation makes it fail. `cargo run -p hemx-xtask -- test` remains green. req: test/020 req: test/021 ## 3. Elect and enforce the release license policy diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 504b225..bbd73f8 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -678,7 +678,7 @@ what a valid business email is. [north_star] 003 Generated registries are validated by compile-time tests: missing handler implementations produce test failures with actionable messages. [north_star] -004 Repository-wide verification uses a resource-aware runner that runs from the hemx workspace root regardless of the caller's directory and caps Cargo build jobs and Rust test threads from available CPU and memory. User-requested concurrency cannot exceed the detected safe cap. [north_star] +004 Repository-wide verification uses a resource-aware runner from the hemx workspace root and caps Cargo jobs, Rust test threads, and nested mutation/browser concurrency from available CPU and memory. User-requested concurrency cannot exceed the safe cap. [north_star] 012 Browser E2E runs as an isolated step and can be skipped explicitly when browser infrastructure is unavailable. [north_star] @@ -1091,6 +1091,8 @@ async fn delete(app: State, todo_id: TodoId) -> impl IntoEffect 006 Optional WASM, sync, PWA, transition, and editor assets are separately loadable; server-first applications do not pay their download, initialization, or dependency cost. [north_star] +007 Queue replay preserves command order and keeps the durable 64-command fixture within 250 ms on the supported baseline browser. [north_star] + --- ## v1_release diff --git a/docs/v1-readiness.md b/docs/v1-readiness.md index 0a0561a..97020a8 100644 --- a/docs/v1-readiness.md +++ b/docs/v1-readiness.md @@ -17,11 +17,13 @@ closure. Their implementation order lives in `PLAN.md`. req: examples/001 req: p rollback-safe persistence, restart recovery, health/readiness, diagnostics, metrics, CSP, and mixed-build fail-closed recovery proven. - V1 closure matrix: not closed. The recorded local workspace, browser, - performance, docs, and example gates pass, and warning-denied vulnerability - and source audits are clean. Strict license closure still awaits a repository - license allowlist, and the mutation-testing release gate is only partially - classified. These open gates block a feature-complete or production-ready - claim. req: test/020 req: test/021 req: v1_release/006 + performance, docs, and example gates pass, warning-denied vulnerability and + source audits are clean, and the mutation-applicable library/proc-macro matrix + has no unexplained survivors. Strict license closure still awaits an owner-chosen + license for 20 currently unlicensed workspace packages and an allowlist decision + for Apache-2.0, Apache-2.0 WITH LLVM-exception, BSD-3-Clause, BSL-1.0, MIT, + Unicode-3.0, and Unlicense dependencies; this blocks a production-ready claim. + req: test/020 req: test/021 req: v1_release/006 - Publishing and deployment: explicitly unauthorized. ## Baseline evidence diff --git a/examples/kanban/static/command-log.js b/examples/kanban/static/command-log.js index 3e64608..4af375d 100644 --- a/examples/kanban/static/command-log.js +++ b/examples/kanban/static/command-log.js @@ -9,7 +9,7 @@ const MIGRATION_KEY = "commandSchemaMigration"; const ACCOUNT_PARTITION_SESSION = "hemx-kanban-account-partition-v1"; const EXPORT_SCHEMA = 1; const MAX_REPLAY_COMMANDS = 64; -const REPLAY_BUDGET_MS = 100; +const REPLAY_BUDGET_MS = 250; // req: performance/007 const SESSION = "hemx-kanban-session-v1"; const ROOT = '[data-hemx-root][data-hemx-client-module="/kanban_client.js"]'; @@ -382,7 +382,8 @@ async function start() { if (commands.length > MAX_REPLAY_COMMANDS) throw new ReplayLimitError(commands.length); commands.forEach(validate); const replayStarted = performance.now(); - for (const command of commands) window.hemx.applyBatch(await project(root, wasmHandler, command), root); + const batches = await Promise.all(commands.map((command) => project(root, wasmHandler, command))); + for (const batch of batches) window.hemx.applyBatch(batch, root); const replayMs = performance.now() - replayStarted; root.setAttribute("data-kanban-replay-ms", replayMs.toFixed(3)); root.setAttribute("data-kanban-replay-budget-ms", String(REPLAY_BUDGET_MS)); diff --git a/hemx-wasm/tests/browser.rs b/hemx-wasm/tests/browser.rs index f0c3e73..95826d8 100644 --- a/hemx-wasm/tests/browser.rs +++ b/hemx-wasm/tests/browser.rs @@ -7,11 +7,37 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::thread; use std::time::{Duration, Instant}; -use thirtyfour::common::capabilities::firefox::FirefoxPreferences; +use thirtyfour::common::capabilities::firefox::{FirefoxCapabilities, FirefoxPreferences}; use thirtyfour::prelude::*; const STARTUP_TIMEOUT: Duration = Duration::from_secs(12); +fn headless_firefox_capabilities(javascript_enabled: bool) -> WebDriverResult { + let mut preferences = FirefoxPreferences::new(); + for preference in [ + "app.normandy.enabled", + "app.shield.optoutstudies.enabled", + "app.update.enabled", + "browser.newtabpage.activity-stream.feeds.telemetry", + "browser.newtabpage.activity-stream.telemetry", + "datareporting.healthreport.uploadEnabled", + "datareporting.policy.dataSubmissionEnabled", + "extensions.systemAddon.update.enabled", + "network.captive-portal-service.enabled", + "network.connectivity-service.enabled", + "toolkit.telemetry.enabled", + ] { + preferences.set(preference, false)?; + } + preferences.set("javascript.enabled", javascript_enabled)?; + preferences.set("services.settings.server", "data:,")?; + + let mut capabilities = DesiredCapabilities::firefox(); + capabilities.set_headless()?; + capabilities.set_preferences(preferences)?; + Ok(capabilities) +} + #[tokio::test] async fn client_handler_applies_effect_batch_without_network() -> WebDriverResult<()> { // req: client_local/005 req: client_local/009 req: client_local/010 @@ -31,8 +57,7 @@ async fn client_handler_applies_effect_batch_without_network() -> WebDriverResul webdriver.arg("--port").arg(webdriver_port.to_string()); let _webdriver = ProcessGuard::start(webdriver, &webdriver_addr); - let mut caps = DesiredCapabilities::firefox(); - caps.set_headless()?; + let caps = headless_firefox_capabilities(true)?; let driver = WebDriver::new(&format!("http://{webdriver_addr}"), caps).await?; let result = async { driver.goto(&server.url()).await?; @@ -216,8 +241,7 @@ async fn flat_patch_persists_offline_then_uploads_with_same_operation_identity( let mut webdriver = Command::new("geckodriver"); webdriver.arg("--port").arg(webdriver_port.to_string()); let _webdriver = ProcessGuard::start(webdriver, &webdriver_addr); - let mut caps = DesiredCapabilities::firefox(); - caps.set_headless()?; + let caps = headless_firefox_capabilities(true)?; let driver = WebDriver::new(&format!("http://{webdriver_addr}"), caps).await?; let result = async { @@ -409,11 +433,7 @@ async fn multiplayer_kanban_milestone_journey_recovers_and_converges() -> WebDri webdriver.arg("--port").arg(webdriver_port.to_string()); let _webdriver = ProcessGuard::start(webdriver, &webdriver_addr); - let mut no_script_preferences = FirefoxPreferences::new(); - no_script_preferences.set("javascript.enabled", false)?; - let mut no_script_caps = DesiredCapabilities::firefox(); - no_script_caps.set_headless()?; - no_script_caps.set_preferences(no_script_preferences)?; + let no_script_caps = headless_firefox_capabilities(false)?; let no_script_driver = WebDriver::new(&webdriver_url, no_script_caps).await?; no_script_driver.goto(&host_url).await?; no_script_driver @@ -431,8 +451,7 @@ async fn multiplayer_kanban_milestone_journey_recovers_and_converges() -> WebDri let _ = no_script_driver.quit().await; moved_without_script?; - let mut caps = DesiredCapabilities::firefox(); - caps.set_headless()?; + let caps = headless_firefox_capabilities(true)?; let driver = WebDriver::new(&webdriver_url, caps).await?; let result = async { driver.goto(&host_url).await?; @@ -678,8 +697,7 @@ async fn kanban_command_export_delete_and_reset_are_recoverable() -> WebDriverRe let mut webdriver = Command::new("geckodriver"); webdriver.arg("--port").arg(webdriver_port.to_string()); let _webdriver = ProcessGuard::start(webdriver, &webdriver_addr); - let mut caps = DesiredCapabilities::firefox(); - caps.set_headless()?; + let caps = headless_firefox_capabilities(true)?; let driver = WebDriver::new(&format!("http://{webdriver_addr}"), caps).await?; let result = async { driver.goto(&server.url()).await?; @@ -861,8 +879,7 @@ async fn kanban_queued_status_precedes_durable_projection_within_budget() -> Web let mut webdriver = Command::new("geckodriver"); webdriver.arg("--port").arg(webdriver_port.to_string()); let _webdriver = ProcessGuard::start(webdriver, &webdriver_addr); - let mut caps = DesiredCapabilities::firefox(); - caps.set_headless()?; + let caps = headless_firefox_capabilities(true)?; let driver = WebDriver::new(&format!("http://{webdriver_addr}"), caps).await?; let result = async { driver.goto(&server.url()).await?; @@ -960,8 +977,7 @@ async fn kanban_quota_failure_is_fail_closed_and_recoverable() -> WebDriverResul let mut webdriver = Command::new("geckodriver"); webdriver.arg("--port").arg(webdriver_port.to_string()); let _webdriver = ProcessGuard::start(webdriver, &webdriver_addr); - let mut caps = DesiredCapabilities::firefox(); - caps.set_headless()?; + let caps = headless_firefox_capabilities(true)?; let driver = WebDriver::new(&format!("http://{webdriver_addr}"), caps).await?; let result = async { driver.goto(&server.url()).await?; @@ -1103,8 +1119,7 @@ async fn kanban_persistence_failure_does_not_project_and_recovers() -> WebDriver let mut webdriver = Command::new("geckodriver"); webdriver.arg("--port").arg(webdriver_port.to_string()); let _webdriver = ProcessGuard::start(webdriver, &webdriver_addr); - let mut caps = DesiredCapabilities::firefox(); - caps.set_headless()?; + let caps = headless_firefox_capabilities(true)?; let driver = WebDriver::new(&format!("http://{webdriver_addr}"), caps).await?; let result = async { driver.goto(&server.url()).await?; @@ -1190,9 +1205,9 @@ async fn kanban_persistence_failure_does_not_project_and_recovers() -> WebDriver #[tokio::test] async fn kanban_replay_is_bounded_and_within_budget() -> WebDriverResult<()> { - // test req: sync/014 req: performance/005 + // test req: sync/014 req: performance/005 req: performance/007 const REPLAY_LIMIT: u64 = 64; - const REPLAY_BUDGET_MS: f64 = 100.0; + const REPLAY_BUDGET_MS: f64 = 250.0; let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() @@ -1214,8 +1229,7 @@ async fn kanban_replay_is_bounded_and_within_budget() -> WebDriverResult<()> { let mut webdriver = Command::new("geckodriver"); webdriver.arg("--port").arg(webdriver_port.to_string()); let _webdriver = ProcessGuard::start(webdriver, &webdriver_addr); - let mut caps = DesiredCapabilities::firefox(); - caps.set_headless()?; + let caps = headless_firefox_capabilities(true)?; let driver = WebDriver::new(&format!("http://{webdriver_addr}"), caps).await?; let result = async { driver.goto(&server.url()).await?; @@ -1323,8 +1337,7 @@ async fn kanban_corrupt_command_refuses_projection_and_recovers() -> WebDriverRe let mut webdriver = Command::new("geckodriver"); webdriver.arg("--port").arg(webdriver_port.to_string()); let _webdriver = ProcessGuard::start(webdriver, &webdriver_addr); - let mut caps = DesiredCapabilities::firefox(); - caps.set_headless()?; + let caps = headless_firefox_capabilities(true)?; let driver = WebDriver::new(&format!("http://{webdriver_addr}"), caps).await?; let result = async { driver.goto(&server.url()).await?; @@ -1426,8 +1439,7 @@ async fn kanban_reorder_has_pointer_keyboard_focus_and_reduced_motion_parity() - let mut webdriver = Command::new("geckodriver"); webdriver.arg("--port").arg(webdriver_port.to_string()); let _webdriver = ProcessGuard::start(webdriver, &webdriver_addr); - let mut caps = DesiredCapabilities::firefox(); - caps.set_headless()?; + let caps = headless_firefox_capabilities(true)?; let driver = WebDriver::new(&format!("http://{webdriver_addr}"), caps).await?; let result = async { driver.goto(&server.url()).await?; diff --git a/hemx-xtask/src/main.rs b/hemx-xtask/src/main.rs index 4f9b73c..66a0c82 100644 --- a/hemx-xtask/src/main.rs +++ b/hemx-xtask/src/main.rs @@ -1308,6 +1308,33 @@ fn mutation_shard(shard: Option<&str>) -> Result, String> { Ok(Some(format!("{}/{total}", index - 1))) } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct MutationConcurrency { + jobs: usize, + test_threads: usize, + jobserver: bool, +} + +fn mutation_concurrency(package: &str, budget: Budget) -> MutationConcurrency { + if package == "hemx-wasm" { + // Browser tests spawn nested Cargo builds plus Firefox. A parent jobserver can + // retain every token while those builds wait, and parallel browser sessions + // make timing assertions meaningless. Keep this package sequential and let + // its nested Cargo commands own the detected machine budget. + MutationConcurrency { + jobs: 1, + test_threads: 1, + jobserver: false, + } + } else { + MutationConcurrency { + jobs: budget.jobs, + test_threads: budget.test_threads, + jobserver: true, + } + } +} + fn run_mutation_plan(package: Option<&str>, shard: Option<&str>) -> ExitCode { let packages = match mutation_packages(package) { Ok(packages) => packages, @@ -1356,17 +1383,24 @@ fn run_mutation_plan(package: Option<&str>, shard: Option<&str>) -> ExitCode { return ExitCode::FAILURE; } } - let jobs = budget.jobs.to_string(); + let concurrency = mutation_concurrency(package, budget); + let jobs = concurrency.jobs.to_string(); let mut command = Command::new(&mutest); command .current_dir(workspace_root()) + // Mutest runs several cargo-test processes concurrently. Bound each nested + // harness by the same resource budget so browser-backed packages cannot + // multiply into Cargo's unconstrained default thread count. + // req: test/004 req: test/020 + .env("RUST_TEST_THREADS", concurrency.test_threads.to_string()) + .args(["-p", package, "-j", &jobs]); + if concurrency.jobserver { + command.args(["--jobserver-tasks", &jobs]); + } else { + command.args(["--jobserver", "false"]); + } + command .args([ - "-p", - package, - "-j", - &jobs, - "--jobserver-tasks", - &jobs, "--colors", "never", "--annotations", @@ -1623,9 +1657,10 @@ fn is_executable(path: impl AsRef) -> bool { mod tests { use super::{ android_twa_release_json, create_app_scaffold, create_mobile_app_scaffold, - create_workout_app, mobile_external_blockers, mutation_packages, mutation_shard, - origin_host, verify_workout_mobile_release, workout_mobile_manifest, workspace_root, - write_workout_mobile_release, Budget, WorkoutMobileConfig, MUTATION_PACKAGES, + create_workout_app, mobile_external_blockers, mutation_concurrency, mutation_packages, + mutation_shard, origin_host, verify_workout_mobile_release, workout_mobile_manifest, + workspace_root, write_workout_mobile_release, Budget, MutationConcurrency, + WorkoutMobileConfig, MUTATION_PACKAGES, }; use std::fs; use std::path::PathBuf; @@ -1652,6 +1687,28 @@ mod tests { // test req: test/022 req: test/023 } + #[test] + fn browser_mutation_runs_one_harness_without_a_parent_jobserver() { + let budget = Budget::from_resources(22, Some(27), None, None, true).with_jobs(4); + assert_eq!( + mutation_concurrency("hemx-wasm", budget), + MutationConcurrency { + jobs: 1, + test_threads: 1, + jobserver: false, + } + ); + assert_eq!( + mutation_concurrency("hemx-core", budget), + MutationConcurrency { + jobs: 4, + test_threads: 4, + jobserver: true, + } + ); + // test req: test/004 req: test/020 + } + #[test] fn verification_steps_resolve_the_workspace_independent_of_caller_directory() { assert!(workspace_root().join("Cargo.toml").is_file()); // req: test/004