From 157b11f5bfe69de5a78df1cb5e0cae425b6bc5cd Mon Sep 17 00:00:00 2001 From: leynos Date: Thu, 20 Aug 2026 21:33:04 +0200 Subject: [PATCH 01/34] Document Locize terminology and translation style Define a glossary schema and a six-field base style guide that map to Locize while preserving Netsuke's established localization contracts. Index both documents beside the translator guide so maintainers can keep the local source of truth synchronized with the hosted project. --- docs/locize-glossary.md | 51 ++++++++++++++++++++++ docs/locize-styleguide.md | 89 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 docs/locize-glossary.md create mode 100644 docs/locize-styleguide.md diff --git a/docs/locize-glossary.md b/docs/locize-glossary.md new file mode 100644 index 000000000..342cf9d3e --- /dev/null +++ b/docs/locize-glossary.md @@ -0,0 +1,51 @@ +# Netsuke Locize glossary + +This document is the local source of truth for Netsuke terminology in Locize. +It follows the preferred, allowed, and forbidden term model described in the +[Locize glossary documentation][^1] and complements the +[translator guide](translators-guide.md). + +## Schema + +Each row in Table 1 represents one Locize term for one language. The first five +columns are the complete publishable record. A semicolon separates multiple +values in `allowed` or `forbidden`; an em dash means that the set is empty. + +Netsuke's source catalogue is `en-US`, but the Locize project currently uses +`en` as its reference language. The glossary therefore uses `en` until that +project setting is corrected. The terms themselves follow the `en-US` source +catalogue and repository documentation. + +Table 1: Locize glossary records for Netsuke + +| title | language | preferred | allowed | forbidden | +| ------------------- | -------- | ------------------- | -------------------------------------------------- | ---------------------------------------- | +| Netsuke | en | `Netsuke` | `netsuke` | — | +| Netsukefile | en | `Netsukefile` | `Netsuke manifest`; `manifest file` | `NetsukeFile`; `Netsuke file` | +| Ninja | en | `Ninja` | `ninja` | — | +| Fluent | en | `Fluent` | `Project Fluent`; `Fluent Translation List`; `FTL` | — | +| manifest | en | `manifest` | `Netsuke manifest`; `manifest file` | `configuration file` | +| target | en | `target` | `build target`; `default target` | `rule` | +| action | en | `action` | `build action`; `action catalogue` | `rule` | +| dependency | en | `dependency` | `direct dependency`; `order-only dependency` | `dependancy` | +| dependencies | en | `dependencies` | `direct dependencies`; `dependency graph` | `dependancies` | +| standard library | en | `standard library` | `template standard library`; `stdlib` | — | +| build graph | en | `build graph` | `dependency graph`; `graph` | — | +| `command_available` | en | `command_available` | `command_available(name, **kwargs)` | `command-available`; `command available` | + +## Usage notes + +- Use `Netsuke` for the product. Use lowercase `netsuke` only for the + executable, package, crate, command, or code path. +- Use `Netsukefile` for the conventional manifest filename. Use `manifest` for + the parsed YAML document or for manifests with another filename. +- Use `Ninja` for the build system and lowercase `ninja` only for its executable + or a literal command. +- A target is a build output or named build entry. An action is an implicitly + phony target. Neither is interchangeable with a Ninja rule. +- Preserve `command_available` exactly because it is a Jinja helper identifier. +- Keep forbidden values conservative. Add a value only for an observed + misspelling, an identifier variant that would break a literal interface, or a + collision between distinct Netsuke concepts. + +[^1]: [Locize glossary documentation](https://www.locize.com/docs/glossary) diff --git a/docs/locize-styleguide.md b/docs/locize-styleguide.md new file mode 100644 index 000000000..eba64a8ee --- /dev/null +++ b/docs/locize-styleguide.md @@ -0,0 +1,89 @@ +# Netsuke Locize style guide + +This document is the local source of truth for Netsuke's base Locize style +guide. Its six sections correspond exactly to the fields defined by the +[Locize styleguide documentation][^1]. The base applies to every language; +language-specific overrides should contain only rules that differ from it. + +## Tone of voice + +Clear, calm, capable, and respectful. Netsuke is human-first and +agent-consistent: messages help both people and automation understand what +happened and what to do next. Diagnostics are concise and actionable. They +state the condition, retain the relevant path, field, command, or value, and +give a safe correction when the source provides one. Avoid blame, drama, +filler, marketing language, and unsupported claims. + +## Level of formality + +Professional plain language with moderate formality. Prefer short sentences and +direct instructions. Avoid slang, jokes, idioms, culturally specific metaphors, +and needlessly ceremonial phrasing. Use the natural professional register of +the target locale rather than translating English formality markers literally. + +## Target audience + +Developers who author and run `Netsukefile` manifests, operators who run builds +locally or in Continuous Integration (CI), and automation agents that consume +the command-line interface or JSON diagnostics. Assume technical competence, +but do not assume knowledge of Netsuke internals that the source message does +not explain. + +## Usage rules (do's and don'ts) + +Do: + +- Keep messages concise, concrete, and useful at the point of action. +- Translate explanatory prose naturally while preserving its meaning and + diagnostic order. +- Preserve code, paths, identifiers, option names, command fragments, message + keys, and Fluent placeables exactly. This includes `{ $path }`, `--locale`, + and `command_available`. +- Keep the distinctions between manifests, targets, actions, rules, + dependencies, graphs, generated Ninja manifests, and executed builds. +- Follow the project glossary for product names and technical terminology. + +Don't: + +- Do not translate product, library, API, Rust path, command, option, manifest + key, or Jinja identifier names. +- Do not add, remove, rename, or change the type of a Fluent placeable. +- Do not turn an actionable diagnostic into a vague status message or invent a + prompt, warning, remediation, or guarantee absent from the source. +- Do not replace a precise technical term with a near-synonym when Netsuke + distinguishes the concepts. +- Do not add humour, idioms, gender assumptions, or culturally specific + metaphors. + +## Localization rules + +- Treat `locales/en-US/messages.ftl` as the source catalogue. Preserve every + message key and the names and number of its placeables. +- Preserve valid Fluent syntax, attributes, terms, select expressions, and + plural variants. Use the target language's required plural categories. +- Apply the target locale's grammar, punctuation, quotation marks, spacing, + word order, number formats, and conventions for addressing the reader. +- Preserve meaningful BCP 47 variants. In particular, do not collapse + `en-GB` into `en-US`, `es-419` into `es-ES`, `pt-BR` into `pt-PT`, or + `zh-Hans` into `zh-Hant`. +- Preserve punctuation that belongs to code, paths, identifiers, placeables, + or literal commands. +- For right-to-left languages, add the direction mark specified by the + [translator guide](translators-guide.md) when the first strong character of + the rendered message would otherwise have the wrong direction. +- Do not translate locale tags, message keys, or literal values. Do not + localize paths, commands, or machine-readable JSON field names. + +## Misc + +Consult the [Netsuke glossary](locize-glossary.md) for terminology and the +[translator guide](translators-guide.md) for Fluent structure, fallback rules, +variables, plurals, bidirectional text, and catalogue validation. When a source +message is ambiguous, inspect its code context and record the question for +review instead of guessing or silently broadening its meaning. + +The Locize project currently labels `en` as its reference language, whereas +Netsuke's source catalogue is `en-US`. Treat `en-US` as authoritative for +message content until the Locize project setting is corrected. + +[^1]: [Locize styleguide documentation](https://www.locize.com/docs/styleguide) From 03032104f830b953d2285745d7a6f3c2796d0573 Mon Sep 17 00:00:00 2001 From: leynos Date: Thu, 20 Aug 2026 21:37:45 +0200 Subject: [PATCH 02/34] Rename Locize documents for localization scope Use product-independent localization filenames and update the documentation index and cross-references to match. --- docs/locize-glossary.md | 51 ---------------------- docs/locize-styleguide.md | 89 --------------------------------------- 2 files changed, 140 deletions(-) delete mode 100644 docs/locize-glossary.md delete mode 100644 docs/locize-styleguide.md diff --git a/docs/locize-glossary.md b/docs/locize-glossary.md deleted file mode 100644 index 342cf9d3e..000000000 --- a/docs/locize-glossary.md +++ /dev/null @@ -1,51 +0,0 @@ -# Netsuke Locize glossary - -This document is the local source of truth for Netsuke terminology in Locize. -It follows the preferred, allowed, and forbidden term model described in the -[Locize glossary documentation][^1] and complements the -[translator guide](translators-guide.md). - -## Schema - -Each row in Table 1 represents one Locize term for one language. The first five -columns are the complete publishable record. A semicolon separates multiple -values in `allowed` or `forbidden`; an em dash means that the set is empty. - -Netsuke's source catalogue is `en-US`, but the Locize project currently uses -`en` as its reference language. The glossary therefore uses `en` until that -project setting is corrected. The terms themselves follow the `en-US` source -catalogue and repository documentation. - -Table 1: Locize glossary records for Netsuke - -| title | language | preferred | allowed | forbidden | -| ------------------- | -------- | ------------------- | -------------------------------------------------- | ---------------------------------------- | -| Netsuke | en | `Netsuke` | `netsuke` | — | -| Netsukefile | en | `Netsukefile` | `Netsuke manifest`; `manifest file` | `NetsukeFile`; `Netsuke file` | -| Ninja | en | `Ninja` | `ninja` | — | -| Fluent | en | `Fluent` | `Project Fluent`; `Fluent Translation List`; `FTL` | — | -| manifest | en | `manifest` | `Netsuke manifest`; `manifest file` | `configuration file` | -| target | en | `target` | `build target`; `default target` | `rule` | -| action | en | `action` | `build action`; `action catalogue` | `rule` | -| dependency | en | `dependency` | `direct dependency`; `order-only dependency` | `dependancy` | -| dependencies | en | `dependencies` | `direct dependencies`; `dependency graph` | `dependancies` | -| standard library | en | `standard library` | `template standard library`; `stdlib` | — | -| build graph | en | `build graph` | `dependency graph`; `graph` | — | -| `command_available` | en | `command_available` | `command_available(name, **kwargs)` | `command-available`; `command available` | - -## Usage notes - -- Use `Netsuke` for the product. Use lowercase `netsuke` only for the - executable, package, crate, command, or code path. -- Use `Netsukefile` for the conventional manifest filename. Use `manifest` for - the parsed YAML document or for manifests with another filename. -- Use `Ninja` for the build system and lowercase `ninja` only for its executable - or a literal command. -- A target is a build output or named build entry. An action is an implicitly - phony target. Neither is interchangeable with a Ninja rule. -- Preserve `command_available` exactly because it is a Jinja helper identifier. -- Keep forbidden values conservative. Add a value only for an observed - misspelling, an identifier variant that would break a literal interface, or a - collision between distinct Netsuke concepts. - -[^1]: [Locize glossary documentation](https://www.locize.com/docs/glossary) diff --git a/docs/locize-styleguide.md b/docs/locize-styleguide.md deleted file mode 100644 index eba64a8ee..000000000 --- a/docs/locize-styleguide.md +++ /dev/null @@ -1,89 +0,0 @@ -# Netsuke Locize style guide - -This document is the local source of truth for Netsuke's base Locize style -guide. Its six sections correspond exactly to the fields defined by the -[Locize styleguide documentation][^1]. The base applies to every language; -language-specific overrides should contain only rules that differ from it. - -## Tone of voice - -Clear, calm, capable, and respectful. Netsuke is human-first and -agent-consistent: messages help both people and automation understand what -happened and what to do next. Diagnostics are concise and actionable. They -state the condition, retain the relevant path, field, command, or value, and -give a safe correction when the source provides one. Avoid blame, drama, -filler, marketing language, and unsupported claims. - -## Level of formality - -Professional plain language with moderate formality. Prefer short sentences and -direct instructions. Avoid slang, jokes, idioms, culturally specific metaphors, -and needlessly ceremonial phrasing. Use the natural professional register of -the target locale rather than translating English formality markers literally. - -## Target audience - -Developers who author and run `Netsukefile` manifests, operators who run builds -locally or in Continuous Integration (CI), and automation agents that consume -the command-line interface or JSON diagnostics. Assume technical competence, -but do not assume knowledge of Netsuke internals that the source message does -not explain. - -## Usage rules (do's and don'ts) - -Do: - -- Keep messages concise, concrete, and useful at the point of action. -- Translate explanatory prose naturally while preserving its meaning and - diagnostic order. -- Preserve code, paths, identifiers, option names, command fragments, message - keys, and Fluent placeables exactly. This includes `{ $path }`, `--locale`, - and `command_available`. -- Keep the distinctions between manifests, targets, actions, rules, - dependencies, graphs, generated Ninja manifests, and executed builds. -- Follow the project glossary for product names and technical terminology. - -Don't: - -- Do not translate product, library, API, Rust path, command, option, manifest - key, or Jinja identifier names. -- Do not add, remove, rename, or change the type of a Fluent placeable. -- Do not turn an actionable diagnostic into a vague status message or invent a - prompt, warning, remediation, or guarantee absent from the source. -- Do not replace a precise technical term with a near-synonym when Netsuke - distinguishes the concepts. -- Do not add humour, idioms, gender assumptions, or culturally specific - metaphors. - -## Localization rules - -- Treat `locales/en-US/messages.ftl` as the source catalogue. Preserve every - message key and the names and number of its placeables. -- Preserve valid Fluent syntax, attributes, terms, select expressions, and - plural variants. Use the target language's required plural categories. -- Apply the target locale's grammar, punctuation, quotation marks, spacing, - word order, number formats, and conventions for addressing the reader. -- Preserve meaningful BCP 47 variants. In particular, do not collapse - `en-GB` into `en-US`, `es-419` into `es-ES`, `pt-BR` into `pt-PT`, or - `zh-Hans` into `zh-Hant`. -- Preserve punctuation that belongs to code, paths, identifiers, placeables, - or literal commands. -- For right-to-left languages, add the direction mark specified by the - [translator guide](translators-guide.md) when the first strong character of - the rendered message would otherwise have the wrong direction. -- Do not translate locale tags, message keys, or literal values. Do not - localize paths, commands, or machine-readable JSON field names. - -## Misc - -Consult the [Netsuke glossary](locize-glossary.md) for terminology and the -[translator guide](translators-guide.md) for Fluent structure, fallback rules, -variables, plurals, bidirectional text, and catalogue validation. When a source -message is ambiguous, inspect its code context and record the question for -review instead of guessing or silently broadening its meaning. - -The Locize project currently labels `en` as its reference language, whereas -Netsuke's source catalogue is `en-US`. Treat `en-US` as authoritative for -message content until the Locize project setting is corrected. - -[^1]: [Locize styleguide documentation](https://www.locize.com/docs/styleguide) From 9f134d085588132f5270010fa65dbfdc0cb4569e Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 23 Aug 2026 21:50:54 +0200 Subject: [PATCH 03/34] Add per-locale terminology sections for all 34 shipped locales Extend the localization glossary with a researched terminology section for every shipped locale. Each section records the address form and register with authoritative sources (Microsoft localization style guides, GNOME/KDE/Mozilla translation-team guidelines, GNU Make translations, and vendor documentation in the target language), the loan words the locale's technical writing conventionally keeps, false friends and mechanical hazards (the `manifest`/manifesto collision, Turkish dotted/dotless `i`, right-to-left direction marks, Persian zero-width non-joiners, Romanian comma-below diacritics, solid compounding in the Germanic languages), a worked example translating `Failed to load manifest at { $path }.`, and a 23-term table keeping `target`, `action`, and `rule` as three distinct words. Terms with no attested precedent are flagged as coined rather than presented as established. Address two review comments on the style guide: narrow the locale-integrity rule so only literal values that form part of a machine-readable contract are frozen, and expand BCP 47 to "Best Current Practice 47" on first use. Disable markdownlint's MD060 table-column-style rule: table alignment is owned by `mdformat-all`, whose padding disagrees with MD060's display-width model for RTL and Indic scripts and combining marks, so the two tools cannot both be satisfied on the new multilingual tables. --- .markdownlint-cli2.jsonc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index c6b393300..6d85b7267 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -8,7 +8,11 @@ "tables": false, "headings": false }, - "MD029": { "style": "ordered" } + "MD029": { "style": "ordered" }, + // Table alignment is owned by mdformat; MD060's display-width model + // disagrees with mdformat's padding for RTL and Indic scripts and for + // combining marks, so the two can never both be satisfied. + "MD060": false }, "ignores": [ "**/.venv/**", From 252cd61083813cace0746b99a271fd8a20d7ab3d Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 30 Aug 2026 10:39:42 +0200 Subject: [PATCH 04/34] Scope the MD060 exception to the localization glossary Disabling MD060 repository-wide in `.markdownlint-cli2.jsonc` removed table-alignment checking from every Markdown file to accommodate one. Restore the repository-level default and suppress the rule only where the conflict is real: `docs/localization-glossary.md` now carries a `markdownlint-disable-file MD060` directive with a comment recording why. Re-enabling the rule reports 53 violations, all in that file, so no other document needs the exception. Document the arrangement in the developers' guide: which tool owns table padding and paragraph wrapping, why MD060's display-width model and `mdtablefix`'s padding cannot both be satisfied for right-to-left, Indic, and combining-mark scripts, and the expectation that contributors scope future suppressions to a file or line rather than the whole repository. --- .markdownlint-cli2.jsonc | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index 6d85b7267..c6b393300 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -8,11 +8,7 @@ "tables": false, "headings": false }, - "MD029": { "style": "ordered" }, - // Table alignment is owned by mdformat; MD060's display-width model - // disagrees with mdformat's padding for RTL and Indic scripts and for - // combining marks, so the two can never both be satisfied. - "MD060": false + "MD029": { "style": "ordered" } }, "ignores": [ "**/.venv/**", From 6aef2e3996c4b85aa48f7c2d16c7dbd54358ff3d Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 15 Jun 2026 22:59:19 +0200 Subject: [PATCH 05/34] Add execplan for conditional action dependency regression coverage (3.14.5) Draft an execution plan for roadmap item 3.14.5: regression coverage for conditional action dependency manifests. The implementation (3.14.2-3.14.4) already exists, so the plan scopes test and documentation work only. The plan was developed with a planning agent team, reviewed and revised by a community-of-experts agent team (structural, contract/correctness, alternatives/DX, and reliability/ops lenses), and cross-checked against the codebase. It covers all four roadmap sub-bullets, frames the work through the manifest-time-expansion boundary (executable-discovery vs command-execution ports), and records open questions for the approver. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...conditional-action-dependency-manifests.md | 793 ++++++++++++++++++ 1 file changed, 793 insertions(+) create mode 100644 docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md diff --git a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md new file mode 100644 index 000000000..f17f34e53 --- /dev/null +++ b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md @@ -0,0 +1,793 @@ +# Regression coverage for conditional action dependency manifests (3.14.5) + +This ExecPlan (execution plan) is a living document. The sections +`Constraints`, `Tolerances`, `Risks`, `Progress`, `Surprises & Discoveries`, +`Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work +proceeds. + +Status: DRAFT + +## Purpose / big picture + +Netsuke already implements manifest-time conditional planning: actions and +targets may carry `foreach` (fan-out) and `when` (filter) keys that are +evaluated before the typed Abstract Syntax Tree (AST) is built (roadmap 3.14.1 +and 3.14.2); `command_available(name, **kwargs)` is a non-throwing probe that +returns `false` for absent commands instead of raising (roadmap 3.14.4); and +target/action `deps` lower into a separate *implicit dependency* class in the +intermediate representation (IR) and into Ninja's `|` implicit-dependency +syntax (roadmap 3.14.3). + +The behaviour works. What is missing is a deliberate, durable *regression net* +that pins these behaviours together as a single coherent contract: "a manifest +can select exactly one of two complementary actions based on tool availability, +the selection happens without running any shell command, and the selected +action's declared dependencies survive lowering into the IR and the generated +Ninja file." Roadmap item 3.14.5 asks for precisely this coverage. + +After this change a contributor can run `make test` and see new tests that +fail if any of the following regress: + +1. Action-level `when` filtering or action-level `foreach` fan-out. +2. Complementary `command_available(...)` / `not command_available(...)` + branches (the motivating real-world case is "use `cargo nextest` when it is + installed, otherwise fall back to `cargo test`") selecting *exactly one* + action. +3. The absent-command fallback path being chosen *without invoking the + `shell()` helper* — that is, conditional selection depends only on the + executable-discovery boundary, never on the command-execution boundary. +4. `deps` lowering into IR `implicit_deps` and into Ninja `build` statements, + including for conditionally-selected actions. + +The deliverable is test code plus small deterministic fixtures and +documentation. No production behaviour changes. + +## Constraints + +Hard invariants that must hold throughout implementation. Violation requires +escalation, not workarounds. + +- This is a **test-and-documentation-only** change. Do not modify production + semantics in `src/manifest/`, `src/ir/`, `src/ninja_gen.rs`, or + `src/stdlib/`. The single permitted exception is adding a narrowly-scoped, + test-only seam (for example a `pub(crate)` constructor or `#[cfg(test)]` + helper) *if and only if* an existing public or `pub(crate)` surface cannot + drive a required scenario; any such seam must be recorded in the Decision Log + and must not change runtime behaviour. +- Do not weaken or delete existing tests. New tests are additive. Existing + passing tests must continue to pass unchanged. +- Preserve the existing implicit `phony: true` behaviour for actions; tests + must observe it, not alter it. +- All commit gateways must pass before each CodeRabbit review and before each + milestone is declared complete: `make check-fmt`, `make typecheck`, + `make lint`, and `make test`. +- British English with Oxford spelling in all prose and doc comments + (per the documentation style guide and `en-gb-oxendict`). +- Determinism: no new test may depend on the host's real `PATH` contents. + "Present" cases must inject a temporary directory containing a fake + executable via the public `StdlibConfig::with_path_override(...)` seam. + "Absent" cases must combine **all three** guards together: an empty + `path_override`, a guaranteed-absent command name (for example a + UUID-suffixed name), *and* `cwd_mode="never"`. An empty `path_override` + alone is **not** sufficient: `parse_path_entries` maps an empty PATH + component to the current directory and the default `CwdMode::Auto` scans the + cwd (`src/stdlib/which/env.rs:91`, `src/stdlib/which/options.rs:11`), so + without `cwd_mode="never"` the temporary workspace's own contents could + shadow the lookup. + +## Tolerances (exception triggers) + +- Scope: if production (non-test) source files require changes beyond a single + additive test-only seam, stop and escalate. If the total new/changed line + count exceeds roughly 900 lines net, stop and reassess granularity. +- Interface: if any existing public API signature must change to make a + scenario testable, stop and escalate. +- Dependencies: adding `googletest` and `pretty_assertions` as + `[dev-dependencies]` is pre-authorised by the task brief (see Decision Log). + Use of `googletest` is **confined to the two in-crate white-box test files** + and is **gated on a Stage A interop spike** (`#[gtest]`+`#[rstest]` under the + pinned `rstest` 0.18.0); if the spike fails, fall back to bare + `verify_that!(...)?` returning `googletest::Result<()>` without `#[gtest]`, + or to `anyhow::ensure!`. `tests/` integration and snapshot files stay on the + existing `ensure!`+`insta` idiom. Any *other* new dependency triggers + escalation. +- Iterations: if a new test cannot be made to pass after 3 focused attempts + and the production behaviour appears correct, stop and escalate with the + evidence (the test may be encoding a wrong expectation). +- Sabotage check: if a newly-added test still passes after the targeted + production line it guards is deliberately broken (see "Validation"), the test + is vacuous; stop and redesign it before proceeding. +- Ambiguity: if the "exactly one action" or "without invoking `shell()`" + requirement turns out to be satisfiable in materially different ways, present + the options with trade-offs. + +## Risks + +- Risk: Tests of `command_available` accidentally depend on the host PATH and + become flaky in CI. + Severity: high. Likelihood: medium. + Mitigation: drive the real `which` resolver through the **public** + `StdlibConfig::with_path_override(...)` seam (threaded into the resolver by + `register_with_config`, `src/stdlib/register.rs:104`) for present cases, and + the three-guard absent recipe (empty `path_override`, a guaranteed-absent + name, and `cwd_mode="never"`) for absent cases. Precedent: + `tests/which_diagnostic_snapshot_tests.rs:30` already uses + `StdlibConfig::new(..).with_path_override(OsString::new())`. Note: the + private `mod which` makes `WhichConfig` unreachable from tests; do **not** + attempt to use it. + +- Risk: The `which` resolver cache is mistaken for process-global state and + someone "fixes" non-existent cross-test contamination with `fresh=true` or + serialisation. + Severity: low. Likelihood: medium. + Mitigation: the `WhichResolver` (with its `Arc>`) is + constructed fresh per `minijinja::Environment` (per `register`/`register_with_config` + call); there is no static/`OnceLock`/`thread_local` resolver. Each test + builds its own environment, so the cache is env-scoped and cannot leak across + tests. The cache key is also partitioned by `path_override`/`cwd`. The new + tests therefore need no `#[serial]` and may run in parallel. + +- Risk: `googletest`/`pretty_assertions` clash with the codebase's existing + uniform `anyhow::ensure!` style and create inconsistency. + Severity: low. Likelihood: high. + Mitigation: confine the new assertion crates to the new 3.14.5 test files; + keep using `ensure!` where it already reads well; document the convention in + `docs/developers-guide.md` so future tests are consistent. + +- Risk: New Ninja snapshots are environment-sensitive (path separators, + ordering) and churn. + Severity: medium. Likelihood: low. + Mitigation: reuse the established `insta` settings and + `tests/snapshots/ninja/` location; keep fixtures POSIX-path only; rely on the + IR's deterministic ordering (outputs sorted) already exercised by existing + snapshots. + +- Risk: Overlap with property/bounded-verification roadmap items (4.2.x Kani, + 4.3.2 Proptest for expansion invariants) leads to scope creep or duplicated + intent. + Severity: medium. Likelihood: medium. + Mitigation: 3.14.5 delivers *example-based* regression coverage only. + Property and bounded-model coverage of the same invariants is explicitly + deferred to 4.2.x/4.3.x and noted in the Decision Log. + +- Risk: The "without invoking `shell()`" assertion is driven through the + `StdlibState::is_impure()` flag, which flips for **any** impure helper — + `shell()`, `grep()`, *and* `fetch()` (network) + (`src/stdlib/command/mod.rs:81-102`, `src/stdlib/network/mod.rs:94,177`). It + is therefore a proxy for "no impure stdlib helper ran during selection", which + is strictly weaker than "the command-execution port specifically was not + driven". + Severity: low. Likelihood: low. + Mitigation: keep the no-shell fixtures minimal (only `command_available` and + plain `command:` recipes, no `grep`/`fetch`) so the flag cleanly means "no + impure helper ran during selection", and state this scoping honestly in both + the test module and the Hexagonal framing. If a finer-grained, + shell-specific observable is ever required, escalate rather than overloading + `is_impure()`. + +## Progress + +- [ ] Stage A: confirm interfaces and finalise the gap analysis (no code). +- [ ] Stage B: add new tests/fixtures in the failing-then-passing discipline + with per-test sabotage evidence (completed: none; remaining: all). +- [ ] Stage C: documentation updates (users-guide, developers-guide, + component architecture, ADR if warranted). +- [ ] Stage D: full gate run, CodeRabbit review, roadmap tick. + +(Timestamps to be added as work proceeds.) + +## Surprises & discoveries + +- Observation: the `path_override` seam is **publicly reachable**, but + `expand_foreach` is `pub(crate)`. + Evidence: `StdlibConfig::with_path_override(...)` is `pub` + (`src/stdlib/config.rs:240`) and `register_with_config` is `pub` and returns + the `StdlibState`; by contrast `mod which` is private + (`src/stdlib/mod.rs:18`), so `WhichConfig` is unreachable, and + `src/manifest/expand.rs:38` shows `pub(crate) fn expand_foreach`. + Impact: path injection needs **no new seam** — use the public + `with_path_override`/`register_with_config` pairing. The deterministic + real-resolver scenarios (nextest-vs-legacy and no-shell) must still be + **in-crate white-box tests** under `src/manifest/expand_test_cases/`, but the + reason is solely that they call the `pub(crate)` `expand_foreach`, *not* the + path override. The earlier speculative "test-only `WhichConfig` constructor + seam" is dropped as unnecessary. + +- Observation: a clean observability seam already exists for "no impure helper + ran": `StdlibState::is_impure()` is `pub`, and `shell()`, `grep()`, and + `fetch()` set the shared `impure: Arc` when they execute. The + flag is set **eagerly** (the first statement in the `shell` closure, before + any spawn), so merely *invoking* the helper flips it regardless of success. + Evidence: `src/stdlib/mod.rs:44` (`pub fn is_impure`), + `src/stdlib/command/mod.rs:84` (eager store), `src/stdlib/network/mod.rs:94`. + Impact: the "absent-command fallback without invoking `shell()`" requirement + is testable by asserting `is_impure() == false` after expansion of a fixture + that uses no impure helpers; and the control sub-case can flip the flag + deterministically with no binary dependency. Scope caveat: `expand_foreach` + evaluates only `when:`/`foreach:` expressions, not `command:` recipes, so the + assertion proves *selection-time* purity (exactly the 3.14.5 boundary), not + whole-pipeline purity. + +- Observation: meaningful coverage already exists and must not be duplicated. + Evidence and inventory are in "Context and orientation". + Impact: 3.14.5 is a *gap-fill*, not a green-field test suite. + +## Decision log + +- Decision: Treat 3.14.5 as test-and-docs only; the implementation + (3.14.2–3.14.4) is already complete. + Rationale: the roadmap marks 3.14.2/3.14.3/3.14.4 done; recon confirms the + behaviour is present. The roadmap bullet text is literally "Test ...". + Date/Author: 2026-06-15, planning agent. + +- Decision: Adopt `googletest` in a **confined, spike-gated** form, plus + `pretty_assertions` broadly, rather than using googletest throughout. + Specifics: `googletest` (`verify_that!`/`assert_that!` with matchers such as + `eq`, `len`, `contains`, `unordered_elements_are!`, `not`, `none`) is used + **only** in the two in-crate white-box files + (`command_available_selection_cases.rs`, `command_available_no_shell_cases.rs`), + and only after a Stage A spike confirms `#[gtest]`+`#[rstest]` compile and run + under the pinned `rstest` 0.18.0. `tests/` integration and snapshot files keep + the established `anyhow::ensure!`+`insta` idiom. `pretty_assertions` 1.4.x + (drop-in `assert_eq!`/`assert_ne!` shadow; human-facing diffs only, never + used for snapshot comparison) may be used wherever a value-equality diff + improves readability. + Rationale: the brief requests these crates for "clear test semantics", but the + community-of-experts review unanimously flagged that (a) googletest has zero + prior use in this repo (which is uniformly `ensure!`-based), (b) its + `#[gtest]`/`#[rstest]` interop is unverified against the old `rstest` 0.18.0 + pin and is a real compatibility risk, and (c) the 3.14.5 assertions are + trivial. Confining googletest to the two files that most benefit (real-world + matcher ergonomics for the selection/no-shell cases) honours the brief while + containing the inconsistency and interop risk; the Stage A spike with a + defined fallback removes the "assume it works" hazard. + Date/Author: 2026-06-15, planning agent. + **OPEN QUESTION FOR THE APPROVER (see "Open questions"):** the crew's + recommendation was stronger — drop `googletest` entirely and keep only + `pretty_assertions`. This plan keeps googletest (confined) to respect the + brief; the approver may instead elect the crew's recommendation at the + approval gate. + +- Decision: Inject `path_override` via the public + `StdlibConfig::with_path_override`; do not use `WhichConfig`. + Rationale: `mod which` is private, so `WhichConfig` is unreachable from tests; + the public `with_path_override`+`register_with_config` pairing already drives + the real resolver (precedent: `tests/which_diagnostic_snapshot_tests.rs:30`), + needing no new seam. In-crate placement is forced only by `expand_foreach` + being `pub(crate)`. + Date/Author: 2026-06-15, planning agent. + +- Decision: Document, but do not work around, the env-scoped `which` cache. + Rationale: the resolver cache is per-`Environment`, not process-global; the + new tests build their own environments and need no `#[serial]` and no + `fresh=true`. Recording this prevents future cargo-culted serialisation. + Date/Author: 2026-06-15, planning agent. + +- Decision: Defer property-based and bounded-model coverage of the same + invariants to roadmap 4.2.x (Kani) and 4.3.2 (Proptest for manifest + expansion invariants). + Rationale: 3.14.5 is scoped to example/behavioural/snapshot regression + coverage; 4.3.2 already owns "foreach preserves non-control fields", "when is + removed after evaluation", and "item/index injected". Duplicating that here + would create two owners for one invariant. + Date/Author: 2026-06-15, planning agent. + +- Decision: `ortho_config` is out of scope for this item. + Rationale: `ortho_config` governs layered CLI/configuration precedence + (roadmap 3.11.x) and localized help; 3.14.5 concerns manifest-time expansion + and lowering, which do not read layered configuration. The brief's mention is + acknowledged and explicitly scoped out to prevent creep. + Date/Author: 2026-06-15, planning agent. + +- Decision: Place deterministic real-resolver tests in-crate; place the + end-to-end deps+Ninja snapshot test in `tests/` using a guaranteed-absent + command for determinism. + Rationale: `WhichConfig`/`expand_foreach` are `pub(crate)` (white-box only); + an absent command resolves to `false` deterministically regardless of host, + so a `tests/`-level snapshot can select the fallback branch reproducibly. + Date/Author: 2026-06-15, planning agent. + +## Outcomes & retrospective + +To be completed at milestone boundaries and at completion. + +## Context and orientation + +This section assumes no prior knowledge of the repository. + +### Vocabulary + +- **Manifest**: the user's YAML build description (`Netsukefile` / `*.yml`). +- **Typed AST**: the deserialized Rust representation of a manifest + (`src/ast.rs`: `NetsukeManifest`, `Target`, `Rule`, `Recipe`, + `StringOrList`). +- **Manifest-time expansion**: evaluation of `foreach`/`when` on the *raw* + manifest value before the typed AST exists (`src/manifest/expand.rs`). +- **IR (intermediate representation)**: the build graph + (`src/ir/graph.rs`: `BuildGraph`, `Action`, `BuildEdge`). +- **Implicit dependency**: a `deps` entry that affects rebuild/ordering but is + not passed to the recipe as `$in`; emitted in Ninja after a `|` separator. +- **`command_available(name, **kwargs)`**: a non-throwing Jinja function that + returns `true`/`false` for executable presence + (`src/stdlib/which/mod.rs`). +- **`shell()`**: an *impure* Jinja filter that runs a command during rendering + and sets the stdlib `impure` flag (`src/stdlib/command/`). + +### Pipeline (data flow) + +`manifest::from_str/from_path` +→ parse YAML to a raw `ManifestValue` +→ register the stdlib into a Jinja `Environment` (this is where + `command_available` and `shell` are bound), capturing a `StdlibState` +→ `expand::expand_foreach(&mut doc, &env)` removes `when:false` entries and + fans out `foreach`, injecting `item`/`index` +→ deserialize to the typed AST (`deserialize_actions` stamps `phony: true`) +→ `render::render_manifest` evaluates remaining string templates +→ `ir::BuildGraph::from_manifest` lowers `sources`→`inputs`, + `deps`→`implicit_deps`, `order_only_deps`→`order_only_deps`, and runs cycle + detection over `inputs` + `implicit_deps` +→ `ninja_gen::generate` formats `build` lines as + `build [| ]: [| ] [|| ]`. + +Key files and symbols (full paths relative to repo root): + +- `src/ast.rs`: `Target` (fields `sources`, `deps`, `order_only_deps`, + `phony`), `Rule.deps`, `StringOrList`, `deserialize_actions`. +- `src/manifest/expand.rs`: `expand_foreach` (`pub(crate)`), + `expand_section`, `expand_target`, `when_allows`, `eval_when`, + `when_context`, `inject_iteration_vars`, `FilteringStats`. +- `src/manifest/mod.rs`: `from_str`, `from_path`, `from_str_named` + (takes `stdlib_config: Option`). +- `src/stdlib/mod.rs`: `StdlibState::is_impure` / `reset_impure` (`pub`). +- `src/stdlib/register.rs`: `register`, `register_with_config` (`pub`). +- `src/stdlib/which/mod.rs`: `WhichConfig::new` (`pub(crate)`), + `command_available_with`, `is_command_available`. +- `src/ir/from_manifest.rs`: `BuildGraph::from_manifest`, `process_targets` + (`to_paths(&target.sources)`→inputs, `to_paths(&target.deps)`→implicit_deps). +- `src/ir/graph.rs`: `BuildEdge { inputs, implicit_deps, order_only_deps, ... }`. +- `src/ninja_gen.rs`: `generate`, `generate_into`, `DisplayEdge` (the `|`/`||` + formatting). + +### Existing coverage (do not duplicate; extend or complement) + +- `src/manifest/expand_test_cases/condition_cases.rs`: parameterised + `targets`/`actions` cases for `when:false` removal before typed AST, + `foreach` iteration-var injection, static action `when` dropping, sequence + `foreach`, truthiness table, invalid-`when` errors, and **complementary + `command_available` branches for actions** — but the latter uses a *stubbed* + `command_available` function, not the real resolver. +- `src/manifest/expand_test_cases/target_command_available_cases.rs`: the same + complementary pattern for targets, also stubbed. +- `tests/data/actions_command_available_absent.yml` + BDD scenarios in + `tests/features/manifest.feature` ("Selecting a fallback action when a + command is unavailable", "Parsing fails when command availability receives + invalid options") and steps in `tests/bdd/steps/conditional_manifest.rs`: + exercise the *real* resolver for the absent case, but do **not** assert that + `shell()` was not invoked, and do not use the nextest-vs-legacy framing. +- `tests/ir_from_manifest_tests.rs`: `manifest_deps_populate_implicit_deps`, + `manifest_deps_do_not_contribute_to_recipe_inputs` — static targets only. +- `tests/ninja_snapshot_tests.rs`: `conditional_manifest_ninja_snapshot` + (foreach+when on a target), `implicit_deps_manifest_ninja_snapshot` + (`tests/data/implicit_deps.yml`) — but the two concerns (conditional + selection and `deps` lowering) are never combined in one fixture. + +### Gap analysis (what 3.14.5 must add) + +1. **Action-level `when` and `foreach`** — mostly covered. Gap: a single + integrated regression fixture exercising an action that carries `foreach`, + `when`, *and* `deps` together, end-to-end to Ninja. +2. **Complementary nextest/legacy branches select exactly one action** — not + covered. The existing complementary tests are stubbed and use generic + names. New: a real-resolver, nextest-vs-legacy scenario proving exactly one + action survives in both the present and absent worlds. +3. **Absent-command fallback without invoking `shell()`** — not covered. New: + assert `StdlibState::is_impure() == false` after selecting the fallback. +4. **`deps` lowering for conditional actions, in IR and Ninja** — partially + covered for static targets only. New: assert that a *conditionally-selected* + action's `deps` become `implicit_deps` in the IR and `| ...` in Ninja, and + that the *unselected* branch's deps never appear. + +### Hexagonal framing (boundaries this suite protects) + +Treat manifest-time conditional expansion as **domain/policy** logic. It +depends on two **driven ports**: + +- *Executable discovery* (the `which` resolver behind `command_available`). +- *Command execution* (the `shell()` helper). + +The core regression contract is a boundary assertion: **conditional selection +drives no impure side-effecting port.** `StdlibState::is_impure()` is the +observable proxy, but it is honest about its breadth — it flips for *any* +impure helper (`shell()`, `grep()`, or `fetch()`), so the precise statement the +test pins is "selection executed no impure stdlib helper", which subsumes (and +is slightly broader than) "the command-execution port was not driven". The +no-shell fixtures deliberately use only `command_available` and plain +`command:` recipes so the flag's meaning is unambiguous in context. No new +ports or adapters are introduced; the tests pin the existing boundary so future +refactors cannot quietly make selection depend on running commands. This plan +uses hexagonal thinking to choose *what to assert at the boundary*, not to +restructure code. (If a strictly shell-only observable is ever needed, that is +an escalation, not an overload of `is_impure()`.) + +## Plan of work + +### Stage A — confirm interfaces, run the interop spike, finalise gap analysis + +This stage adds **one throwaway spike** (immediately reverted) and otherwise +changes no code. + +1. **googletest interop spike (go/no-go for the googletest decision).** Add a + single throwaway test under the lib target: + + ```rust + // scratch, never committed + use googletest::prelude::*; + use rstest::rstest; + #[gtest] + #[rstest] + #[case(1)] + fn spike(#[case] n: i32) -> googletest::Result<()> { verify_that!(n, eq(1)) } + ``` + + Run it. If it compiles and passes under `rstest` 0.18.0, proceed with the + confined googletest approach. If it fails, adopt the fallback (bare + `verify_that!(...)?` in a plain `#[rstest] -> googletest::Result<()>` without + `#[gtest]`, or `ensure!`), and record the outcome in the Decision Log. + Revert the spike before any milestone commit. + +2. **Confirm the injection seam (public, no new seam).** Verify + `crate::stdlib::register_with_config(&mut Environment, StdlibConfig) + -> anyhow::Result` and that `StdlibConfig::with_path_override` + threads into the resolver (`src/stdlib/register.rs:104`). Mirror the + precedent in `tests/which_diagnostic_snapshot_tests.rs:30`. Do **not** use + `WhichConfig` (private module). + +3. **Confirm `expand_foreach` drives selection** in a white-box test with the + *real* `command_available` registered (existing cases use a stubbed + `Environment::new()` function; the new cases register the full stdlib so the + real resolver runs). + +4. **Confirm deterministic deps ordering** in `DisplayEdge::fmt` + (`src/ninja_gen.rs`): record the exact `build` line shape and confirm + `implicit_deps`/`order_only_deps` are emitted in a deterministic order + before writing any snapshot. If ordering is insertion-order rather than + sorted, fixtures must use already-sorted deps so the snapshot is stable. + +Go/no-go: if the spike fails *and* no acceptable fallback compiles, or any +required seam is genuinely missing, escalate per Tolerances. + +### Stage B — red/green tests with sabotage evidence + +Because the production behaviour already exists, classic Red-Green-Refactor is +adapted: each new test is written, shown to **pass** against current code, and +then proven **non-vacuous** by a *sabotage check* — temporarily breaking the +single production line it guards and confirming the test fails. The sabotage +evidence must capture the **assertion-level failure message** (the specific +`verify_that!`/`ensure!`/`assert_snapshot!` line that fired and a diff that +corresponds to the guarded behaviour, e.g. "expected len 1, got 2", or +`implicit_deps` content appearing under `inputs`), not merely "test failed" — +this proves the test fails for the *right reason*, which a bare pass/fail +toggle does not. The sabotage diff is reverted with an explicit +`git checkout -- ` (recorded so a resumed agent can confirm a clean tree) +and is never committed; the transcript is recorded under "Artifacts and notes". +This is the documented substitute for the red stage (see the execplans guidance +on observable substitutes). + +Add the following, smallest first: + +1. **In-crate: nextest-vs-legacy exactly-one-action (real resolver).** + New file `src/manifest/expand_test_cases/command_available_selection_cases.rs` + (wired into the `expand_test_cases` module). An `#[rstest]` parameterised + over two worlds, driving the *real* `command_available` (registered via + `register_with_config` with a `StdlibConfig`): + - *present*: a `tempfile::TempDir` containing a fake `cargo-nextest` + executable, injected via `StdlibConfig::with_path_override`. Reuse the + repo's existing cross-platform helper idiom from + `tests/which_diagnostic_snapshot_tests.rs:48-75` (`tool_filename` adds + `.cmd` on Windows; `mark_executable` sets `0o755` on Unix) so the present + branch runs on **both** platforms rather than a bare `#[cfg(unix)]` — the + Windows PATHEXT path (`src/stdlib/which/env.rs`) is the part most likely to + regress. Expect exactly the `run-tests-nextest` action (`command: cargo + nextest run`) to survive. + - *absent*: the three-guard recipe — empty `path_override` + + guaranteed-absent command name + `cwd_mode="never"`. Expect exactly the + `run-tests-legacy` action (`command: cargo test`) to survive. + Drive `expand_foreach`, then assert (googletest, per Stage A spike): + `verify_that!(actions, len(eq(1)))?`, the surviving action's `name`/`command` + via `matches_pattern!`/`eq`, and that `when` was removed. + This also explicitly pins **bullet 1** for actions: include one case where + the surviving action additionally carries a `foreach` so the assertion + covers action-level fan-out and `when` together (complementing, not + duplicating, `condition_cases.rs`). + +2. **In-crate: absent fallback without `shell()`.** + New file `src/manifest/expand_test_cases/command_available_no_shell_cases.rs`. + Register the full stdlib (so `command_available`, `shell`, `grep`, `fetch` + all exist), capture `StdlibState`, expand a manifest whose two actions use + `command_available("")` / `not command_available(...)` + and whose recipes are plain `command:` strings (no impure helpers), then + assert: exactly one action (the fallback) survives, `when` removed, and + `verify_that!(state.is_impure(), eq(false))?`. Add a contrasting *control* + sub-case proving the proxy is non-vacuous: a fixture that calls + `shell('true')` **inside a `when:` expression** (e.g. + `when: "{{ '' | shell('true') }} == ''"`) — because `expand_foreach` + evaluates `when:`/`foreach:` but not `command:` recipes, the helper must be + in a `when:` to flip the flag during the same call under test. The impure + flag is set eagerly on invocation, so assert `is_impure()==true` + deterministically with **no** `ensure_binaries_available` gate (avoiding a + skip-driven hole). + +3. **Integration + IR: conditional action carries `deps` into `implicit_deps`, + including the `item`-in-`deps` interaction.** + New fixture `tests/data/conditional_action_deps.yml`: a target and an action, + each selected via a complementary `command_available` pair using a + guaranteed-absent command (`cwd_mode="never"` retained) so the fallback + branch is deterministic, each carrying `sources`, `deps`, and + `order_only_deps`. Crucially, the action uses `foreach` and interpolates + `{{ item }}` (or `{{ index }}`) into **one `deps` entry** (e.g. + `deps: [build/{{ item }}.o]`) — the per-item implicit-dependency case is the + single most likely real-world regression and is currently untested anywhere. + **Add the new tests to the existing `tests/ir_from_manifest_tests.rs`** (beside + the static-deps cases `manifest_deps_populate_implicit_deps` / + `manifest_deps_do_not_contribute_to_recipe_inputs`, keeping all deps-lowering + tests in one place). Assert that each selected edge has `implicit_deps` equal + (unordered) to the declared/substituted `deps`, `inputs` equal to `sources` + only, `order_only_deps` carried through, the expected per-`item` + substitution, and that none of the *unselected* branch's paths appear in any + edge. (Cycle detection over `implicit_deps` is already owned by + `src/ir/cycle.rs` unit tests and is not re-tested here.) + +4. **Snapshot + real-ninja: conditional action deps reach the Ninja file.** + New test `conditional_action_deps_ninja_snapshot` **added to + `tests/ninja_snapshot_tests.rs`** using the fixture from B.3. Assert the + selected `build` line contains `| ` (implicit) **and** the `|| ` + segment (make `order_only_deps` definitely present in the fixture so this is + not "where applicable"); assert the unselected branch's outputs/deps are + absent; then `insta::assert_snapshot!` into `tests/snapshots/ninja/` reusing + the existing `Settings::set_snapshot_path`. Use `insta::assert_snapshot!` + only for the rendered Ninja text — never `pretty_assertions::assert_eq!`. + Where `ninja`/`python3` are available, also run `ninja -t query`/`-n` to + prove the file is valid and reaches a no-op second pass, mirroring + `touch_manifest_ninja_validation`; surface any skip via `eprintln!` (captured + by the harness) in addition to `tracing::warn!` so a binary-less CI run does + not silently validate nothing (see Risks / Doggylump S2). + +5. **BDD: the combined conditional-action-with-deps scenario only.** + The unit (B.1), IR (B.3), and snapshot+real-ninja (B.4) layers already pin + selection and deps emission; existing BDD scenarios + (`tests/features/manifest.feature:132`, `tests/features/ninja.feature:31`) + already cover fallback-selection and deps-emission *separately*. To avoid + redundant step-matcher maintenance, B.5 adds **only** the genuinely new + externally-observable case: a single scenario where a *conditionally-selected* + action's `deps` appear as Ninja implicit dependencies end-to-end. Reuse + existing steps in `tests/bdd/steps/conditional_manifest.rs` and + `tests/bdd/steps/ninja.rs`; add at most one new `Then` (the selected action + exposes a given implicit dependency) if no existing step fits. If even this + is fully covered by reframing an existing scenario, prefer reframing over a + new scenario. + +Each of B.1–B.5 ends with the focused test passing, its sabotage check +recorded, and the relevant gate (`make test`) green. + +### Stage C — documentation + +- `docs/users-guide.md`: ensure the conditional-action / `deps` semantics are + described from a user's perspective (selection by tool availability; `deps` + as implicit dependencies that affect rebuilds but not recipe arguments). Add + a short worked nextest-vs-legacy example if not already present. (Behaviour + is unchanged, so this is clarification, not new UI.) +- `docs/developers-guide.md`: document the test conventions introduced here — + when to reach for `googletest`/`pretty_assertions` vs `ensure!`, the + `path_override`/`cwd_mode` pattern for deterministic `command_available` + tests, and the `is_impure()`-as-boundary-proxy idiom. +- Component architecture doc for the manifest/stdlib boundary: record the + port framing (executable-discovery vs command-execution) and that 3.14.5 + pins it. If the decision to standardise the boundary assertion is judged + substantive, capture it as an ADR using the `arch-decision-records` + Y-Statement format and reference it from this plan and the design doc. +- `docs/roadmap.md`: tick 3.14.5 and its four sub-bullets on completion. + +### Stage D — gates, review, finalise + +Run the full gate suite, then `coderabbit review --agent`; clear all concerns +before declaring done. Update Progress/Outcomes. + +## Concrete steps + +Run from the repository root. Use `tee` to a per-action log under `/tmp` so +truncated output can be reviewed: + +```bash +# focused red/green loop for a single new test (example) +cargo test --test ir_from_manifest_tests conditional_action_deps \ + 2>&1 | tee /tmp/test-netsuke-$(git branch --show-current).out +``` + +```bash +# in-crate (white-box) expansion tests live in the library target +cargo test --lib expand_test_cases::command_available \ + 2>&1 | tee /tmp/test-netsuke-$(git branch --show-current).out +``` + +```bash +# update/inspect snapshots deliberately (review before accepting) +cargo insta test --review # local authoring only +# CI / gates must never auto-accept a changed snapshot: +INSTA_UPDATE=no make test +``` + +```bash +# milestone gates (run sequentially to benefit from build caching) +make check-fmt 2>&1 | tee /tmp/check-fmt-netsuke-$(git branch --show-current).out +make typecheck 2>&1 | tee /tmp/typecheck-netsuke-$(git branch --show-current).out +make lint 2>&1 | tee /tmp/lint-netsuke-$(git branch --show-current).out +make test 2>&1 | tee /tmp/test-netsuke-$(git branch --show-current).out +``` + +Expected: each gate reports success; `make test` shows the new tests passing +and the existing suite unchanged. + +## Validation and acceptance + +Acceptance is behavioural: + +1. `make test` passes with the new tests present. +2. For each new test, the recorded **sabotage check** demonstrates the test + fails when its guarded production line is broken and passes when restored. + Representative sabotage points: + - selection: force `is_command_available` to always return `true` (or always + `false`) → the exactly-one-action and nextest/legacy tests must fail. + - no-shell: make conditional selection call `shell()` (or force + `impure` true) → the `is_impure()==false` test must fail. + - deps: make `process_targets` route `to_paths(&target.deps)` into `inputs` + instead of `implicit_deps` → the IR and Ninja deps tests must fail. +3. The new Ninja snapshot matches and, where `ninja`/`python3` are available, + the generated file builds and a second pass reports "no work to do". +4. BDD scenarios for selection and deps emission pass via the `bdd_tests` + harness. + +Quality criteria ("done"): + +- Tests: all new unit (`rstest` + `googletest`), integration, snapshot + (`insta`), and BDD (`rstest-bdd`) tests pass; existing tests unchanged. +- Lint/typecheck/format: `make lint`, `make typecheck`, `make check-fmt` clean + with `-D warnings`. +- Docs: users-guide, developers-guide, component architecture (and ADR if + created) updated; markdown lint clean. +- Review: `coderabbit review --agent` concerns all resolved. + +## Idempotence and recovery + +All steps are additive and re-runnable. Snapshots are created deliberately via +`cargo insta` and reviewed before acceptance; a wrong snapshot is corrected by +re-reviewing, not by force-accepting. Sabotage checks are always reverted +(`git checkout -- `) and never committed. Commit after each of B.1–B.5 +so any step can be rolled back independently. + +## Artifacts and notes + +Record here, as work proceeds: the exact surviving-action assertions, the +generated Ninja snapshot for the conditional-deps fixture, and the sabotage +transcripts (broken-line diff + failing test name + restored pass) that prove +each new test is non-vacuous. + +## Interfaces and dependencies + +New `[dev-dependencies]` in `Cargo.toml` (pre-authorised by the brief): + +```toml +# Cargo.toml +googletest = "0.14" +pretty_assertions = "1.4" +``` + +Existing crates reused: `rstest` 0.18, `rstest-bdd`/`rstest-bdd-macros` 0.5, +`insta` 1 (yaml), `tempfile`, `test_support`, `anyhow` (`ensure!`). +(`serial_test` is **not** needed — the new tests build their own environments +and inject `path_override`, so they never touch process-global PATH; see the +env-scoped-cache Decision.) + +Seams relied upon (all already public/`pub(crate)` and present; **no new seam +required**): + +- `crate::manifest::expand::expand_foreach(&mut ManifestValue, &Environment) + -> anyhow::Result` (`pub(crate)` — forces in-crate placement + of the selection/no-shell tests). +- `crate::stdlib::register_with_config(&mut Environment, StdlibConfig) + -> anyhow::Result` (`pub`; note the `Result`), + `StdlibConfig::with_path_override(impl Into)` (`pub`, + `src/stdlib/config.rs:240`), and `StdlibState::is_impure(&self) -> bool` + (`pub`). Do **not** reference `WhichConfig` (private module). +- `netsuke::ir::BuildGraph::from_manifest(&NetsukeManifest) + -> Result` and `netsuke::ninja_gen::generate(&BuildGraph) + -> Result` (`pub`) for the integration/snapshot tests. + +New/changed test files and fixtures (final names to be confirmed in Stage A): + +- new `src/manifest/expand_test_cases/command_available_selection_cases.rs` +- new `src/manifest/expand_test_cases/command_available_no_shell_cases.rs` + (both wired into the `expand_test_cases` module). +- new `tests/data/conditional_action_deps.yml`. +- additions to existing `tests/ir_from_manifest_tests.rs` (deps lowering) and + `tests/ninja_snapshot_tests.rs` (Ninja snapshot + real-ninja) — kept beside + their static-deps counterparts, not in a separate file. +- new `tests/snapshots/ninja/ninja_snapshot_tests__conditional_action_deps_ninja.snap`. +- one new combined scenario in `tests/features/ninja.feature` (or + `manifest.feature`) plus at most one new step. +- new `[dev-dependencies]` entries `googletest = "0.14"` and + `pretty_assertions = "1.4"` in `Cargo.toml`. + +## Open questions for the approver + +1. **googletest.** The brief mandates googletest; the community-of-experts + review unanimously recommended dropping it (keeping only `pretty_assertions`) + because it has no prior use here, its `#[gtest]`+`#[rstest]` interop is + unverified under `rstest` 0.18.0, and the assertions are trivial. This plan + keeps googletest in a confined, spike-gated form to respect the brief. The + approver may instead choose: (a) confined + spike-gated (as planned), + (b) `pretty_assertions` only, no googletest, or (c) googletest throughout. +2. **BDD scope.** Reviewers judged a full BDD layer largely redundant; the plan + reduces B.5 to the single combined scenario. Confirm this is the desired + depth, or request fuller BDD coverage. +3. **Stray file.** `src/stdlib/command/mod_backup.rs` is an unreferenced ~41 KB + backup duplicating impure-flag logic. Out of scope for 3.14.5, but flagged + for separate cleanup so future grep-based reasoning is not misled. + +## Signposted documentation and skills + +Documentation to consult while implementing: + +- `docs/netsuke-design.md` §2.5 (manifest-time `foreach`/`when` semantics), + §2.4 and §5.3 (dependency classes and Ninja lowering), + and the "executable discovery" section (`command_available`). +- `docs/rust-testing-with-rstest-fixtures.md` — fixture and `#[case]` patterns. +- `docs/reliable-testing-in-rust-via-dependency-injection.md` — the + `path_override`/resolver-injection approach used for deterministic + `command_available` tests. +- `docs/rust-doctest-dry-guide.md` — if any doc examples are added. +- `docs/rstest-bdd-users-guide.md` — feature/step wiring for the BDD additions. +- `docs/ortho-config-users-guide.md` — referenced by the brief; explicitly + out of scope here (see Decision Log). + +Skills to load while implementing: + +- `rust-router` then `rust-unit-testing` (assertion helpers, fixtures, table + tests, `googletest`/`pretty_assertions`/`insta` usage). +- `hexagonal-architecture` — to keep the boundary assertions honest. +- `nextest` — for the nextest-vs-legacy framing and running the suite. +- `leta` — for navigation/refactors. +- `arch-decision-records` — only if the boundary decision warrants an ADR. +- `proptest` / `kani` — *not* used here; their invariants are owned by 4.3.2 / + 4.2.x (recorded in the Decision Log). + +## Revision note + +- 2026-06-15 — Revised after a community-of-experts review (Logisphere crew: + structural, contract/correctness, alternatives/DX, reliability/ops lenses). + What changed and why: + - Corrected the injection seam: `WhichConfig` is in a private module and + unreachable; switched to the public `StdlibConfig::with_path_override` + +`register_with_config` pairing (precedent in + `tests/which_diagnostic_snapshot_tests.rs`). Removed the speculative + test-only seam. In-crate placement is now justified solely by + `expand_foreach` being `pub(crate)`. + - Restated the `is_impure()` boundary proxy honestly: it flips for `shell()`, + `grep()`, *and* `fetch()`; downgraded the hexagonal claim accordingly. + - Added a Stage A `#[gtest]`+`#[rstest]` interop spike (rstest 0.18.0 risk) + with a defined fallback; confined googletest to the two white-box files and + surfaced the crew's "drop googletest" recommendation as an approver choice. + - Hardened determinism: the absent case now requires empty `path_override` + + guaranteed-absent name + `cwd_mode="never"` together. + - Fixed the `shell()` control sub-case to live in a `when:` expression + (expansion evaluates `when`/`foreach`, not `command:` recipes) and removed + its binary-availability gate (the impure flag is set eagerly). + - Added the `item`-in-`deps` interaction (highest-value uncaught regression), + pinned action-level `foreach` explicitly, and folded the IR/Ninja tests + into existing files. Trimmed the BDD layer to the single combined scenario. + - Added CI snapshot discipline (`INSTA_UPDATE=no`), skip visibility via + `eprintln!`, the env-scoped-cache note (no `serial_test`), corrected + `register_with_config -> anyhow::Result`, and an + "Open questions for the approver" section. + Effect on remaining work: scope and file count are essentially unchanged; the + plan is now anchored to reachable seams and verifiable assumptions. No code + has been written; the plan remains in DRAFT pending approval. From b341b775c7001d8c7c7c4291b611db2cfb3a3a9b Mon Sep 17 00:00:00 2001 From: leynos Date: Tue, 28 Jul 2026 23:57:04 +0200 Subject: [PATCH 06/34] Refresh conditional-manifest regression plan Update the rebased plan for the current `rstest` version and remove the upstream-resolved backup-module question. Apply repository Markdown formatting and Oxford spelling so the plan passes the current documentation gates. --- ...conditional-action-dependency-manifests.md | 426 +++++++++--------- 1 file changed, 209 insertions(+), 217 deletions(-) diff --git a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md index f17f34e53..c6f6bd395 100644 --- a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md +++ b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md @@ -1,9 +1,8 @@ # Regression coverage for conditional action dependency manifests (3.14.5) -This ExecPlan (execution plan) is a living document. The sections -`Constraints`, `Tolerances`, `Risks`, `Progress`, `Surprises & Discoveries`, -`Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work -proceeds. +This ExecPlan (execution plan) is a living document. The sections `Constraints`, +`Tolerances`, `Risks`, `Progress`, `Surprises & Discoveries`, `Decision Log`, +and `Outcomes & Retrospective` must be kept up to date as work proceeds. Status: DRAFT @@ -25,8 +24,8 @@ the selection happens without running any shell command, and the selected action's declared dependencies survive lowering into the IR and the generated Ninja file." Roadmap item 3.14.5 asks for precisely this coverage. -After this change a contributor can run `make test` and see new tests that -fail if any of the following regress: +After this change a contributor can run `make test` and see new tests that fail +if any of the following regress: 1. Action-level `when` filtering or action-level `foreach` fan-out. 2. Complementary `command_available(...)` / `not command_available(...)` @@ -68,12 +67,12 @@ escalation, not workarounds. executable via the public `StdlibConfig::with_path_override(...)` seam. "Absent" cases must combine **all three** guards together: an empty `path_override`, a guaranteed-absent command name (for example a - UUID-suffixed name), *and* `cwd_mode="never"`. An empty `path_override` - alone is **not** sufficient: `parse_path_entries` maps an empty PATH - component to the current directory and the default `CwdMode::Auto` scans the - cwd (`src/stdlib/which/env.rs:91`, `src/stdlib/which/options.rs:11`), so - without `cwd_mode="never"` the temporary workspace's own contents could - shadow the lookup. + UUID-suffixed name), *and* `cwd_mode="never"`. An empty `path_override` alone + is **not** sufficient: `parse_path_entries` maps an empty PATH component to + the current directory and the default `CwdMode::Auto` scans the cwd + (`src/stdlib/which/env.rs:91`, `src/stdlib/which/options.rs:11`), so without + `cwd_mode="never"` the temporary workspace's own contents could shadow the + lookup. ## Tolerances (exception triggers) @@ -83,10 +82,10 @@ escalation, not workarounds. - Interface: if any existing public API signature must change to make a scenario testable, stop and escalate. - Dependencies: adding `googletest` and `pretty_assertions` as - `[dev-dependencies]` is pre-authorised by the task brief (see Decision Log). + `[dev-dependencies]` is pre-authorized by the task brief (see Decision Log). Use of `googletest` is **confined to the two in-crate white-box test files** and is **gated on a Stage A interop spike** (`#[gtest]`+`#[rstest]` under the - pinned `rstest` 0.18.0); if the spike fails, fall back to bare + pinned `rstest` 0.26.1); if the spike fails, fall back to bare `verify_that!(...)?` returning `googletest::Result<()>` without `#[gtest]`, or to `anyhow::ensure!`. `tests/` integration and snapshot files stay on the existing `ensure!`+`insta` idiom. Any *other* new dependency triggers @@ -104,70 +103,62 @@ escalation, not workarounds. ## Risks - Risk: Tests of `command_available` accidentally depend on the host PATH and - become flaky in CI. - Severity: high. Likelihood: medium. - Mitigation: drive the real `which` resolver through the **public** + become flaky in CI. Severity: high. Likelihood: medium. Mitigation: drive the + real `which` resolver through the **public** `StdlibConfig::with_path_override(...)` seam (threaded into the resolver by `register_with_config`, `src/stdlib/register.rs:104`) for present cases, and the three-guard absent recipe (empty `path_override`, a guaranteed-absent name, and `cwd_mode="never"`) for absent cases. Precedent: `tests/which_diagnostic_snapshot_tests.rs:30` already uses - `StdlibConfig::new(..).with_path_override(OsString::new())`. Note: the - private `mod which` makes `WhichConfig` unreachable from tests; do **not** - attempt to use it. + `StdlibConfig::new(..).with_path_override(OsString::new())`. Note: the private + `mod which` makes `WhichConfig` unreachable from tests; do **not** attempt + to use it. - Risk: The `which` resolver cache is mistaken for process-global state and someone "fixes" non-existent cross-test contamination with `fresh=true` or - serialisation. - Severity: low. Likelihood: medium. - Mitigation: the `WhichResolver` (with its `Arc>`) is - constructed fresh per `minijinja::Environment` (per `register`/`register_with_config` - call); there is no static/`OnceLock`/`thread_local` resolver. Each test - builds its own environment, so the cache is env-scoped and cannot leak across - tests. The cache key is also partitioned by `path_override`/`cwd`. The new - tests therefore need no `#[serial]` and may run in parallel. + serialization. Severity: low. Likelihood: medium. Mitigation: the + `WhichResolver` (with its `Arc>`) is constructed fresh per + `minijinja::Environment` (per `register`/`register_with_config` call); there + is no static/`OnceLock`/`thread_local` resolver. Each test builds its own + environment, so the cache is env-scoped and cannot leak across tests. The + cache key is also partitioned by `path_override`/`cwd`. The new tests + therefore need no `#[serial]` and may run in parallel. - Risk: `googletest`/`pretty_assertions` clash with the codebase's existing - uniform `anyhow::ensure!` style and create inconsistency. - Severity: low. Likelihood: high. - Mitigation: confine the new assertion crates to the new 3.14.5 test files; - keep using `ensure!` where it already reads well; document the convention in - `docs/developers-guide.md` so future tests are consistent. + uniform `anyhow::ensure!` style and create inconsistency. Severity: low. + Likelihood: high. Mitigation: confine the new assertion crates to the new + 3.14.5 test files; keep using `ensure!` where it already reads well; document + the convention in `docs/developers-guide.md` so future tests are consistent. - Risk: New Ninja snapshots are environment-sensitive (path separators, - ordering) and churn. - Severity: medium. Likelihood: low. - Mitigation: reuse the established `insta` settings and - `tests/snapshots/ninja/` location; keep fixtures POSIX-path only; rely on the - IR's deterministic ordering (outputs sorted) already exercised by existing - snapshots. + ordering) and churn. Severity: medium. Likelihood: low. Mitigation: reuse the + established `insta` settings and `tests/snapshots/ninja/` location; keep + fixtures POSIX-path only; rely on the IR's deterministic ordering (outputs + sorted) already exercised by existing snapshots. - Risk: Overlap with property/bounded-verification roadmap items (4.2.x Kani, 4.3.2 Proptest for expansion invariants) leads to scope creep or duplicated - intent. - Severity: medium. Likelihood: medium. - Mitigation: 3.14.5 delivers *example-based* regression coverage only. - Property and bounded-model coverage of the same invariants is explicitly - deferred to 4.2.x/4.3.x and noted in the Decision Log. + intent. Severity: medium. Likelihood: medium. Mitigation: 3.14.5 delivers + *example-based* regression coverage only. Property and bounded-model coverage + of the same invariants is explicitly deferred to 4.2.x/4.3.x and noted in the + Decision Log. - Risk: The "without invoking `shell()`" assertion is driven through the `StdlibState::is_impure()` flag, which flips for **any** impure helper — `shell()`, `grep()`, *and* `fetch()` (network) (`src/stdlib/command/mod.rs:81-102`, `src/stdlib/network/mod.rs:94,177`). It - is therefore a proxy for "no impure stdlib helper ran during selection", which - is strictly weaker than "the command-execution port specifically was not - driven". - Severity: low. Likelihood: low. - Mitigation: keep the no-shell fixtures minimal (only `command_available` and - plain `command:` recipes, no `grep`/`fetch`) so the flag cleanly means "no - impure helper ran during selection", and state this scoping honestly in both - the test module and the Hexagonal framing. If a finer-grained, - shell-specific observable is ever required, escalate rather than overloading - `is_impure()`. + is therefore a proxy for "no impure stdlib helper ran during selection", + which is strictly weaker than "the command-execution port specifically was + not driven". Severity: low. Likelihood: low. Mitigation: keep the no-shell + fixtures minimal (only `command_available` and plain `command:` recipes, no + `grep`/`fetch`) so the flag cleanly means "no impure helper ran during + selection", and state this scoping honestly in both the test module and the + Hexagonal framing. If a finer-grained, shell-specific observable is ever + required, escalate rather than overloading `is_impure()`. ## Progress -- [ ] Stage A: confirm interfaces and finalise the gap analysis (no code). +- [ ] Stage A: confirm interfaces and finalize the gap analysis (no code). - [ ] Stage B: add new tests/fixtures in the failing-then-passing discipline with per-test sabotage evidence (completed: none; remaining: all). - [ ] Stage C: documentation updates (users-guide, developers-guide, @@ -179,19 +170,19 @@ escalation, not workarounds. ## Surprises & discoveries - Observation: the `path_override` seam is **publicly reachable**, but - `expand_foreach` is `pub(crate)`. - Evidence: `StdlibConfig::with_path_override(...)` is `pub` - (`src/stdlib/config.rs:240`) and `register_with_config` is `pub` and returns - the `StdlibState`; by contrast `mod which` is private - (`src/stdlib/mod.rs:18`), so `WhichConfig` is unreachable, and - `src/manifest/expand.rs:38` shows `pub(crate) fn expand_foreach`. - Impact: path injection needs **no new seam** — use the public - `with_path_override`/`register_with_config` pairing. The deterministic - real-resolver scenarios (nextest-vs-legacy and no-shell) must still be - **in-crate white-box tests** under `src/manifest/expand_test_cases/`, but the - reason is solely that they call the `pub(crate)` `expand_foreach`, *not* the - path override. The earlier speculative "test-only `WhichConfig` constructor - seam" is dropped as unnecessary. + `expand_foreach` is `pub(crate)`. Evidence: + `StdlibConfig::with_path_override(...)` is `pub` (`src/stdlib/config.rs:240`) + and `register_with_config` is `pub` and returns the `StdlibState`; by contrast + `mod which` is private (`src/stdlib/mod.rs:18`), so `WhichConfig` is + unreachable, and `src/manifest/expand.rs:38` shows + `pub(crate) fn expand_foreach`. Impact: path injection needs **no new seam** + — use the public `with_path_override`/`register_with_config` pairing. The + deterministic real-resolver scenarios (nextest-vs-legacy and no-shell) must + still be **in-crate white-box tests** under + `src/manifest/expand_test_cases/`, but the reason is solely that they call the + `pub(crate)` `expand_foreach`, *not* the path override. The earlier + speculative "test-only `WhichConfig` constructor seam" is dropped as + unnecessary. - Observation: a clean observability seam already exists for "no impure helper ran": `StdlibState::is_impure()` is `pub`, and `shell()`, `grep()`, and @@ -209,83 +200,79 @@ escalation, not workarounds. whole-pipeline purity. - Observation: meaningful coverage already exists and must not be duplicated. - Evidence and inventory are in "Context and orientation". - Impact: 3.14.5 is a *gap-fill*, not a green-field test suite. + Evidence and inventory are in "Context and orientation". Impact: 3.14.5 is a + *gap-fill*, not a green-field test suite. ## Decision log - Decision: Treat 3.14.5 as test-and-docs only; the implementation - (3.14.2–3.14.4) is already complete. - Rationale: the roadmap marks 3.14.2/3.14.3/3.14.4 done; recon confirms the - behaviour is present. The roadmap bullet text is literally "Test ...". - Date/Author: 2026-06-15, planning agent. + (3.14.2–3.14.4) is already complete. Rationale: the roadmap marks + 3.14.2/3.14.3/3.14.4 done; recon confirms the behaviour is present. The + roadmap bullet text is literally "Test …". Date/Author: 2026-06-15, planning + agent. - Decision: Adopt `googletest` in a **confined, spike-gated** form, plus `pretty_assertions` broadly, rather than using googletest throughout. Specifics: `googletest` (`verify_that!`/`assert_that!` with matchers such as `eq`, `len`, `contains`, `unordered_elements_are!`, `not`, `none`) is used **only** in the two in-crate white-box files - (`command_available_selection_cases.rs`, `command_available_no_shell_cases.rs`), - and only after a Stage A spike confirms `#[gtest]`+`#[rstest]` compile and run - under the pinned `rstest` 0.18.0. `tests/` integration and snapshot files keep - the established `anyhow::ensure!`+`insta` idiom. `pretty_assertions` 1.4.x - (drop-in `assert_eq!`/`assert_ne!` shadow; human-facing diffs only, never - used for snapshot comparison) may be used wherever a value-equality diff - improves readability. - Rationale: the brief requests these crates for "clear test semantics", but the - community-of-experts review unanimously flagged that (a) googletest has zero - prior use in this repo (which is uniformly `ensure!`-based), (b) its - `#[gtest]`/`#[rstest]` interop is unverified against the old `rstest` 0.18.0 - pin and is a real compatibility risk, and (c) the 3.14.5 assertions are - trivial. Confining googletest to the two files that most benefit (real-world - matcher ergonomics for the selection/no-shell cases) honours the brief while - containing the inconsistency and interop risk; the Stage A spike with a - defined fallback removes the "assume it works" hazard. - Date/Author: 2026-06-15, planning agent. - **OPEN QUESTION FOR THE APPROVER (see "Open questions"):** the crew's - recommendation was stronger — drop `googletest` entirely and keep only - `pretty_assertions`. This plan keeps googletest (confined) to respect the - brief; the approver may instead elect the crew's recommendation at the - approval gate. + (`command_available_selection_cases.rs`, + `command_available_no_shell_cases.rs`), and only after a Stage A spike + confirms `#[gtest]`+`#[rstest]` compile and run under the pinned `rstest` + 0.26.1. `tests/` integration and snapshot files keep the established + `anyhow::ensure!`+`insta` idiom. `pretty_assertions` 1.4.x (drop-in + `assert_eq!`/`assert_ne!` shadow; human-facing diffs only, never used for + snapshot comparison) may be used wherever a value-equality diff improves + readability. Rationale: the brief requests these crates for "clear test + semantics", but the community-of-experts review unanimously flagged that (a) + googletest has zero prior use in this repo (which is uniformly + `ensure!`-based), (b) its `#[gtest]`/`#[rstest]` interop is unverified against + `rstest` 0.26.1 and is a real compatibility risk, and (c) the 3.14.5 + assertions are trivial. Confining googletest to the two files that most + benefit (real-world matcher ergonomics for the selection/no-shell cases) + honours the brief while containing the inconsistency and interop risk; the + Stage A spike with a defined fallback removes the "assume it works" hazard. + Date/Author: 2026-06-15, planning agent. **OPEN QUESTION FOR THE APPROVER + (see "Open questions"):** the crew's recommendation was stronger — drop + `googletest` entirely and keep only `pretty_assertions`. This plan keeps + googletest (confined) to respect the brief; the approver may instead elect + the crew's recommendation at the approval gate. - Decision: Inject `path_override` via the public - `StdlibConfig::with_path_override`; do not use `WhichConfig`. - Rationale: `mod which` is private, so `WhichConfig` is unreachable from tests; - the public `with_path_override`+`register_with_config` pairing already drives - the real resolver (precedent: `tests/which_diagnostic_snapshot_tests.rs:30`), - needing no new seam. In-crate placement is forced only by `expand_foreach` - being `pub(crate)`. - Date/Author: 2026-06-15, planning agent. + `StdlibConfig::with_path_override`; do not use `WhichConfig`. Rationale: + `mod which` is private, so `WhichConfig` is unreachable from tests; the public + `with_path_override`+`register_with_config` pairing already drives the real + resolver (precedent: `tests/which_diagnostic_snapshot_tests.rs:30`), needing + no new seam. In-crate placement is forced only by `expand_foreach` being + `pub(crate)`. Date/Author: 2026-06-15, planning agent. - Decision: Document, but do not work around, the env-scoped `which` cache. Rationale: the resolver cache is per-`Environment`, not process-global; the new tests build their own environments and need no `#[serial]` and no - `fresh=true`. Recording this prevents future cargo-culted serialisation. + `fresh=true`. Recording this prevents future cargo-culted serialization. Date/Author: 2026-06-15, planning agent. - Decision: Defer property-based and bounded-model coverage of the same - invariants to roadmap 4.2.x (Kani) and 4.3.2 (Proptest for manifest - expansion invariants). - Rationale: 3.14.5 is scoped to example/behavioural/snapshot regression - coverage; 4.3.2 already owns "foreach preserves non-control fields", "when is - removed after evaluation", and "item/index injected". Duplicating that here - would create two owners for one invariant. - Date/Author: 2026-06-15, planning agent. + invariants to roadmap 4.2.x (Kani) and 4.3.2 (Proptest for manifest expansion + invariants). Rationale: 3.14.5 is scoped to example/behavioural/snapshot + regression coverage; 4.3.2 already owns "foreach preserves non-control + fields", "when is removed after evaluation", and "item/index injected". + Duplicating that here would create two owners for one invariant. Date/Author: + 2026-06-15, planning agent. - Decision: `ortho_config` is out of scope for this item. Rationale: `ortho_config` governs layered CLI/configuration precedence (roadmap 3.11.x) and localized help; 3.14.5 concerns manifest-time expansion and lowering, which do not read layered configuration. The brief's mention is - acknowledged and explicitly scoped out to prevent creep. - Date/Author: 2026-06-15, planning agent. + acknowledged and explicitly scoped out to prevent creep. Date/Author: + 2026-06-15, planning agent. - Decision: Place deterministic real-resolver tests in-crate; place the end-to-end deps+Ninja snapshot test in `tests/` using a guaranteed-absent - command for determinism. - Rationale: `WhichConfig`/`expand_foreach` are `pub(crate)` (white-box only); - an absent command resolves to `false` deterministically regardless of host, - so a `tests/`-level snapshot can select the fallback branch reproducibly. - Date/Author: 2026-06-15, planning agent. + command for determinism. Rationale: `WhichConfig`/`expand_foreach` are + `pub(crate)` (white-box only); an absent command resolves to `false` + deterministically regardless of host, so a `tests/`-level snapshot can select + the fallback branch reproducibly. Date/Author: 2026-06-15, planning agent. ## Outcomes & retrospective @@ -299,8 +286,7 @@ This section assumes no prior knowledge of the repository. - **Manifest**: the user's YAML build description (`Netsukefile` / `*.yml`). - **Typed AST**: the deserialized Rust representation of a manifest - (`src/ast.rs`: `NetsukeManifest`, `Target`, `Rule`, `Recipe`, - `StringOrList`). + (`src/ast.rs`: `NetsukeManifest`, `Target`, `Rule`, `Recipe`, `StringOrList`). - **Manifest-time expansion**: evaluation of `foreach`/`when` on the *raw* manifest value before the typed AST exists (`src/manifest/expand.rs`). - **IR (intermediate representation)**: the build graph @@ -308,34 +294,31 @@ This section assumes no prior knowledge of the repository. - **Implicit dependency**: a `deps` entry that affects rebuild/ordering but is not passed to the recipe as `$in`; emitted in Ninja after a `|` separator. - **`command_available(name, **kwargs)`**: a non-throwing Jinja function that - returns `true`/`false` for executable presence - (`src/stdlib/which/mod.rs`). + returns `true`/`false` for executable presence (`src/stdlib/which/mod.rs`). - **`shell()`**: an *impure* Jinja filter that runs a command during rendering and sets the stdlib `impure` flag (`src/stdlib/command/`). ### Pipeline (data flow) -`manifest::from_str/from_path` -→ parse YAML to a raw `ManifestValue` -→ register the stdlib into a Jinja `Environment` (this is where - `command_available` and `shell` are bound), capturing a `StdlibState` -→ `expand::expand_foreach(&mut doc, &env)` removes `when:false` entries and - fans out `foreach`, injecting `item`/`index` -→ deserialize to the typed AST (`deserialize_actions` stamps `phony: true`) -→ `render::render_manifest` evaluates remaining string templates -→ `ir::BuildGraph::from_manifest` lowers `sources`→`inputs`, - `deps`→`implicit_deps`, `order_only_deps`→`order_only_deps`, and runs cycle - detection over `inputs` + `implicit_deps` -→ `ninja_gen::generate` formats `build` lines as - `build [| ]: [| ] [|| ]`. +`manifest::from_str/from_path` → parse YAML to a raw `ManifestValue` → register +the stdlib into a Jinja `Environment` (this is where `command_available` and +`shell` are bound), capturing a `StdlibState` → +`expand::expand_foreach(&mut doc, &env)` removes `when:false` entries and fans +out `foreach`, injecting `item`/`index` → deserialize to the typed AST +(`deserialize_actions` stamps `phony: true`) → `render::render_manifest` +evaluates remaining string templates → `ir::BuildGraph::from_manifest` lowers +`sources`→`inputs`, `deps`→`implicit_deps`, `order_only_deps`→ +`order_only_deps`, and runs cycle detection over `inputs` + `implicit_deps` → +`ninja_gen::generate` formats `build` lines as +`build [| ]: [| ] [|| ]`. Key files and symbols (full paths relative to repo root): - `src/ast.rs`: `Target` (fields `sources`, `deps`, `order_only_deps`, `phony`), `Rule.deps`, `StringOrList`, `deserialize_actions`. - `src/manifest/expand.rs`: `expand_foreach` (`pub(crate)`), - `expand_section`, `expand_target`, `when_allows`, `eval_when`, - `when_context`, `inject_iteration_vars`, `FilteringStats`. + `expand_section`, `expand_target`, `when_allows`, `eval_when`, `when_context`, + `inject_iteration_vars`, `FilteringStats`. - `src/manifest/mod.rs`: `from_str`, `from_path`, `from_str_named` (takes `stdlib_config: Option`). - `src/stdlib/mod.rs`: `StdlibState::is_impure` / `reset_impure` (`pub`). @@ -344,13 +327,14 @@ Key files and symbols (full paths relative to repo root): `command_available_with`, `is_command_available`. - `src/ir/from_manifest.rs`: `BuildGraph::from_manifest`, `process_targets` (`to_paths(&target.sources)`→inputs, `to_paths(&target.deps)`→implicit_deps). -- `src/ir/graph.rs`: `BuildEdge { inputs, implicit_deps, order_only_deps, ... }`. +- `src/ir/graph.rs`: + `BuildEdge { inputs, implicit_deps, order_only_deps, ... }`. - `src/ninja_gen.rs`: `generate`, `generate_into`, `DisplayEdge` (the `|`/`||` formatting). ### Existing coverage (do not duplicate; extend or complement) -- `src/manifest/expand_test_cases/condition_cases.rs`: parameterised +- `src/manifest/expand_test_cases/condition_cases.rs`: parameterized `targets`/`actions` cases for `when:false` removal before typed AST, `foreach` iteration-var injection, static action `when` dropping, sequence `foreach`, truthiness table, invalid-`when` errors, and **complementary @@ -377,9 +361,9 @@ Key files and symbols (full paths relative to repo root): integrated regression fixture exercising an action that carries `foreach`, `when`, *and* `deps` together, end-to-end to Ninja. 2. **Complementary nextest/legacy branches select exactly one action** — not - covered. The existing complementary tests are stubbed and use generic - names. New: a real-resolver, nextest-vs-legacy scenario proving exactly one - action survives in both the present and absent worlds. + covered. The existing complementary tests are stubbed and use generic names. + New: a real-resolver, nextest-vs-legacy scenario proving exactly one action + survives in both the present and absent worlds. 3. **Absent-command fallback without invoking `shell()`** — not covered. New: assert `StdlibState::is_impure() == false` after selecting the fallback. 4. **`deps` lowering for conditional actions, in IR and Ninja** — partially @@ -411,7 +395,7 @@ an escalation, not an overload of `is_impure()`.) ## Plan of work -### Stage A — confirm interfaces, run the interop spike, finalise gap analysis +### Stage A — confirm interfaces, run the interop spike, finalize gap analysis This stage adds **one throwaway spike** (immediately reverted) and otherwise changes no code. @@ -437,10 +421,11 @@ changes no code. 2. **Confirm the injection seam (public, no new seam).** Verify `crate::stdlib::register_with_config(&mut Environment, StdlibConfig) - -> anyhow::Result` and that `StdlibConfig::with_path_override` - threads into the resolver (`src/stdlib/register.rs:104`). Mirror the - precedent in `tests/which_diagnostic_snapshot_tests.rs:30`. Do **not** use - `WhichConfig` (private module). + -> anyhow::Result` + and that `StdlibConfig::with_path_override` threads into the resolver + (`src/stdlib/register.rs:104`). Mirror the precedent in + `tests/which_diagnostic_snapshot_tests.rs:30`. Do **not** use `WhichConfig` + (private module). 3. **Confirm `expand_foreach` drives selection** in a white-box test with the *real* `command_available` registered (existing cases use a stubbed @@ -469,16 +454,17 @@ corresponds to the guarded behaviour, e.g. "expected len 1, got 2", or this proves the test fails for the *right reason*, which a bare pass/fail toggle does not. The sabotage diff is reverted with an explicit `git checkout -- ` (recorded so a resumed agent can confirm a clean tree) -and is never committed; the transcript is recorded under "Artifacts and notes". +and is never committed; the transcript is recorded under "Artefacts and notes". This is the documented substitute for the red stage (see the execplans guidance on observable substitutes). Add the following, smallest first: 1. **In-crate: nextest-vs-legacy exactly-one-action (real resolver).** - New file `src/manifest/expand_test_cases/command_available_selection_cases.rs` - (wired into the `expand_test_cases` module). An `#[rstest]` parameterised - over two worlds, driving the *real* `command_available` (registered via + New file + `src/manifest/expand_test_cases/command_available_selection_cases.rs` (wired + into the `expand_test_cases` module). An `#[rstest]` parameterized over two + worlds, driving the *real* `command_available` (registered via `register_with_config` with a `StdlibConfig`): - *present*: a `tempfile::TempDir` containing a fake `cargo-nextest` executable, injected via `StdlibConfig::with_path_override`. Reuse the @@ -493,15 +479,16 @@ Add the following, smallest first: guaranteed-absent command name + `cwd_mode="never"`. Expect exactly the `run-tests-legacy` action (`command: cargo test`) to survive. Drive `expand_foreach`, then assert (googletest, per Stage A spike): - `verify_that!(actions, len(eq(1)))?`, the surviving action's `name`/`command` - via `matches_pattern!`/`eq`, and that `when` was removed. - This also explicitly pins **bullet 1** for actions: include one case where - the surviving action additionally carries a `foreach` so the assertion - covers action-level fan-out and `when` together (complementing, not - duplicating, `condition_cases.rs`). + `verify_that!(actions, len(eq(1)))?`, the surviving action's `name`/ + `command` via `matches_pattern!`/`eq`, and that `when` was removed. This + also explicitly pins **bullet 1** for actions: include one case where the + surviving action additionally carries a `foreach` so the assertion covers + action-level fan-out and `when` together (complementing, not duplicating, + `condition_cases.rs`). 2. **In-crate: absent fallback without `shell()`.** - New file `src/manifest/expand_test_cases/command_available_no_shell_cases.rs`. + New file + `src/manifest/expand_test_cases/command_available_no_shell_cases.rs`. Register the full stdlib (so `command_available`, `shell`, `grep`, `fetch` all exist), capture `StdlibState`, expand a manifest whose two actions use `command_available("")` / `not command_available(...)` @@ -518,39 +505,40 @@ Add the following, smallest first: skip-driven hole). 3. **Integration + IR: conditional action carries `deps` into `implicit_deps`, - including the `item`-in-`deps` interaction.** - New fixture `tests/data/conditional_action_deps.yml`: a target and an action, - each selected via a complementary `command_available` pair using a + including the `item`-in-`deps` interaction.** New fixture + `tests/data/conditional_action_deps.yml`: a target and an action, each + selected via a complementary `command_available` pair using a guaranteed-absent command (`cwd_mode="never"` retained) so the fallback branch is deterministic, each carrying `sources`, `deps`, and `order_only_deps`. Crucially, the action uses `foreach` and interpolates `{{ item }}` (or `{{ index }}`) into **one `deps` entry** (e.g. `deps: [build/{{ item }}.o]`) — the per-item implicit-dependency case is the single most likely real-world regression and is currently untested anywhere. - **Add the new tests to the existing `tests/ir_from_manifest_tests.rs`** (beside - the static-deps cases `manifest_deps_populate_implicit_deps` / - `manifest_deps_do_not_contribute_to_recipe_inputs`, keeping all deps-lowering - tests in one place). Assert that each selected edge has `implicit_deps` equal - (unordered) to the declared/substituted `deps`, `inputs` equal to `sources` - only, `order_only_deps` carried through, the expected per-`item` - substitution, and that none of the *unselected* branch's paths appear in any - edge. (Cycle detection over `implicit_deps` is already owned by - `src/ir/cycle.rs` unit tests and is not re-tested here.) + **Add the new tests to the existing `tests/ir_from_manifest_tests.rs`** + (beside the static-deps cases `manifest_deps_populate_implicit_deps` / + `manifest_deps_do_not_contribute_to_recipe_inputs`, keeping all + deps-lowering tests in one place). Assert that each selected edge has + `implicit_deps` equal (unordered) to the declared/substituted `deps`, + `inputs` equal to `sources` only, `order_only_deps` carried through, the + expected per-`item` substitution, and that none of the *unselected* branch's + paths appear in any edge. (Cycle detection over `implicit_deps` is already + owned by `src/ir/cycle.rs` unit tests and is not re-tested here.) 4. **Snapshot + real-ninja: conditional action deps reach the Ninja file.** New test `conditional_action_deps_ninja_snapshot` **added to `tests/ninja_snapshot_tests.rs`** using the fixture from B.3. Assert the - selected `build` line contains `| ` (implicit) **and** the `|| ` - segment (make `order_only_deps` definitely present in the fixture so this is - not "where applicable"); assert the unselected branch's outputs/deps are - absent; then `insta::assert_snapshot!` into `tests/snapshots/ninja/` reusing - the existing `Settings::set_snapshot_path`. Use `insta::assert_snapshot!` - only for the rendered Ninja text — never `pretty_assertions::assert_eq!`. - Where `ninja`/`python3` are available, also run `ninja -t query`/`-n` to - prove the file is valid and reaches a no-op second pass, mirroring - `touch_manifest_ninja_validation`; surface any skip via `eprintln!` (captured - by the harness) in addition to `tracing::warn!` so a binary-less CI run does - not silently validate nothing (see Risks / Doggylump S2). + selected `build` line contains `| ` (implicit) **and** the + `|| ` segment (make `order_only_deps` definitely present in the + fixture so this is not "where applicable"); assert the unselected branch's + outputs/deps are absent; then `insta::assert_snapshot!` into + `tests/snapshots/ninja/` reusing the existing `Settings::set_snapshot_path`. + Use `insta::assert_snapshot!` only for the rendered Ninja text — never + `pretty_assertions::assert_eq!`. Where `ninja`/`python3` are available, also + run `ninja -t query`/`-n` to prove the file is valid and reaches a no-op + second pass, mirroring `touch_manifest_ninja_validation`; surface any skip + via `eprintln!` (captured by the harness) in addition to `tracing::warn!` so + a binary-less CI run does not silently validate nothing (see Risks / + Doggylump S2). 5. **BDD: the combined conditional-action-with-deps scenario only.** The unit (B.1), IR (B.3), and snapshot+real-ninja (B.4) layers already pin @@ -558,13 +546,13 @@ Add the following, smallest first: (`tests/features/manifest.feature:132`, `tests/features/ninja.feature:31`) already cover fallback-selection and deps-emission *separately*. To avoid redundant step-matcher maintenance, B.5 adds **only** the genuinely new - externally-observable case: a single scenario where a *conditionally-selected* - action's `deps` appear as Ninja implicit dependencies end-to-end. Reuse - existing steps in `tests/bdd/steps/conditional_manifest.rs` and - `tests/bdd/steps/ninja.rs`; add at most one new `Then` (the selected action - exposes a given implicit dependency) if no existing step fits. If even this - is fully covered by reframing an existing scenario, prefer reframing over a - new scenario. + externally-observable case: a single scenario where a + *conditionally-selected* action's `deps` appear as Ninja implicit + dependencies end-to-end. Reuse existing steps in + `tests/bdd/steps/conditional_manifest.rs` and `tests/bdd/steps/ninja.rs`; + add at most one new `Then` (the selected action exposes a given implicit + dependency) if no existing step fits. If even this is fully covered by + reframing an existing scenario, prefer reframing over a new scenario. Each of B.1–B.5 ends with the focused test passing, its sabotage check recorded, and the relevant gate (`make test`) green. @@ -581,13 +569,13 @@ recorded, and the relevant gate (`make test`) green. `path_override`/`cwd_mode` pattern for deterministic `command_available` tests, and the `is_impure()`-as-boundary-proxy idiom. - Component architecture doc for the manifest/stdlib boundary: record the - port framing (executable-discovery vs command-execution) and that 3.14.5 - pins it. If the decision to standardise the boundary assertion is judged + port framing (executable-discovery vs command-execution) and that 3.14.5 pins + it. If the decision to standardize the boundary assertion is judged substantive, capture it as an ADR using the `arch-decision-records` Y-Statement format and reference it from this plan and the design doc. - `docs/roadmap.md`: tick 3.14.5 and its four sub-bullets on completion. -### Stage D — gates, review, finalise +### Stage D — gates, review, finalize Run the full gate suite, then `coderabbit review --agent`; clear all concerns before declaring done. Update Progress/Outcomes. @@ -661,10 +649,10 @@ Quality criteria ("done"): All steps are additive and re-runnable. Snapshots are created deliberately via `cargo insta` and reviewed before acceptance; a wrong snapshot is corrected by re-reviewing, not by force-accepting. Sabotage checks are always reverted -(`git checkout -- `) and never committed. Commit after each of B.1–B.5 -so any step can be rolled back independently. +(`git checkout -- `) and never committed. Commit after each of B.1–B.5 so +any step can be rolled back independently. -## Artifacts and notes +## Artefacts and notes Record here, as work proceeds: the exact surviving-action assertions, the generated Ninja snapshot for the conditional-deps fixture, and the sabotage @@ -673,7 +661,7 @@ each new test is non-vacuous. ## Interfaces and dependencies -New `[dev-dependencies]` in `Cargo.toml` (pre-authorised by the brief): +New `[dev-dependencies]` in `Cargo.toml` (pre-authorized by the brief): ```toml # Cargo.toml @@ -681,7 +669,7 @@ googletest = "0.14" pretty_assertions = "1.4" ``` -Existing crates reused: `rstest` 0.18, `rstest-bdd`/`rstest-bdd-macros` 0.5, +Existing crates reused: `rstest` 0.26, `rstest-bdd`/`rstest-bdd-macros` 0.5, `insta` 1 (yaml), `tempfile`, `test_support`, `anyhow` (`ensure!`). (`serial_test` is **not** needed — the new tests build their own environments and inject `path_override`, so they never touch process-global PATH; see the @@ -690,17 +678,17 @@ env-scoped-cache Decision.) Seams relied upon (all already public/`pub(crate)` and present; **no new seam required**): -- `crate::manifest::expand::expand_foreach(&mut ManifestValue, &Environment) - -> anyhow::Result` (`pub(crate)` — forces in-crate placement - of the selection/no-shell tests). -- `crate::stdlib::register_with_config(&mut Environment, StdlibConfig) - -> anyhow::Result` (`pub`; note the `Result`), +- `crate::manifest::expand::expand_foreach` accepts a mutable `ManifestValue` + and an `Environment`, then returns `anyhow::Result`. It is + `pub(crate)`, which forces in-crate placement of the selection/no-shell tests. +- `crate::stdlib::register_with_config(&mut Environment, StdlibConfig) -> anyhow::Result` + (`pub`; note the `Result`), `StdlibConfig::with_path_override(impl Into)` (`pub`, `src/stdlib/config.rs:240`), and `StdlibState::is_impure(&self) -> bool` (`pub`). Do **not** reference `WhichConfig` (private module). -- `netsuke::ir::BuildGraph::from_manifest(&NetsukeManifest) - -> Result` and `netsuke::ninja_gen::generate(&BuildGraph) - -> Result` (`pub`) for the integration/snapshot tests. +- `netsuke::ir::BuildGraph::from_manifest` and + `netsuke::ninja_gen::generate(&BuildGraph) -> Result` + are public seams for the integration/snapshot tests. New/changed test files and fixtures (final names to be confirmed in Stage A): @@ -711,7 +699,8 @@ New/changed test files and fixtures (final names to be confirmed in Stage A): - additions to existing `tests/ir_from_manifest_tests.rs` (deps lowering) and `tests/ninja_snapshot_tests.rs` (Ninja snapshot + real-ninja) — kept beside their static-deps counterparts, not in a separate file. -- new `tests/snapshots/ninja/ninja_snapshot_tests__conditional_action_deps_ninja.snap`. +- new + `tests/snapshots/ninja/ninja_snapshot_tests__conditional_action_deps_ninja.snap`. - one new combined scenario in `tests/features/ninja.feature` (or `manifest.feature`) plus at most one new step. - new `[dev-dependencies]` entries `googletest = "0.14"` and @@ -720,26 +709,24 @@ New/changed test files and fixtures (final names to be confirmed in Stage A): ## Open questions for the approver 1. **googletest.** The brief mandates googletest; the community-of-experts - review unanimously recommended dropping it (keeping only `pretty_assertions`) - because it has no prior use here, its `#[gtest]`+`#[rstest]` interop is - unverified under `rstest` 0.18.0, and the assertions are trivial. This plan - keeps googletest in a confined, spike-gated form to respect the brief. The - approver may instead choose: (a) confined + spike-gated (as planned), - (b) `pretty_assertions` only, no googletest, or (c) googletest throughout. + review unanimously recommended dropping it (keeping only + `pretty_assertions`) because it has no prior use here, its `#[gtest]`+ + `#[rstest]` interop is unverified under `rstest` 0.26.1, and the assertions + are trivial. This plan keeps googletest in a confined, spike-gated form to + respect the brief. The approver may instead choose: (a) confined + + spike-gated (as planned), (b) `pretty_assertions` only, no googletest, or + (c) googletest throughout. 2. **BDD scope.** Reviewers judged a full BDD layer largely redundant; the plan reduces B.5 to the single combined scenario. Confirm this is the desired depth, or request fuller BDD coverage. -3. **Stray file.** `src/stdlib/command/mod_backup.rs` is an unreferenced ~41 KB - backup duplicating impure-flag logic. Out of scope for 3.14.5, but flagged - for separate cleanup so future grep-based reasoning is not misled. ## Signposted documentation and skills Documentation to consult while implementing: - `docs/netsuke-design.md` §2.5 (manifest-time `foreach`/`when` semantics), - §2.4 and §5.3 (dependency classes and Ninja lowering), - and the "executable discovery" section (`command_available`). + §2.4 and §5.3 (dependency classes and Ninja lowering), and the "executable + discovery" section (`command_available`). - `docs/rust-testing-with-rstest-fixtures.md` — fixture and `#[case]` patterns. - `docs/reliable-testing-in-rust-via-dependency-injection.md` — the `path_override`/resolver-injection approach used for deterministic @@ -773,7 +760,7 @@ Skills to load while implementing: `expand_foreach` being `pub(crate)`. - Restated the `is_impure()` boundary proxy honestly: it flips for `shell()`, `grep()`, *and* `fetch()`; downgraded the hexagonal claim accordingly. - - Added a Stage A `#[gtest]`+`#[rstest]` interop spike (rstest 0.18.0 risk) + - Added a Stage A `#[gtest]`+`#[rstest]` interop spike (rstest 0.26.1 risk) with a defined fallback; confined googletest to the two white-box files and surfaced the crew's "drop googletest" recommendation as an approver choice. - Hardened determinism: the absent case now requires empty `path_override` + @@ -791,3 +778,8 @@ Skills to load while implementing: Effect on remaining work: scope and file count are essentially unchanged; the plan is now anchored to reachable seams and verifiable assumptions. No code has been written; the plan remains in DRAFT pending approval. + +- 2026-07-28 — Rebased onto `origin/main`. Updated the plan for the repository's + `rstest` 0.26.1 upgrade and removed the resolved `mod_backup.rs` cleanup + question because that file was deleted upstream. No implementation scope or + architectural boundary changed. From 049ff59d798a54b99b9326a51365e88a47d38d48 Mon Sep 17 00:00:00 2001 From: leynos Date: Wed, 29 Jul 2026 00:09:32 +0200 Subject: [PATCH 07/34] Validate assertion tooling for regression tests Add the approved googletest and pretty-assertions development dependencies. Record the successful googletest/rstest interoperability spike and the confirmed test seams in the living execution plan before implementation. --- Cargo.toml | 2 + ...conditional-action-dependency-manifests.md | 110 ++++++++++++------ 2 files changed, 78 insertions(+), 34 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2fc6d6400..3b9488ec1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,6 +149,8 @@ thiserror = "1" time = { version = "0.3.44", features = ["formatting"] } tracing = "0.1" [dev-dependencies] +googletest = "0.14" +pretty_assertions = "1.4" rstest = "0.26.1" rstest-bdd = "0.5.0" rstest-bdd-macros = { version = "0.5.0", features = ["strict-compile-time-validation"] } diff --git a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md index c6f6bd395..c6b76e9ca 100644 --- a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md +++ b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md @@ -4,7 +4,7 @@ This ExecPlan (execution plan) is a living document. The sections `Constraints`, `Tolerances`, `Risks`, `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. -Status: DRAFT +Status: IN PROGRESS ## Purpose / big picture @@ -84,12 +84,12 @@ escalation, not workarounds. - Dependencies: adding `googletest` and `pretty_assertions` as `[dev-dependencies]` is pre-authorized by the task brief (see Decision Log). Use of `googletest` is **confined to the two in-crate white-box test files** - and is **gated on a Stage A interop spike** (`#[gtest]`+`#[rstest]` under the - pinned `rstest` 0.26.1); if the spike fails, fall back to bare - `verify_that!(...)?` returning `googletest::Result<()>` without `#[gtest]`, - or to `anyhow::ensure!`. `tests/` integration and snapshot files stay on the - existing `ensure!`+`insta` idiom. Any *other* new dependency triggers - escalation. + and is **gated on a Stage A interop spike** (`#[googletest::test]`+ + `#[rstest]` under the pinned `rstest` 0.26.1); if the spike fails, fall back + to bare `verify_that!(...)?` returning `googletest::Result<()>` without the + googletest test attribute, or to `anyhow::ensure!`. `tests/` integration and + snapshot files stay on the existing `ensure!`+`insta` idiom. Any *other* new + dependency triggers escalation. - Iterations: if a new test cannot be made to pass after 3 focused attempts and the production behaviour appears correct, stop and escalate with the evidence (the test may be encoding a wrong expectation). @@ -158,7 +158,9 @@ escalation, not workarounds. ## Progress -- [ ] Stage A: confirm interfaces and finalize the gap analysis (no code). +- [x] (2026-07-28 22:03Z) Stage A: confirmed interfaces, dependency ordering, + and googletest/rstest interoperability; retained only the approved dev + dependencies after reverting the throwaway spike. - [ ] Stage B: add new tests/fixtures in the failing-then-passing discipline with per-test sabotage evidence (completed: none; remaining: all). - [ ] Stage C: documentation updates (users-guide, developers-guide, @@ -203,6 +205,18 @@ escalation, not workarounds. Evidence and inventory are in "Context and orientation". Impact: 3.14.5 is a *gap-fill*, not a green-field test suite. +- Observation: `#[googletest::test]` and `#[rstest]` interoperate under the + current dependency set when the googletest attribute comes first. Evidence: + the throwaway Stage A spike ran exactly one generated case and reported + `1 passed; 0 failed; 436 filtered out`. Impact: use both attributes in the + two planned white-box test modules; do not use the shorter speculative + `#[gtest]` spelling from the draft plan. + +- Observation: `DisplayEdge::fmt` preserves the stored vector order while + emitting explicit inputs, implicit dependencies, then order-only + dependencies. Evidence: `src/ninja_gen.rs:286-304`. Impact: keep fixture + dependency lists deliberately ordered so the new snapshot is deterministic. + ## Decision log - Decision: Treat 3.14.5 as test-and-docs only; the implementation @@ -218,25 +232,25 @@ escalation, not workarounds. **only** in the two in-crate white-box files (`command_available_selection_cases.rs`, `command_available_no_shell_cases.rs`), and only after a Stage A spike - confirms `#[gtest]`+`#[rstest]` compile and run under the pinned `rstest` - 0.26.1. `tests/` integration and snapshot files keep the established + confirms `#[googletest::test]`+`#[rstest]` compile and run under the pinned + `rstest` 0.26.1. `tests/` integration and snapshot files keep the established `anyhow::ensure!`+`insta` idiom. `pretty_assertions` 1.4.x (drop-in `assert_eq!`/`assert_ne!` shadow; human-facing diffs only, never used for snapshot comparison) may be used wherever a value-equality diff improves readability. Rationale: the brief requests these crates for "clear test semantics", but the community-of-experts review unanimously flagged that (a) googletest has zero prior use in this repo (which is uniformly - `ensure!`-based), (b) its `#[gtest]`/`#[rstest]` interop is unverified against - `rstest` 0.26.1 and is a real compatibility risk, and (c) the 3.14.5 - assertions are trivial. Confining googletest to the two files that most - benefit (real-world matcher ergonomics for the selection/no-shell cases) - honours the brief while containing the inconsistency and interop risk; the - Stage A spike with a defined fallback removes the "assume it works" hazard. - Date/Author: 2026-06-15, planning agent. **OPEN QUESTION FOR THE APPROVER - (see "Open questions"):** the crew's recommendation was stronger — drop - `googletest` entirely and keep only `pretty_assertions`. This plan keeps - googletest (confined) to respect the brief; the approver may instead elect - the crew's recommendation at the approval gate. + `ensure!`-based), (b) its googletest/rstest interop was unverified against + `rstest` 0.26.1 before Stage A, and (c) the 3.14.5 assertions are trivial. + Confining googletest to the two files that most benefit (real-world matcher + ergonomics for the selection/no-shell cases) honours the brief while + containing the inconsistency and interop risk; the Stage A spike with a + defined fallback removes the "assume it works" hazard. Date/Author: + 2026-06-15, planning agent. **OPEN QUESTION FOR THE APPROVER (see "Open + questions"):** the crew's recommendation was stronger — drop `googletest` + entirely and keep only `pretty_assertions`. This plan keeps googletest + (confined) to respect the brief; the approver may instead elect the crew's + recommendation at the approval gate. - Decision: Inject `path_override` via the public `StdlibConfig::with_path_override`; do not use `WhichConfig`. Rationale: @@ -274,9 +288,18 @@ escalation, not workarounds. deterministically regardless of host, so a `tests/`-level snapshot can select the fallback branch reproducibly. Date/Author: 2026-06-15, planning agent. +- Decision: Use `#[googletest::test]` immediately before `#[rstest]` in the + confined white-box tests. Rationale: the Stage A spike proved the ordering + registers each rstest case exactly once with googletest 0.14.3 and rstest + 0.26.1, matching the documented interoperability contract supplied by the + approver. Date/Author: 2026-07-28, implementation agent. + ## Outcomes & retrospective -To be completed at milestone boundaries and at completion. +Stage A completed without requiring a new seam or changing production code. The +approved dev dependencies resolve to googletest 0.14.3 and pretty_assertions +1.4.1. The real resolver injection, expansion entry point, and deterministic +Ninja dependency ordering all remain available as planned. ## Context and orientation @@ -407,17 +430,17 @@ changes no code. // scratch, never committed use googletest::prelude::*; use rstest::rstest; - #[gtest] + #[googletest::test] #[rstest] #[case(1)] fn spike(#[case] n: i32) -> googletest::Result<()> { verify_that!(n, eq(1)) } ``` - Run it. If it compiles and passes under `rstest` 0.18.0, proceed with the + Run it. If it compiles and passes under `rstest` 0.26.1, proceed with the confined googletest approach. If it fails, adopt the fallback (bare - `verify_that!(...)?` in a plain `#[rstest] -> googletest::Result<()>` without - `#[gtest]`, or `ensure!`), and record the outcome in the Decision Log. - Revert the spike before any milestone commit. + `verify_that!(...)?` in a plain `#[rstest] -> googletest::Result<()>` + without the googletest test attribute, or `ensure!`), and record the outcome + in the Decision Log. Revert the spike before any milestone commit. 2. **Confirm the injection seam (public, no new seam).** Verify `crate::stdlib::register_with_config(&mut Environment, StdlibConfig) @@ -659,6 +682,18 @@ generated Ninja snapshot for the conditional-deps fixture, and the sabotage transcripts (broken-line diff + failing test name + restored pass) that prove each new test is non-vacuous. +Stage A interoperability evidence: + +```plaintext +running 1 test +...googletest_runs_each_rstest_case_once::case_1 ... ok +test result: ok. 1 passed; 0 failed; 436 filtered out +``` + +The throwaway module and its wiring were removed immediately after this run. +The complete transcript is in +`/tmp/stage-a-googletest-rstest-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out`. + ## Interfaces and dependencies New `[dev-dependencies]` in `Cargo.toml` (pre-authorized by the brief): @@ -710,12 +745,12 @@ New/changed test files and fixtures (final names to be confirmed in Stage A): 1. **googletest.** The brief mandates googletest; the community-of-experts review unanimously recommended dropping it (keeping only - `pretty_assertions`) because it has no prior use here, its `#[gtest]`+ - `#[rstest]` interop is unverified under `rstest` 0.26.1, and the assertions - are trivial. This plan keeps googletest in a confined, spike-gated form to - respect the brief. The approver may instead choose: (a) confined + - spike-gated (as planned), (b) `pretty_assertions` only, no googletest, or - (c) googletest throughout. + `pretty_assertions`) because it had no prior use here and the assertions are + trivial. Stage A has now verified `#[googletest::test]`+`#[rstest]` + interoperability under `rstest` 0.26.1. This plan keeps googletest in a + confined form to respect the brief. The approver may instead choose: (a) + confined + spike-gated (as planned), (b) `pretty_assertions` only, no + googletest, or (c) googletest throughout. 2. **BDD scope.** Reviewers judged a full BDD layer largely redundant; the plan reduces B.5 to the single combined scenario. Confirm this is the desired depth, or request fuller BDD coverage. @@ -760,7 +795,8 @@ Skills to load while implementing: `expand_foreach` being `pub(crate)`. - Restated the `is_impure()` boundary proxy honestly: it flips for `shell()`, `grep()`, *and* `fetch()`; downgraded the hexagonal claim accordingly. - - Added a Stage A `#[gtest]`+`#[rstest]` interop spike (rstest 0.26.1 risk) + - Added a Stage A `#[googletest::test]`+`#[rstest]` interop spike (rstest + 0.26.1 risk) with a defined fallback; confined googletest to the two white-box files and surfaced the crew's "drop googletest" recommendation as an approver choice. - Hardened determinism: the absent case now requires empty `path_override` + @@ -783,3 +819,9 @@ Skills to load while implementing: `rstest` 0.26.1 upgrade and removed the resolved `mod_backup.rs` cleanup question because that file was deleted upstream. No implementation scope or architectural boundary changed. + +- 2026-07-28 — Marked the approved plan in progress and recorded Stage A. + Confirmed the existing seams, deterministic dependency emission order, and + googletest/rstest interoperability with the approver-specified attribute + order. Remaining work starts at Stage B.1; no production change or new seam + is required. From e5067caec3092b4985d029e627a1c54e2c61907f Mon Sep 17 00:00:00 2001 From: leynos Date: Wed, 29 Jul 2026 00:43:11 +0200 Subject: [PATCH 08/34] Test conditional action selection Exercise the real command resolver with deterministic present and absent tool worlds. Verify complementary branches retain exactly one action and that action-level foreach expansion removes its control fields. Record the focused sabotage evidence and implementation findings in the living ExecPlan. --- ...conditional-action-dependency-manifests.md | 40 +++- .../command_available_selection_cases.rs | 172 ++++++++++++++++++ src/manifest/expand_tests.rs | 3 + 3 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 src/manifest/expand_test_cases/command_available_selection_cases.rs diff --git a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md index c6b76e9ca..d7536be91 100644 --- a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md +++ b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md @@ -162,7 +162,7 @@ escalation, not workarounds. and googletest/rstest interoperability; retained only the approved dev dependencies after reverting the throwaway spike. - [ ] Stage B: add new tests/fixtures in the failing-then-passing discipline - with per-test sabotage evidence (completed: none; remaining: all). + with per-test sabotage evidence (completed: B.1; remaining: B.2–B.5). - [ ] Stage C: documentation updates (users-guide, developers-guide, component architecture, ADR if warranted). - [ ] Stage D: full gate run, CodeRabbit review, roadmap tick. @@ -217,6 +217,14 @@ escalation, not workarounds. dependencies. Evidence: `src/ninja_gen.rs:286-304`. Impact: keep fixture dependency lists deliberately ordered so the new snapshot is deterministic. +- Observation: googletest's `OrFail` trait is the intended bridge from fixture + setup errors into `googletest::Result`. Evidence: the first B.1 compile + attempt could not convert `TestAssertionFailure` into `anyhow::Error`; + changing the test return type to `googletest::Result<()>` and applying + `.or_fail()?` to setup operations preserved native matcher failures and + passed both parameterized cases. Impact: use the same error boundary in B.2 + instead of flattening matcher diagnostics into `anyhow`. + ## Decision log - Decision: Treat 3.14.5 as test-and-docs only; the implementation @@ -301,6 +309,11 @@ approved dev dependencies resolve to googletest 0.14.3 and pretty_assertions 1.4.1. The real resolver injection, expansion entry point, and deterministic Ninja dependency ordering all remain available as planned. +B.1 now pins complementary action selection through the real resolver in both +deterministic worlds. The present case also combines action `foreach` with +`when`; both cases assert exactly one action survives and that expansion +removes control fields. No production seam or semantic change was required. + ## Context and orientation This section assumes no prior knowledge of the repository. @@ -694,6 +707,26 @@ The throwaway module and its wiring were removed immediately after this run. The complete transcript is in `/tmp/stage-a-googletest-rstest-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out`. +Stage B.1 real-resolver and sabotage evidence: + +```plaintext +passing baseline: +2 passed; 0 failed; 436 filtered out + +temporary sabotage: +is_command_available(NotFound | DirectNotFound): Ok(false) -> Ok(true) +case_2_nextest_absent failed at the surviving-action name assertion: +expected "run-tests-legacy" +actual "run-tests-nextest" +1 passed; 1 failed; 436 filtered out +``` + +The production line was restored immediately with an explicit inverse patch; +`git diff -- src/stdlib/which/mod.rs` was empty afterwards. Transcripts: +`/tmp/b1-focused-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out` +and +`/tmp/b1-sabotage-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out`. + ## Interfaces and dependencies New `[dev-dependencies]` in `Cargo.toml` (pre-authorized by the brief): @@ -825,3 +858,8 @@ Skills to load while implementing: googletest/rstest interoperability with the approver-specified attribute order. Remaining work starts at Stage B.1; no production change or new seam is required. + +- 2026-07-28 — Completed Stage B.1 implementation and its non-vacuity check. + Added deterministic present/absent real-resolver cases, including the + action-level `foreach` interaction, and recorded the assertion-level sabotage + failure. Remaining work starts at Stage B.2. diff --git a/src/manifest/expand_test_cases/command_available_selection_cases.rs b/src/manifest/expand_test_cases/command_available_selection_cases.rs new file mode 100644 index 000000000..81e4c0d33 --- /dev/null +++ b/src/manifest/expand_test_cases/command_available_selection_cases.rs @@ -0,0 +1,172 @@ +//! Real-resolver coverage for complementary action selection. + +use std::ffi::OsString; + +use anyhow::{Context, Result, anyhow}; +use camino::{Utf8Path, Utf8PathBuf}; +use cap_std::{ambient_authority, fs_utf8::Dir}; +use googletest::prelude::*; +use minijinja::Environment; +use rstest::rstest; + +use super::{actions, expand_foreach}; +use crate::{ + manifest::ManifestValue, + stdlib::{self, StdlibConfig}, +}; + +const ABSENT_COMMAND: &str = "netsuke-guaranteed-absent-command-3-14-5"; + +struct ExpectedAction { + name: &'static str, + command: &'static str, + has_iteration_vars: bool, +} + +impl ExpectedAction { + const fn nextest() -> Self { + Self { + name: "run-tests-nextest", + command: "cargo nextest run", + has_iteration_vars: true, + } + } + + const fn legacy() -> Self { + Self { + name: "run-tests-legacy", + command: "cargo test", + has_iteration_vars: false, + } + } +} + +struct ResolverWorld { + _temp: tempfile::TempDir, + root: Utf8PathBuf, +} + +impl ResolverWorld { + fn new(has_nextest: bool) -> Result { + let temp = tempfile::tempdir().context("create resolver workspace")?; + let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) + .map_err(|path| anyhow!("temporary path should be UTF-8: {path:?}"))?; + if has_nextest { + write_tool(&root, "cargo-nextest")?; + } + Ok(Self { _temp: temp, root }) + } + + fn environment(&self, has_nextest: bool) -> Result> { + let workspace = Dir::open_ambient_dir(&self.root, ambient_authority()) + .with_context(|| format!("open resolver workspace {}", self.root))?; + let path_override = if has_nextest { + OsString::from(self.root.as_str()) + } else { + OsString::new() + }; + let config = StdlibConfig::new(workspace)? + .with_workspace_root_path(self.root.clone())? + .with_path_override(path_override); + let mut env = Environment::new(); + stdlib::register_with_config(&mut env, config)?; + Ok(env) + } +} + +#[googletest::test] +#[rstest] +#[case::nextest_present(true, "cargo-nextest", ExpectedAction::nextest())] +#[case::nextest_absent(false, ABSENT_COMMAND, ExpectedAction::legacy())] +fn real_command_available_selects_exactly_one_action( + #[case] has_nextest: bool, + #[case] command_name: &str, + #[case] expected: ExpectedAction, +) -> googletest::Result<()> { + let world = ResolverWorld::new(has_nextest).or_fail()?; + let env = world.environment(has_nextest).or_fail()?; + let yaml = format!( + "actions: + - name: run-tests-nextest + command: cargo nextest run + foreach: [nextest] + when: command_available({command_name:?}, cwd_mode='never') + - name: run-tests-legacy + command: cargo test + when: not command_available({command_name:?}, cwd_mode='never') +targets: [] +" + ); + let mut doc: ManifestValue = serde_saphyr::from_str(&yaml).or_fail()?; + + expand_foreach(&mut doc, &env).or_fail()?; + + let selected_actions = actions(&doc).or_fail()?; + verify_that!(selected_actions, len(eq(1)))?; + let selected = selected_actions.first().or_fail()?; + let selected_map = selected.as_object().or_fail()?; + verify_that!(string_field(selected, "name").or_fail()?, eq(expected.name))?; + verify_that!( + string_field(selected, "command").or_fail()?, + eq(expected.command) + )?; + verify_that!(selected_map.contains_key("when"), eq(false))?; + verify_that!(selected_map.contains_key("foreach"), eq(false))?; + verify_that!( + selected_map.contains_key("vars"), + eq(expected.has_iteration_vars) + )?; + Ok(()) +} + +fn string_field<'a>(value: &'a ManifestValue, field: &str) -> Result<&'a str> { + value + .as_object() + .context("selected action map")? + .get(field) + .and_then(ManifestValue::as_str) + .with_context(|| format!("selected action {field}")) +} + +fn write_tool(root: &Utf8Path, name: &str) -> Result<()> { + let path = root.join(tool_filename(name)); + std::fs::write(path.as_std_path(), script_contents()) + .with_context(|| format!("write fixture tool {path}"))?; + mark_executable(&path) +} + +fn tool_filename(name: &str) -> String { + if cfg!(windows) { + format!("{name}.cmd") + } else { + name.to_owned() + } +} + +const fn script_contents() -> &'static [u8] { + #[cfg(windows)] + { + b"@echo off\r\n" + } + #[cfg(not(windows))] + { + b"#!/bin/sh\nexit 0\n" + } +} + +#[cfg(unix)] +fn mark_executable(path: &Utf8Path) -> Result<()> { + use std::os::unix::fs::PermissionsExt; + + let mut permissions = std::fs::metadata(path.as_std_path()) + .with_context(|| format!("stat {path}"))? + .permissions(); + permissions.set_mode(0o755); + std::fs::set_permissions(path.as_std_path(), permissions) + .with_context(|| format!("chmod {path}")) +} + +#[cfg(not(unix))] +fn mark_executable(_path: &Utf8Path) -> Result<()> { + Ok(()) +} diff --git a/src/manifest/expand_tests.rs b/src/manifest/expand_tests.rs index baeab5999..2452823b5 100644 --- a/src/manifest/expand_tests.rs +++ b/src/manifest/expand_tests.rs @@ -8,6 +8,9 @@ use rstest::fixture; mod a_tracing_capture; #[path = "expand_test_cases/action_condition_cases.rs"] mod action_condition_cases; + +#[path = "expand_test_cases/command_available_selection_cases.rs"] +mod command_available_selection_cases; #[path = "expand_test_cases/condition_cases.rs"] mod condition_cases; From 52fa2f1b13c1dd31ae00bcbd3e33e639f8387f66 Mon Sep 17 00:00:00 2001 From: leynos Date: Wed, 29 Jul 2026 00:51:32 +0200 Subject: [PATCH 09/34] Test pure conditional selection Assert that absent-tool fallback selection leaves the shared impurity marker clear. Pair it with a cross-platform shell-in-when control that proves the boundary observable is live during expansion. Record the focused sabotage evidence and findings in the ExecPlan. --- ...conditional-action-dependency-manifests.md | 41 ++++++- .../command_available_no_shell_cases.rs | 116 ++++++++++++++++++ src/manifest/expand_tests.rs | 2 + 3 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 src/manifest/expand_test_cases/command_available_no_shell_cases.rs diff --git a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md index d7536be91..326ad29bb 100644 --- a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md +++ b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md @@ -162,7 +162,7 @@ escalation, not workarounds. and googletest/rstest interoperability; retained only the approved dev dependencies after reverting the throwaway spike. - [ ] Stage B: add new tests/fixtures in the failing-then-passing discipline - with per-test sabotage evidence (completed: B.1; remaining: B.2–B.5). + with per-test sabotage evidence (completed: B.1–B.2; remaining: B.3–B.5). - [ ] Stage C: documentation updates (users-guide, developers-guide, component architecture, ADR if warranted). - [ ] Stage D: full gate run, CodeRabbit review, roadmap tick. @@ -225,6 +225,13 @@ escalation, not workarounds. passed both parameterized cases. Impact: use the same error boundary in B.2 instead of flattening matcher diagnostics into `anyhow`. +- Observation: a successful shell control can avoid external-program + discovery while remaining cross-platform by invoking the platform shell's + no-op built-in (`:` on Unix, `exit /b 0` on Windows). Evidence: B.2 evaluates + the built-in from a `when` expression and the expansion succeeds while + `StdlibState::is_impure()` flips to `true`. Impact: the control verifies the + same expansion boundary without an `ensure_binaries_available` skip. + ## Decision log - Decision: Treat 3.14.5 as test-and-docs only; the implementation @@ -314,6 +321,12 @@ deterministic worlds. The present case also combines action `foreach` with `when`; both cases assert exactly one action survives and that expansion removes control fields. No production seam or semantic change was required. +B.2 now proves the absent-tool fallback runs without invoking any impure stdlib +helper. Its paired shell-in-`when` control proves the shared impurity +observable is live during the same expansion boundary. The claim remains +deliberately broader than shell alone, as recorded under Risks and Hexagonal +framing. + ## Context and orientation This section assumes no prior knowledge of the repository. @@ -727,6 +740,27 @@ The production line was restored immediately with an explicit inverse patch; and `/tmp/b1-sabotage-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out`. +Stage B.2 impurity-boundary and sabotage evidence: + +```plaintext +passing baseline: +2 passed; 0 failed; 438 filtered out + +temporary sabotage: +StdlibState::is_impure(): load(...) -> !load(...) +command_available_fallback_does_not_invoke_impure_helpers: + expected false, actual true +shell_in_when_marks_selection_impure: + expected true, actual false +0 passed; 2 failed; 438 filtered out +``` + +The production line was restored immediately with an explicit inverse patch. +Transcripts: +`/tmp/b2-focused-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out` +and +`/tmp/b2-sabotage-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out`. + ## Interfaces and dependencies New `[dev-dependencies]` in `Cargo.toml` (pre-authorized by the brief): @@ -863,3 +897,8 @@ Skills to load while implementing: Added deterministic present/absent real-resolver cases, including the action-level `foreach` interaction, and recorded the assertion-level sabotage failure. Remaining work starts at Stage B.2. + +- 2026-07-28 — Completed Stage B.2 implementation and its non-vacuity check. + Added a pure absent-command selection case and a cross-platform shell + control, then recorded both assertion-level failures under an inverted + impurity observable. Remaining work starts at Stage B.3. diff --git a/src/manifest/expand_test_cases/command_available_no_shell_cases.rs b/src/manifest/expand_test_cases/command_available_no_shell_cases.rs new file mode 100644 index 000000000..7b1acb8e2 --- /dev/null +++ b/src/manifest/expand_test_cases/command_available_no_shell_cases.rs @@ -0,0 +1,116 @@ +//! Boundary coverage for pure command-availability selection. + +use std::ffi::OsString; + +use anyhow::{Context, Result, anyhow}; +use camino::Utf8PathBuf; +use cap_std::{ambient_authority, fs_utf8::Dir}; +use googletest::prelude::*; +use minijinja::Environment; +use rstest::rstest; + +use super::{actions, expand_foreach}; +use crate::{ + manifest::ManifestValue, + stdlib::{self, StdlibConfig, StdlibState}, +}; + +const ABSENT_COMMAND: &str = "netsuke-guaranteed-absent-command-no-shell-3-14-5"; + +struct ExpansionWorld { + _temp: tempfile::TempDir, + environment: Environment<'static>, + state: StdlibState, +} + +impl ExpansionWorld { + fn new() -> Result { + let temp = tempfile::tempdir().context("create expansion workspace")?; + let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) + .map_err(|path| anyhow!("temporary path should be UTF-8: {path:?}"))?; + let workspace = Dir::open_ambient_dir(&root, ambient_authority()) + .with_context(|| format!("open expansion workspace {root}"))?; + let config = StdlibConfig::new(workspace)? + .with_workspace_root_path(root)? + .with_path_override(OsString::new()); + let mut environment = Environment::new(); + let state = stdlib::register_with_config(&mut environment, config)?; + Ok(Self { + _temp: temp, + environment, + state, + }) + } +} + +#[googletest::test] +#[rstest] +fn command_available_fallback_does_not_invoke_impure_helpers() -> googletest::Result<()> { + let world = ExpansionWorld::new().or_fail()?; + let yaml = format!( + "actions: + - name: unavailable-tool + command: unavailable-tool + when: command_available({ABSENT_COMMAND:?}, cwd_mode='never') + - name: fallback + command: cargo test + when: not command_available({ABSENT_COMMAND:?}, cwd_mode='never') +targets: [] +" + ); + let mut doc: ManifestValue = serde_saphyr::from_str(&yaml).or_fail()?; + + expand_foreach(&mut doc, &world.environment).or_fail()?; + + let selected_actions = actions(&doc).or_fail()?; + verify_that!(selected_actions, len(eq(1)))?; + let selected = selected_actions.first().or_fail()?; + let selected_map = selected.as_object().or_fail()?; + verify_that!(string_field(selected, "name").or_fail()?, eq("fallback"))?; + verify_that!(selected_map.contains_key("when"), eq(false))?; + verify_that!(world.state.is_impure(), eq(false))?; + Ok(()) +} + +#[googletest::test] +#[rstest] +fn shell_in_when_marks_selection_impure() -> googletest::Result<()> { + let world = ExpansionWorld::new().or_fail()?; + let command = shell_control_command(); + let yaml = format!( + "actions: + - name: shell-control + command: echo control + when: >- + ('' | shell({command:?})) == '' +targets: [] +" + ); + let mut doc: ManifestValue = serde_saphyr::from_str(&yaml).or_fail()?; + + expand_foreach(&mut doc, &world.environment).or_fail()?; + + verify_that!(actions(&doc).or_fail()?, len(eq(1)))?; + verify_that!(world.state.is_impure(), eq(true))?; + Ok(()) +} + +fn string_field<'a>(value: &'a ManifestValue, field: &str) -> Result<&'a str> { + value + .as_object() + .context("selected action map")? + .get(field) + .and_then(ManifestValue::as_str) + .with_context(|| format!("selected action {field}")) +} + +const fn shell_control_command() -> &'static str { + #[cfg(windows)] + { + "exit /b 0" + } + #[cfg(not(windows))] + { + ":" + } +} diff --git a/src/manifest/expand_tests.rs b/src/manifest/expand_tests.rs index 2452823b5..83f591201 100644 --- a/src/manifest/expand_tests.rs +++ b/src/manifest/expand_tests.rs @@ -9,6 +9,8 @@ mod a_tracing_capture; #[path = "expand_test_cases/action_condition_cases.rs"] mod action_condition_cases; +#[path = "expand_test_cases/command_available_no_shell_cases.rs"] +mod command_available_no_shell_cases; #[path = "expand_test_cases/command_available_selection_cases.rs"] mod command_available_selection_cases; #[path = "expand_test_cases/condition_cases.rs"] From 7eb3eea4eccfb50ca139a20bd0e63f4bf3f51302 Mon Sep 17 00:00:00 2001 From: leynos Date: Wed, 29 Jul 2026 00:59:48 +0200 Subject: [PATCH 10/34] Test conditional dependency lowering Add a deterministic fixture combining complementary selection, action foreach expansion, and all three dependency classes. Verify selected actions and targets lower dependencies into the correct IR fields while filtered branches contribute no paths. Document direct-path isolation and the focused sabotage evidence. --- ...conditional-action-dependency-manifests.md | 70 ++++++++++-- tests/data/conditional_action_deps.yml | 32 ++++++ tests/ir_from_manifest_tests.rs | 102 ++++++++++++++++++ 3 files changed, 195 insertions(+), 9 deletions(-) create mode 100644 tests/data/conditional_action_deps.yml diff --git a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md index 326ad29bb..a94987af4 100644 --- a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md +++ b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md @@ -65,14 +65,18 @@ escalation, not workarounds. - Determinism: no new test may depend on the host's real `PATH` contents. "Present" cases must inject a temporary directory containing a fake executable via the public `StdlibConfig::with_path_override(...)` seam. - "Absent" cases must combine **all three** guards together: an empty - `path_override`, a guaranteed-absent command name (for example a - UUID-suffixed name), *and* `cwd_mode="never"`. An empty `path_override` alone - is **not** sufficient: `parse_path_entries` maps an empty PATH component to - the current directory and the default `CwdMode::Auto` scans the cwd - (`src/stdlib/which/env.rs:91`, `src/stdlib/which/options.rs:11`), so without - `cwd_mode="never"` the temporary workspace's own contents could shadow the - lookup. + "Absent" cases using a bare command name must combine **all three** guards + together: an empty `path_override`, a guaranteed-absent command name (for + example a UUID-suffixed name), *and* `cwd_mode="never"`. An empty + `path_override` alone is **not** sufficient: `parse_path_entries` maps an + empty PATH component to the current directory and the default `CwdMode::Auto` + scans the cwd (`src/stdlib/which/env.rs:91`, + `src/stdlib/which/options.rs:11`), so without `cwd_mode="never"` the + temporary workspace's own contents could shadow the lookup. Integration + fixtures that cannot inject `path_override` must instead use a + guaranteed-absent **direct path** containing a separator; `resolve_direct` + bypasses PATH traversal entirely + (`src/stdlib/which/lookup/mod.rs:44,107-120`). ## Tolerances (exception triggers) @@ -162,7 +166,7 @@ escalation, not workarounds. and googletest/rstest interoperability; retained only the approved dev dependencies after reverting the throwaway spike. - [ ] Stage B: add new tests/fixtures in the failing-then-passing discipline - with per-test sabotage evidence (completed: B.1–B.2; remaining: B.3–B.5). + with per-test sabotage evidence (completed: B.1–B.3; remaining: B.4–B.5). - [ ] Stage C: documentation updates (users-guide, developers-guide, component architecture, ADR if warranted). - [ ] Stage D: full gate run, CodeRabbit review, roadmap tick. @@ -232,6 +236,14 @@ escalation, not workarounds. `StdlibState::is_impure()` flips to `true`. Impact: the control verifies the same expansion boundary without an `ensure_binaries_available` skip. +- Observation: the public integration-level manifest loaders do not expose + `StdlibConfig::with_path_override`; `from_path_with_policy` constructs its + own config. Evidence: `src/manifest/mod.rs:216-235`. Impact: the B.3 fixture + uses a guaranteed-absent direct path containing `/`, which routes through + `resolve_direct` and bypasses PATH traversal entirely. The Constraints + section now distinguishes this stronger direct-path guard from the three + guards required for bare command names. + ## Decision log - Decision: Treat 3.14.5 as test-and-docs only; the implementation @@ -309,6 +321,14 @@ escalation, not workarounds. 0.26.1, matching the documented interoperability contract supplied by the approver. Date/Author: 2026-07-28, implementation agent. +- Decision: Use a direct guaranteed-absent command path in external integration + fixtures. Rationale: public integration loaders intentionally own stdlib + configuration and do not accept a PATH override; adding a production API or + mutating process PATH would violate the test-only and isolation constraints. + A command containing `/` takes the resolver's direct path and never traverses + PATH, which is stronger than empty-PATH isolation for this absent-only + fixture. Date/Author: 2026-07-28, implementation agent. + ## Outcomes & retrospective Stage A completed without requiring a new seam or changing production code. The @@ -327,6 +347,12 @@ observable is live during the same expansion boundary. The claim remains deliberately broader than shell alone, as recorded under Risks and Hexagonal framing. +B.3 now combines conditional selection with action `foreach` and dependency +lowering. The selected alpha/beta actions preserve item-substituted explicit, +implicit, and order-only dependency classes and remain phony; the selected +target preserves the same classes without becoming phony. Filtered-branch paths +are absent from the graph. + ## Context and orientation This section assumes no prior knowledge of the repository. @@ -761,6 +787,26 @@ Transcripts: and `/tmp/b2-sabotage-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out`. +Stage B.3 IR-lowering and sabotage evidence: + +```plaintext +passing baseline: +1 passed; 0 failed; 15 filtered out + +temporary sabotage: +implicit_deps = to_paths(&target.deps) -> Vec::new() +conditional_action_deps_populate_distinct_ir_classes: + fallback-alpha expected ["build/alpha.o", "shared/action.cfg"] + actual [] +0 passed; 1 failed; 15 filtered out +``` + +The production line was restored immediately with an explicit inverse patch. +Transcripts: +`/tmp/b3-focused-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out` +and +`/tmp/b3-sabotage-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out`. + ## Interfaces and dependencies New `[dev-dependencies]` in `Cargo.toml` (pre-authorized by the brief): @@ -902,3 +948,9 @@ Skills to load while implementing: Added a pure absent-command selection case and a cross-platform shell control, then recorded both assertion-level failures under an inverted impurity observable. Remaining work starts at Stage B.3. + +- 2026-07-28 — Completed Stage B.3 implementation and its non-vacuity check. + Added the combined conditional dependency fixture and verified selected + foreach actions and the selected target lower dependency classes correctly. + Recorded the direct-path determinism decision and assertion-level sabotage + failure. Remaining work starts at Stage B.4. diff --git a/tests/data/conditional_action_deps.yml b/tests/data/conditional_action_deps.yml new file mode 100644 index 000000000..5aa51e8bd --- /dev/null +++ b/tests/data/conditional_action_deps.yml @@ -0,0 +1,32 @@ +netsuke_version: "1.0.0" +actions: + - name: preferred-action + command: echo preferred + sources: preferred/action.src + deps: preferred/action.dep + order_only_deps: preferred/action.order + when: command_available("./.netsuke-missing/tool-3-14-5", cwd_mode="never") + - name: "fallback-{{ item }}" + command: "echo {{ item }}" + foreach: + - alpha + - beta + sources: "src/{{ item }}.in" + deps: + - "build/{{ item }}.o" + - shared/action.cfg + order_only_deps: "order/{{ item }}.stamp" + when: not command_available("./.netsuke-missing/tool-3-14-5", cwd_mode="never") +targets: + - name: preferred-target + command: echo preferred + sources: preferred/target.src + deps: preferred/target.dep + order_only_deps: preferred/target.order + when: command_available("./.netsuke-missing/tool-3-14-5", cwd_mode="never") + - name: out/fallback + command: echo fallback + sources: src/target.in + deps: include/fallback.h + order_only_deps: order/target.stamp + when: not command_available("./.netsuke-missing/tool-3-14-5", cwd_mode="never") diff --git a/tests/ir_from_manifest_tests.rs b/tests/ir_from_manifest_tests.rs index 3fcc928e6..bd4a769c7 100644 --- a/tests/ir_from_manifest_tests.rs +++ b/tests/ir_from_manifest_tests.rs @@ -258,6 +258,108 @@ fn manifest_deps_do_not_contribute_to_recipe_inputs() -> Result<()> { Ok(()) } +#[rstest] +fn conditional_action_deps_populate_distinct_ir_classes() -> Result<()> { + let manifest = manifest::from_path("tests/data/conditional_action_deps.yml")?; + let graph = BuildGraph::from_manifest(&manifest).context("expected graph generation")?; + + assert_conditional_edge( + &graph, + "fallback-alpha", + &ExpectedEdge { + inputs: &["src/alpha.in"], + implicit_deps: &["build/alpha.o", "shared/action.cfg"], + order_only_deps: &["order/alpha.stamp"], + is_phony: true, + }, + )?; + assert_conditional_edge( + &graph, + "fallback-beta", + &ExpectedEdge { + inputs: &["src/beta.in"], + implicit_deps: &["build/beta.o", "shared/action.cfg"], + order_only_deps: &["order/beta.stamp"], + is_phony: true, + }, + )?; + assert_conditional_edge( + &graph, + "out/fallback", + &ExpectedEdge { + inputs: &["src/target.in"], + implicit_deps: &["include/fallback.h"], + order_only_deps: &["order/target.stamp"], + is_phony: false, + }, + )?; + + let rendered_paths = graph + .targets + .iter() + .flat_map(|(output, edge)| { + std::iter::once(output) + .chain(&edge.inputs) + .chain(&edge.implicit_deps) + .chain(&edge.order_only_deps) + }) + .map(|path| path.as_str()) + .collect::>(); + ensure!( + rendered_paths + .iter() + .all(|path| !path.starts_with("preferred")), + "filtered branches should not contribute paths to the IR: {rendered_paths:?}" + ); + Ok(()) +} + +struct ExpectedEdge<'a> { + inputs: &'a [&'a str], + implicit_deps: &'a [&'a str], + order_only_deps: &'a [&'a str], + is_phony: bool, +} + +fn assert_conditional_edge( + graph: &BuildGraph, + output: &str, + expected: &ExpectedEdge<'_>, +) -> Result<()> { + let edge = graph + .targets + .get(&Utf8PathBuf::from(output)) + .with_context(|| format!("expected edge for {output}"))?; + let expected_paths = |paths: &[&str]| { + paths + .iter() + .copied() + .map(Utf8PathBuf::from) + .collect::>() + }; + ensure!( + edge.inputs == expected_paths(expected.inputs), + "unexpected explicit inputs for {output}: {:?}", + edge.inputs + ); + ensure!( + edge.implicit_deps == expected_paths(expected.implicit_deps), + "unexpected implicit deps for {output}: {:?}", + edge.implicit_deps + ); + ensure!( + edge.order_only_deps == expected_paths(expected.order_only_deps), + "unexpected order-only deps for {output}: {:?}", + edge.order_only_deps + ); + ensure!( + edge.phony == expected.is_phony, + "unexpected phony flag for {output}: {}", + edge.phony + ); + Ok(()) +} + #[test] fn dependency_only_entries_lower_to_deduplicated_phony_actions() -> Result<()> { let yaml = concat!( From ca005f8d7034a53ab9cc30e7f3b1686794177c00 Mon Sep 17 00:00:00 2001 From: leynos Date: Wed, 29 Jul 2026 01:08:55 +0200 Subject: [PATCH 11/34] Snapshot conditional dependency output Pin Ninja emission for selected foreach actions and the selected target, including explicit, implicit, and order-only dependency syntax. Validate the generated file with real Ninja queries, a harmless build, and a deterministic no-op second pass. Record snapshot and sabotage evidence in the ExecPlan. --- ...conditional-action-dependency-manifests.md | 41 ++++- tests/ninja_snapshot_tests.rs | 145 +++++++++++++++++- ..._tests__conditional_action_deps_ninja.snap | 18 +++ 3 files changed, 199 insertions(+), 5 deletions(-) create mode 100644 tests/snapshots/ninja/ninja_snapshot_tests__conditional_action_deps_ninja.snap diff --git a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md index a94987af4..a93f62744 100644 --- a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md +++ b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md @@ -166,7 +166,7 @@ escalation, not workarounds. and googletest/rstest interoperability; retained only the approved dev dependencies after reverting the throwaway spike. - [ ] Stage B: add new tests/fixtures in the failing-then-passing discipline - with per-test sabotage evidence (completed: B.1–B.3; remaining: B.4–B.5). + with per-test sabotage evidence (completed: B.1–B.4; remaining: B.5). - [ ] Stage C: documentation updates (users-guide, developers-guide, component architecture, ADR if warranted). - [ ] Stage D: full gate run, CodeRabbit review, roadmap tick. @@ -244,6 +244,14 @@ escalation, not workarounds. section now distinguishes this stronger direct-path guard from the three guards required for bare command names. +- Observation: a manually-created output is not enough for Ninja's no-op check, + even when its modification time is newer than every prerequisite, because no + `.ninja_log` entry records the command metadata. Evidence: B.4's first two + focused attempts remained dirty until one harmless real pass populated the + log; the following `-n` pass then reported `no work to do`. Impact: + real-Ninja validation creates a future-dated output, runs the selected target + once to record its command, and then asserts the second pass is a no-op. + ## Decision log - Decision: Treat 3.14.5 as test-and-docs only; the implementation @@ -353,6 +361,11 @@ implicit, and order-only dependency classes and remain phony; the selected target preserves the same classes without becoming phony. Filtered-branch paths are absent from the graph. +B.4 now pins the generated Ninja text for the shared fixture. Assertions and +the reviewed snapshot preserve `|` implicit and `||` order-only syntax for both +foreach actions and the selected target; real Ninja accepts and queries the +selected edges and reaches a no-op second pass. Filtered paths remain absent. + ## Context and orientation This section assumes no prior knowledge of the repository. @@ -807,6 +820,26 @@ Transcripts: and `/tmp/b3-sabotage-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out`. +Stage B.4 Ninja-emission and sabotage evidence: + +```plaintext +passing baseline: +1 passed; 0 failed; 3 filtered out +snapshot matched; Ninja query/build passed; second pass was a no-op + +temporary sabotage: +write implicit deps as " {}" instead of " | {}" +conditional_action_deps_ninja_snapshot: + fallback-alpha build line lacked "|" before build/alpha.o +0 passed; 1 failed; 3 filtered out +``` + +The production line was restored immediately with an explicit inverse patch. +Transcripts: +`/tmp/b4-focused-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out` +and +`/tmp/b4-sabotage-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out`. + ## Interfaces and dependencies New `[dev-dependencies]` in `Cargo.toml` (pre-authorized by the brief): @@ -954,3 +987,9 @@ Skills to load while implementing: foreach actions and the selected target lower dependency classes correctly. Recorded the direct-path determinism decision and assertion-level sabotage failure. Remaining work starts at Stage B.4. + +- 2026-07-28 — Completed Stage B.4 implementation and its non-vacuity check. + Added the conditional-dependency Ninja snapshot, explicit dependency-class + assertions, and real-Ninja query/build/no-op validation. Recorded the + `.ninja_log` finding and missing-separator sabotage failure. Remaining work + starts at Stage B.5. diff --git a/tests/ninja_snapshot_tests.rs b/tests/ninja_snapshot_tests.rs index 32824ea52..a9c1d423b 100644 --- a/tests/ninja_snapshot_tests.rs +++ b/tests/ninja_snapshot_tests.rs @@ -8,13 +8,10 @@ use anyhow::{Context, Result, ensure}; use cap_std::{ambient_authority, fs_utf8::Dir}; use insta::{Settings, assert_snapshot}; use netsuke::{ir::BuildGraph, manifest, ninja_gen, stdlib::StdlibConfig}; -#[cfg(unix)] use std::process::Command; -#[cfg(unix)] +use std::{fs, io::Write, process::Command, time::Duration}; use tempfile::tempdir; -#[cfg(unix)] use test_support::ensure_binaries_available; -#[cfg(unix)] use test_support::fs; #[cfg(unix)] @@ -257,6 +254,146 @@ fn implicit_deps_manifest_ninja_snapshot() -> Result<()> { Ok(()) } +#[test] +fn conditional_action_deps_ninja_snapshot() -> Result<()> { + let manifest = manifest::from_path("tests/data/conditional_action_deps.yml")?; + let ir = BuildGraph::from_manifest(&manifest)?; + let ninja_content = ninja_gen::generate(&ir)?; + + assert_dependency_classes( + &ninja_content, + &ExpectedNinjaEdge { + output: "fallback-alpha", + input: "src/alpha.in", + implicit_deps: "build/alpha.o shared/action.cfg", + order_only_deps: "order/alpha.stamp", + }, + )?; + assert_dependency_classes( + &ninja_content, + &ExpectedNinjaEdge { + output: "fallback-beta", + input: "src/beta.in", + implicit_deps: "build/beta.o shared/action.cfg", + order_only_deps: "order/beta.stamp", + }, + )?; + assert_dependency_classes( + &ninja_content, + &ExpectedNinjaEdge { + output: "out/fallback", + input: "src/target.in", + implicit_deps: "include/fallback.h", + order_only_deps: "order/target.stamp", + }, + )?; + ensure!( + !ninja_content.contains("preferred"), + "filtered branches should not appear in Ninja output:\n{ninja_content}" + ); + + let mut settings = Settings::new(); + settings.set_snapshot_path(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/snapshots/ninja" + )); + settings.bind(|| { + assert_snapshot!("conditional_action_deps_ninja", ninja_content); + }); + + validate_conditional_ninja(&ninja_content) +} + +struct ExpectedNinjaEdge<'a> { + output: &'a str, + input: &'a str, + implicit_deps: &'a str, + order_only_deps: &'a str, +} + +fn assert_dependency_classes(ninja_content: &str, expected: &ExpectedNinjaEdge<'_>) -> Result<()> { + let build_line = ninja_content + .lines() + .find(|line| line.starts_with(&format!("build {}:", expected.output))) + .with_context(|| format!("expected build line for {}", expected.output))?; + ensure!( + build_line.contains(&format!( + " {} | {} || {}", + expected.input, expected.implicit_deps, expected.order_only_deps + )), + "unexpected dependency classes for {}: {build_line}", + expected.output + ); + Ok(()) +} + +fn validate_conditional_ninja(ninja_content: &str) -> Result<()> { + if let Err(err) = ensure_binaries_available(&[("ninja", &["--version"])]) { + tracing::warn!("skipping real Ninja validation: {}", err); + writeln!( + std::io::stderr().lock(), + "skipping real Ninja validation: {err}" + ) + .context("write real Ninja skip reason")?; + return Ok(()); + } + + let dir = tempdir().context("create temp dir for conditional Ninja validation")?; + let build_file = dir.path().join("build.ninja"); + fs::write(&build_file, ninja_content) + .with_context(|| format!("write Ninja file to {}", build_file.display()))?; + for relative_path in ["src/target.in", "include/fallback.h", "order/target.stamp"] { + let dependency_path = dir.path().join(relative_path); + let parent = dependency_path + .parent() + .context("dependency path should have parent")?; + fs::create_dir_all(parent) + .with_context(|| format!("create dependency directory {}", parent.display()))?; + fs::write(&dependency_path, "") + .with_context(|| format!("write Ninja dependency {}", dependency_path.display()))?; + } + + let ninja_cmd = |args: &[&str]| { + let mut cmd = Command::new("ninja"); + cmd.arg("-f").arg(&build_file).args(args); + cmd.current_dir(dir.path()); + run_ok(&mut cmd) + }; + ninja_cmd(&["-t", "query", "fallback-alpha"])?; + ninja_cmd(&["-t", "query", "out/fallback"])?; + let first = ninja_cmd(&["-n", "out/fallback"])?; + ensure!( + first.contains("echo fallback"), + "expected dry run to reach selected target, got:\n{first}" + ); + + let output = dir.path().join("out/fallback"); + let output_dir = output.parent().context("output path should have parent")?; + fs::create_dir_all(output_dir) + .with_context(|| format!("create output directory {}", output_dir.display()))?; + fs::write(&output, "") + .with_context(|| format!("write up-to-date output {}", output.display()))?; + let latest_dependency = dir.path().join("order/target.stamp"); + let output_modified = fs::metadata(&latest_dependency) + .with_context(|| format!("stat dependency {}", latest_dependency.display()))? + .modified() + .context("read dependency modification time")? + + Duration::from_secs(1); + fs::File::options() + .write(true) + .open(&output) + .with_context(|| format!("open output {}", output.display()))? + .set_times(fs::FileTimes::new().set_modified(output_modified)) + .with_context(|| format!("set output time {}", output.display()))?; + ninja_cmd(&["out/fallback"])?; + let second = ninja_cmd(&["-n", "out/fallback"])?; + ensure!( + second.contains("no work to do"), + "expected no-op second pass, got:\n{second}" + ); + Ok(()) +} + #[test] fn dependency_only_manifest_ninja_snapshot() -> Result<()> { let manifest_yaml = r#" diff --git a/tests/snapshots/ninja/ninja_snapshot_tests__conditional_action_deps_ninja.snap b/tests/snapshots/ninja/ninja_snapshot_tests__conditional_action_deps_ninja.snap new file mode 100644 index 000000000..e82befe75 --- /dev/null +++ b/tests/snapshots/ninja/ninja_snapshot_tests__conditional_action_deps_ninja.snap @@ -0,0 +1,18 @@ +--- +source: tests/ninja_snapshot_tests.rs +expression: ninja_content +--- +rule dfd2ff5921f146a85d62fcfeb725508df48b315134db4f325585bd6981940de6 + command = echo alpha + +rule e119fafadd9fa5af44c4dc58e047f27d387ed098c0702aeffbce67f90746ca3c + command = echo beta + +rule e8c3bab461990fead7f11b007334dcd68b89db712472c5a294c69609dd7302b5 + command = echo fallback + +build fallback-alpha: dfd2ff5921f146a85d62fcfeb725508df48b315134db4f325585bd6981940de6 src/alpha.in | build/alpha.o shared/action.cfg || order/alpha.stamp + +build fallback-beta: e119fafadd9fa5af44c4dc58e047f27d387ed098c0702aeffbce67f90746ca3c src/beta.in | build/beta.o shared/action.cfg || order/beta.stamp + +build out/fallback: e8c3bab461990fead7f11b007334dcd68b89db712472c5a294c69609dd7302b5 src/target.in | include/fallback.h || order/target.stamp From 0f5d3a667ff8d0d3643df8bad66a0e7a57748b36 Mon Sep 17 00:00:00 2001 From: leynos Date: Wed, 29 Jul 2026 01:16:57 +0200 Subject: [PATCH 12/34] Cover conditional deps behaviour Add one combined BDD scenario proving a selected foreach action retains its explicit and implicit dependencies through Ninja generation. Reuse the existing IR and Ninja steps to avoid expanding the matcher surface. Mark Stage B complete and record the behavioural sabotage evidence. --- ...conditional-action-dependency-manifests.md | 34 +++++++++++++++++-- tests/features/ninja.feature | 8 +++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md index a93f62744..f388669e4 100644 --- a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md +++ b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md @@ -165,8 +165,8 @@ escalation, not workarounds. - [x] (2026-07-28 22:03Z) Stage A: confirmed interfaces, dependency ordering, and googletest/rstest interoperability; retained only the approved dev dependencies after reverting the throwaway spike. -- [ ] Stage B: add new tests/fixtures in the failing-then-passing discipline - with per-test sabotage evidence (completed: B.1–B.4; remaining: B.5). +- [x] (2026-07-28 23:14Z) Stage B: added B.1–B.5 tests and fixtures with a + passing focused run and assertion-level sabotage evidence for each. - [ ] Stage C: documentation updates (users-guide, developers-guide, component architecture, ADR if warranted). - [ ] Stage D: full gate run, CodeRabbit review, roadmap tick. @@ -366,6 +366,11 @@ the reviewed snapshot preserve `|` implicit and `||` order-only syntax for both foreach actions and the selected target; real Ninja accepts and queries the selected edges and reaches a no-op second pass. Filtered paths remain absent. +B.5 completes the behavioural layer with one combined scenario assembled +entirely from existing steps. It observes the selected action's explicit and +implicit IR inputs and the emitted `|`/`||` Ninja segment without adding step +matcher maintenance. + ## Context and orientation This section assumes no prior knowledge of the repository. @@ -840,6 +845,27 @@ Transcripts: and `/tmp/b4-sabotage-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out`. +Stage B.5 combined-BDD and sabotage evidence: + +```plaintext +passing baseline: +features_scenarios::ninja_selected_conditional_action_deps_become_implicit_ninja_dependencies +1 passed; 0 failed; 241 filtered out + +temporary sabotage: +write implicit deps as " {}" instead of " | {}" +failing step 5: +Then the ninja file contains +" src/alpha.in | build/alpha.o shared/action.cfg || order/alpha.stamp" +0 passed; 1 failed; 241 filtered out +``` + +The production line was restored immediately with an explicit inverse patch. +Transcripts: +`/tmp/b5-focused-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out` +and +`/tmp/b5-sabotage-netsuke-3-14-5-regression-coverage-for-conditional-action-dependency-manifests.out`. + ## Interfaces and dependencies New `[dev-dependencies]` in `Cargo.toml` (pre-authorized by the brief): @@ -993,3 +1019,7 @@ Skills to load while implementing: assertions, and real-Ninja query/build/no-op validation. Recorded the `.ninja_log` finding and missing-separator sabotage failure. Remaining work starts at Stage B.5. + +- 2026-07-28 — Completed Stage B.5 and Stage B overall. Added one combined BDD + scenario using only existing steps, then recorded the public Gherkin + missing-separator failure. Remaining work starts at Stage C documentation. diff --git a/tests/features/ninja.feature b/tests/features/ninja.feature index 82e158d36..d9c9199be 100644 --- a/tests/features/ninja.feature +++ b/tests/features/ninja.feature @@ -36,3 +36,11 @@ Feature: Ninja file generation And the ninja file contains "build regenerate: " And the ninja file contains " | schemas/user.yml tools/generator" And the ninja file contains "command = echo src/main.c src/main.c > out/app" + + Scenario: Selected conditional action deps become implicit Ninja dependencies + When the manifest file "tests/data/conditional_action_deps.yml" is compiled to IR + Then the graph target "fallback-alpha" has inputs "src/alpha.in" + And the graph target "fallback-alpha" has implicit deps "build/alpha.o, shared/action.cfg" + When the ninja file is generated + Then the ninja file contains "build fallback-alpha: " + And the ninja file contains " src/alpha.in | build/alpha.o shared/action.cfg || order/alpha.stamp" From 449ff504f930554e2c8f3344ec25aae10edcd294 Mon Sep 17 00:00:00 2001 From: leynos Date: Wed, 29 Jul 2026 02:45:27 +0200 Subject: [PATCH 13/34] Document conditional dependency coverage Explain complementary action selection and implicit dependency behaviour for users. Record deterministic resolver tests, assertion conventions, and the existing discovery-versus-execution boundary for maintainers. Mark roadmap item 3.14.5 complete and record why no ADR is required. --- docs/developers-guide.md | 31 +++++++++++++++++++ ...conditional-action-dependency-manifests.md | 19 ++++++++++++ docs/netsuke-design.md | 11 +++++++ docs/roadmap.md | 16 +++------- docs/users-guide.md | 10 ++++++ 5 files changed, 76 insertions(+), 11 deletions(-) diff --git a/docs/developers-guide.md b/docs/developers-guide.md index ad69c1bb5..75e2ff560 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -2345,6 +2345,37 @@ targets: command: "cargo clippy --manifest-path {{ item }}/Cargo.toml" ``` +### Testing conditional manifest boundaries + +Keep command-availability tests deterministic and at the narrowest useful +boundary: + +- For a present bare command, create a fake cross-platform executable in a + temporary directory and inject that directory with + `StdlibConfig::with_path_override`. +- For an absent bare command, combine an empty `path_override`, a + guaranteed-absent name, and `cwd_mode="never"`. The empty override alone is + insufficient because an empty PATH entry can resolve to the current directory. +- External integration tests cannot inject a `StdlibConfig` through the public + manifest loader. Use a guaranteed-absent direct path containing a separator + in those fixtures; direct resolution bypasses PATH traversal. Do not mutate + process PATH to simulate absence. + +Use `googletest` only for the in-crate white-box conditional-expansion tests. +When combining it with `rstest`, place `#[googletest::test]` before `#[rstest]` +so each generated case is registered once. Return `googletest::Result<()>`, use +matchers for the behavioural assertions, and convert fallible fixture setup with +`.or_fail()?`. Existing integration and behavioural tests retain the +`anyhow::ensure!` convention; use `pretty_assertions` only where an ordinary +value-equality diff is materially clearer, never for snapshot comparison. + +`StdlibState::is_impure()` is the observable for selection-time boundary tests. +An absent `command_available` branch should leave it `false`; a control that +invokes `shell` from a `when` expression should make it `true`. This flag +covers all impure stdlib helpers (`shell`, `grep`, and `fetch`), so fixtures +must avoid the latter two and describe the assertion as "no impure helper ran", +not as a shell-specific counter. + ## Manifest glob module boundary Glob expansion lives in `src/manifest/glob/`, and `glob_paths` is its only diff --git a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md index f388669e4..33ace99b7 100644 --- a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md +++ b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md @@ -337,6 +337,13 @@ escalation, not workarounds. PATH, which is stronger than empty-PATH isolation for this absent-only fixture. Date/Author: 2026-07-28, implementation agent. +- Decision: Do not add an ADR for the regression boundary. Rationale: Stage C + documents and tests existing architecture—the executable-discovery resolver, + command-execution helpers, and shared impurity marker—without introducing or + selecting a new port, adapter, interface, or runtime policy. The design and + developers' guides are the appropriate homes for this test contract. + Date/Author: 2026-07-28, implementation agent. + ## Outcomes & retrospective Stage A completed without requiring a new seam or changing production code. The @@ -371,6 +378,12 @@ entirely from existing steps. It observes the selected action's explicit and implicit IR inputs and the emitted `|`/`||` Ninja segment without adding step matcher maintenance. +Stage C documentation now explains the selected action's `deps` semantics to +users, records deterministic conditional-test conventions for developers, +states the manifest/stdlib port boundary in the design, and marks roadmap item +3.14.5 and its four sub-items complete. No ADR is warranted because runtime +architecture and behaviour are unchanged. + ## Context and orientation This section assumes no prior knowledge of the repository. @@ -1023,3 +1036,9 @@ Skills to load while implementing: - 2026-07-28 — Completed Stage B.5 and Stage B overall. Added one combined BDD scenario using only existing steps, then recorded the public Gherkin missing-separator failure. Remaining work starts at Stage C documentation. + +- 2026-07-28 — Drafted Stage C documentation across the users' guide, + developers' guide, design, and roadmap. Recorded that no ADR is warranted + because this work pins existing boundaries rather than creating a new + architectural decision. Stage C remains pending until its gates and + CodeRabbit review pass. diff --git a/docs/netsuke-design.md b/docs/netsuke-design.md index c7d9c7641..ae8851473 100644 --- a/docs/netsuke-design.md +++ b/docs/netsuke-design.md @@ -481,6 +481,17 @@ therefore contain only the selected entries. Build-time branching remains the responsibility of the recipe command or script unless a separate future feature explicitly models runtime conditions. +Conditional selection crosses the executable-discovery boundary only. +`command_available(...)` drives the existing `which` resolver and converts a +typed search miss into `false`; it does not drive the command-execution +boundary used by `shell()`. Regression tests observe this separation through +`StdlibState::is_impure()`: a minimal availability-only selection keeps the +flag clear, while a shell-in-`when` control sets it. The flag also covers other +impure helpers such as `grep()` and `fetch()`, so it is intentionally a broader +assertion that no impure stdlib helper ran during selection, rather than a +shell-specific invocation count. No new port or adapter is introduced for this +test contract. + ```yaml - foreach: glob('assets/svg/*.svg') when: item | basename != 'logo.svg' diff --git a/docs/roadmap.md b/docs/roadmap.md index b8ae58c52..0a7434a8b 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -232,19 +232,13 @@ and agents. - [x] Return `false` for absent commands instead of raising `netsuke::jinja::which::not_found`. - [x] Preserve argument validation diagnostics for invalid options. -- [ ] 3.14.5. Add regression coverage for conditional action dependency +- [x] 3.14.5. Add regression coverage for conditional action dependency manifests. - - [ ] Test action-level `when` and action-level `foreach`. - - [ ] Test complementary nextest and legacy branches select exactly one + - [x] Test action-level `when` and action-level `foreach`. + - [x] Test complementary nextest and legacy branches select exactly one action. - - [ ] Test absent-command fallback without invoking `shell()`. - - [ ] Test `deps` lowering in the IR and emitted Ninja build statements. - - Note: the manifest-expansion building blocks already exist in - `src/manifest/expand_test_cases/action_condition_cases.rs` and the BDD - scenarios in `tests/bdd/steps/conditional_manifest.rs`. The outstanding gap - is end-to-end coverage that traces a conditionally selected action through - `deps` lowering into emitted Ninja, plus the named nextest-versus-legacy - scenario (existing tests use a generic `preferred-tool`). + - [x] Test absent-command fallback without invoking `shell()`. + - [x] Test `deps` lowering in the IR and emitted Ninja build statements. - [ ] 3.14.6. Add rule-level `deps_from` for compiler dependency imports. Requires 3.14.3. See [netsuke-design.md §2.3](netsuke-design.md#planned-compiler-dependency-import). diff --git a/docs/users-guide.md b/docs/users-guide.md index 72dbd2c44..fef74bb85 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -627,10 +627,14 @@ netsuke_version: "1.0.0" actions: - name: test-fast command: "cargo nextest run" + deps: + - config/test-profile.toml when: command_available("cargo-nextest") - name: test-fast command: "cargo test" + deps: + - config/test-profile.toml when: not command_available("cargo-nextest") targets: [] @@ -639,6 +643,12 @@ defaults: - test-fast ``` +Netsuke evaluates both guards while loading the manifest, without running +either recipe, so exactly one `test-fast` action enters the build graph. The +selected action's `deps` become Ninja implicit dependencies: changes to +`config/test-profile.toml` make the action stale, but the path is not appended +to `cargo nextest run` or `cargo test` as a recipe argument. + Both helpers accept: - `all=true`: return all `which` matches. It does not change the boolean result From 9f088df0f3a1b8eedaefee19468501dc86de3496 Mon Sep 17 00:00:00 2001 From: leynos Date: Wed, 29 Jul 2026 02:51:11 +0200 Subject: [PATCH 14/34] Record documentation milestone completion Mark Stage C complete after its full gate stack and CodeRabbit review passed, leaving only final branch-wide validation and publication. --- ...overage-for-conditional-action-dependency-manifests.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md index 33ace99b7..4a9146e05 100644 --- a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md +++ b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md @@ -167,8 +167,8 @@ escalation, not workarounds. dependencies after reverting the throwaway spike. - [x] (2026-07-28 23:14Z) Stage B: added B.1–B.5 tests and fixtures with a passing focused run and assertion-level sabotage evidence for each. -- [ ] Stage C: documentation updates (users-guide, developers-guide, - component architecture, ADR if warranted). +- [x] (2026-07-29 00:49Z) Stage C: updated the users' guide, developers' guide, + component design, and roadmap; recorded why no ADR is warranted. - [ ] Stage D: full gate run, CodeRabbit review, roadmap tick. (Timestamps to be added as work proceeds.) @@ -1042,3 +1042,7 @@ Skills to load while implementing: because this work pins existing boundaries rather than creating a new architectural decision. Stage C remains pending until its gates and CodeRabbit review pass. + +- 2026-07-29 — Completed Stage C after the full gate stack and CodeRabbit + returned clean. Remaining work is Stage D final branch-wide validation, + review, publication, and ExecPlan closure. From f82f424aba82fd1bee3291a52edaa22fb4396ea3 Mon Sep 17 00:00:00 2001 From: leynos Date: Wed, 29 Jul 2026 02:57:06 +0200 Subject: [PATCH 15/34] Close conditional coverage ExecPlan Record the final branch-wide gate results, clean CodeRabbit review, and completed roadmap status after all implementation milestones passed. --- ...or-conditional-action-dependency-manifests.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md index 4a9146e05..638c04aba 100644 --- a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md +++ b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md @@ -4,7 +4,7 @@ This ExecPlan (execution plan) is a living document. The sections `Constraints`, `Tolerances`, `Risks`, `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. -Status: IN PROGRESS +Status: COMPLETE ## Purpose / big picture @@ -169,7 +169,8 @@ escalation, not workarounds. passing focused run and assertion-level sabotage evidence for each. - [x] (2026-07-29 00:49Z) Stage C: updated the users' guide, developers' guide, component design, and roadmap; recorded why no ADR is warranted. -- [ ] Stage D: full gate run, CodeRabbit review, roadmap tick. +- [x] (2026-07-29 00:55Z) Stage D: passed the full gate stack and final + CodeRabbit review; roadmap item 3.14.5 is complete. (Timestamps to be added as work proceeds.) @@ -384,6 +385,13 @@ states the manifest/stdlib port boundary in the design, and marks roadmap item 3.14.5 and its four sub-items complete. No ADR is warranted because runtime architecture and behaviour are unchanged. +The completed change is test-and-documentation only. It adds deterministic +real-resolver unit coverage, selection-time impurity coverage, conditional IR +dependency assertions, a reviewed Ninja snapshot with real-Ninja validation, +and one combined BDD scenario. All five focused sabotage checks failed at the +intended assertion. The final branch-wide formatting, documentation, type, +lint, and test gates passed, and CodeRabbit reported no actionable findings. + ## Context and orientation This section assumes no prior knowledge of the repository. @@ -1046,3 +1054,7 @@ Skills to load while implementing: - 2026-07-29 — Completed Stage C after the full gate stack and CodeRabbit returned clean. Remaining work is Stage D final branch-wide validation, review, publication, and ExecPlan closure. + +- 2026-07-29 — Completed Stage D and closed the ExecPlan. The final branch-wide + gate stack passed with snapshot mutation disabled, and CodeRabbit returned + zero actionable findings across the complete committed diff. From 26ad22c674a51e780ed888e2a36ec1eb0ed322ab Mon Sep 17 00:00:00 2001 From: leynos Date: Fri, 14 Aug 2026 23:59:21 +0200 Subject: [PATCH 16/34] Restore rebase validation compatibility Rebuild `Cargo.lock` from the rebased manifests and preserve the existing time-offset contract against the updated dependency set. Update affected rendered-boolean assertions and apply the current Rust lint policy, including capability-scoped fixture filesystem access, so the rebased branch remains fully validated. --- src/diagnostic_json_tests.rs | 2 +- .../command_available_selection_cases.rs | 21 +++++---- .../expand_test_cases/structure_cases.rs | 3 +- src/manifest/glob/tests/pattern.rs | 2 +- src/stdlib/network/tests_support.rs | 3 +- src/stdlib/time/mod.rs | 8 +++- src/stdlib/which/lookup/tests.rs | 2 +- tests/ast_tests/string_or_list.rs | 4 +- tests/bdd/steps/cli_verify.rs | 11 ++--- tests/bdd/steps/fs.rs | 4 +- tests/bdd/steps/manifest_command_tests.rs | 6 +-- tests/bdd/steps/ninja.rs | 4 +- tests/bdd/steps/stdlib/workspace.rs | 8 +++- tests/cli_tests/merge.rs | 2 +- tests/cli_tests/parsing.rs | 6 +-- tests/documentation_examples_e2e_tests.rs | 4 +- tests/ir_from_manifest_tests.rs | 12 ++--- tests/locale_resolution_tests.rs | 5 +- tests/ninja_gen_integration_tests.rs | 8 ++-- tests/std_filter_tests/command_filters/mod.rs | 8 +++- tests/std_filter_tests/support.rs | 2 +- tests/stdlib_expanduser_filter_tests.rs | 2 +- tests/stdlib_which_tests.rs | 46 +++++++++---------- tests/stdlib_workspace_switch_tests.rs | 4 +- tests/which_diagnostic_snapshot_tests.rs | 2 +- 25 files changed, 87 insertions(+), 92 deletions(-) diff --git a/src/diagnostic_json_tests.rs b/src/diagnostic_json_tests.rs index 5230046a1..ca4e375a8 100644 --- a/src/diagnostic_json_tests.rs +++ b/src/diagnostic_json_tests.rs @@ -139,7 +139,7 @@ fn render_plain_error_json_records_cause_chain() -> Result<()> { .get("labels") .context("labels should be present")?; - ensure!(schema_version == 1, "schema version should be stable",); + ensure!(schema_version == 1, "schema version should be stable"); ensure!( generator_name == "netsuke", "generator name should be present", diff --git a/src/manifest/expand_test_cases/command_available_selection_cases.rs b/src/manifest/expand_test_cases/command_available_selection_cases.rs index 81e4c0d33..33c006dfd 100644 --- a/src/manifest/expand_test_cases/command_available_selection_cases.rs +++ b/src/manifest/expand_test_cases/command_available_selection_cases.rs @@ -4,6 +4,8 @@ use std::ffi::OsString; use anyhow::{Context, Result, anyhow}; use camino::{Utf8Path, Utf8PathBuf}; +#[cfg(unix)] +use cap_std::fs::PermissionsExt; use cap_std::{ambient_authority, fs_utf8::Dir}; use googletest::prelude::*; use minijinja::Environment; @@ -129,10 +131,12 @@ fn string_field<'a>(value: &'a ManifestValue, field: &str) -> Result<&'a str> { } fn write_tool(root: &Utf8Path, name: &str) -> Result<()> { - let path = root.join(tool_filename(name)); - std::fs::write(path.as_std_path(), script_contents()) + let dir = Dir::open_ambient_dir(root, ambient_authority()) + .with_context(|| format!("open fixture directory {root}"))?; + let path = Utf8PathBuf::from(tool_filename(name)); + dir.write(&path, script_contents()) .with_context(|| format!("write fixture tool {path}"))?; - mark_executable(&path) + mark_executable(&dir, &path) } fn tool_filename(name: &str) -> String { @@ -155,18 +159,17 @@ const fn script_contents() -> &'static [u8] { } #[cfg(unix)] -fn mark_executable(path: &Utf8Path) -> Result<()> { - use std::os::unix::fs::PermissionsExt; - - let mut permissions = std::fs::metadata(path.as_std_path()) +fn mark_executable(dir: &Dir, path: &Utf8Path) -> Result<()> { + let mut permissions = dir + .metadata(path) .with_context(|| format!("stat {path}"))? .permissions(); permissions.set_mode(0o755); - std::fs::set_permissions(path.as_std_path(), permissions) + dir.set_permissions(path, permissions) .with_context(|| format!("chmod {path}")) } #[cfg(not(unix))] -fn mark_executable(_path: &Utf8Path) -> Result<()> { +fn mark_executable(_dir: &Dir, _path: &Utf8Path) -> Result<()> { Ok(()) } diff --git a/src/manifest/expand_test_cases/structure_cases.rs b/src/manifest/expand_test_cases/structure_cases.rs index 164d76009..ae3ed277c 100644 --- a/src/manifest/expand_test_cases/structure_cases.rs +++ b/src/manifest/expand_test_cases/structure_cases.rs @@ -26,8 +26,7 @@ fn expand_foreach_preserves_object_key_order() -> Result<()> { let keys: Vec<&str> = map.keys().map(String::as_str).collect(); anyhow::ensure!( keys == ["name", "vars", "after"], - "key order should remain stable: {:?}", - keys + "key order should remain stable: {keys:?}" ); } Ok(()) diff --git a/src/manifest/glob/tests/pattern.rs b/src/manifest/glob/tests/pattern.rs index b93fefdb0..42763d5c9 100644 --- a/src/manifest/glob/tests/pattern.rs +++ b/src/manifest/glob/tests/pattern.rs @@ -13,7 +13,7 @@ use test_support::fluent::normalize_fluent_isolates; /// Helper to assert that a pattern produces a syntax error. fn assert_syntax_error(pattern: &str, context_msg: &str) -> Result<()> { match validate_brace_matching(pattern) { - Ok(()) => Err(anyhow!("{}", context_msg)), + Ok(()) => Err(anyhow!("{context_msg}")), Err(err) => { ensure!( err.kind() == ErrorKind::SyntaxError, diff --git a/src/stdlib/network/tests_support.rs b/src/stdlib/network/tests_support.rs index 1b65ee315..57e28a160 100644 --- a/src/stdlib/network/tests_support.rs +++ b/src/stdlib/network/tests_support.rs @@ -78,8 +78,7 @@ pub(super) fn assert_cache_entry_exists( let entry = workspace.join(cache_relative).join(entry_name); ensure!( fs::exists(entry.as_std_path()), - "entry {} should exist", - entry + "entry {entry} should exist" ); Ok(()) } diff --git a/src/stdlib/time/mod.rs b/src/stdlib/time/mod.rs index bff7beeb8..2e2672b15 100644 --- a/src/stdlib/time/mod.rs +++ b/src/stdlib/time/mod.rs @@ -79,7 +79,13 @@ fn parse_offset(raw: &str) -> Result { return Err(invalid_offset(raw)); } - UtcOffset::parse(trimmed, OFFSET_FMT).map_err(|_| invalid_offset(raw)) + let parsed = UtcOffset::parse(trimmed, OFFSET_FMT).map_err(|_| invalid_offset(raw))?; + // `time` accepts offsets beyond a civil day; templates expose ISO 8601 offsets. + if parsed.whole_hours().abs() >= 24 { + return Err(invalid_offset(raw)); + } + + Ok(parsed) } /// Build the invalid-offset error for the given string. diff --git a/src/stdlib/which/lookup/tests.rs b/src/stdlib/which/lookup/tests.rs index 1bb2737a9..dac046e7d 100644 --- a/src/stdlib/which/lookup/tests.rs +++ b/src/stdlib/which/lookup/tests.rs @@ -17,7 +17,7 @@ impl TempWorkspace { fn new() -> Result { let tempdir = TempDir::new().context("create tempdir")?; let root = Utf8PathBuf::from_path_buf(tempdir.path().to_path_buf()) - .map_err(|path| anyhow!("utf8 path required, got {:?}", path))?; + .map_err(|path| anyhow!("utf8 path required, got {path:?}"))?; Ok(Self { root, _tempdir: tempdir, diff --git a/tests/ast_tests/string_or_list.rs b/tests/ast_tests/string_or_list.rs index e1bc736f4..88c56f3d5 100644 --- a/tests/ast_tests/string_or_list.rs +++ b/tests/ast_tests/string_or_list.rs @@ -68,9 +68,7 @@ fn string_or_list_variants() -> Result<()> { let expected = vec!["hello".to_owned(), "world".to_owned()]; ensure!( names == &expected, - "unexpected names: got {:?}, expected {:?}", - names, - expected + "unexpected names: got {names:?}, expected {expected:?}" ); } other => bail!("Expected List variant, got: {other:?}"), diff --git a/tests/bdd/steps/cli_verify.rs b/tests/bdd/steps/cli_verify.rs index 93e252b75..b505c13c2 100644 --- a/tests/bdd/steps/cli_verify.rs +++ b/tests/bdd/steps/cli_verify.rs @@ -150,8 +150,7 @@ pub(super) fn verify_cli_policy_allows(world: &TestWorld, url: &UrlString) -> Re let parsed = url.parse().context("parse URL for CLI policy check")?; ensure!( policy.evaluate(&parsed).is_ok(), - "expected CLI policy to allow {}", - url, + "expected CLI policy to allow {url}", ); Ok(()) } @@ -164,14 +163,13 @@ pub(super) fn verify_cli_policy_rejects( let policy = cli_network_policy(world)?; let parsed = url.parse().context("parse URL for CLI policy check")?; let Err(err) = policy.evaluate(&parsed) else { - bail!("expected CLI policy to reject {}", url); + bail!("expected CLI policy to reject {url}"); }; let normalized_error = normalize_fluent_isolates(&err.to_string()); let normalized_message = normalize_fluent_isolates(message.as_str()); ensure!( normalized_error.contains(&normalized_message), - "expected error to mention '{}', got '{err}'", - message, + "expected error to mention '{message}', got '{err}'", ); Ok(()) } @@ -201,8 +199,7 @@ pub(super) fn verify_error_contains(world: &TestWorld, fragment: &ErrorFragment) let normalized_fragment = normalize_fluent_isolates(fragment.as_str()); ensure!( normalized_error.contains(&normalized_fragment), - "Error message '{error}' does not contain expected '{}'", - fragment + "Error message '{error}' does not contain expected '{fragment}'" ); Ok(()) } diff --git a/tests/bdd/steps/fs.rs b/tests/bdd/steps/fs.rs index a5f4dc438..1d880631d 100644 --- a/tests/bdd/steps/fs.rs +++ b/tests/bdd/steps/fs.rs @@ -15,7 +15,7 @@ use std::os::unix::fs::FileTypeExt; fn setup_workspace() -> Result<(tempfile::TempDir, Utf8PathBuf, Dir)> { let temp = tempfile::tempdir().context("create tempdir for file-type workspace")?; let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("workspace path is not valid UTF-8: {path:?}"))?; + .map_err(|path| anyhow!("workspace path is not valid UTF-8: {}", path.display()))?; let handle = Dir::open_ambient_dir(&root, ambient_authority()) .context("open ambient dir for file-type workspace")?; Ok((temp, root, handle)) @@ -55,7 +55,7 @@ fn find_block_device_fallback() -> Result { if file_type.is_block_device() { let path = entry.path(); let utf8 = Utf8PathBuf::from_path_buf(path) - .map_err(|p| anyhow!("block device path is not valid UTF-8: {p:?}"))?; + .map_err(|p| anyhow!("block device path is not valid UTF-8: {}", p.display()))?; return Ok(utf8); } } diff --git a/tests/bdd/steps/manifest_command_tests.rs b/tests/bdd/steps/manifest_command_tests.rs index 278fbedde..c959f8013 100644 --- a/tests/bdd/steps/manifest_command_tests.rs +++ b/tests/bdd/steps/manifest_command_tests.rs @@ -41,8 +41,7 @@ fn world_env_vars_with_value_are_applied(prepared_world: Result) -> R let val = env_value(&cmd, "NETSUKE_TEST_FLAG").expect("NETSUKE_TEST_FLAG should be present"); ensure!( val == OsStr::new("enabled"), - "expected NETSUKE_TEST_FLAG to be 'enabled', got {:?}", - val + "expected NETSUKE_TEST_FLAG to be 'enabled', got {val:?}" ); Ok(()) } @@ -77,8 +76,7 @@ fn scenario_path_override_is_forwarded_and_netsuke_executable_is_used( env_value(&cmd, "PATH").expect("PATH should be explicitly forwarded to the command"); ensure!( path_val == OsStr::new("/fake/bin"), - "expected PATH to be '/fake/bin', got {:?}", - path_val + "expected PATH to be '/fake/bin', got {path_val:?}" ); // Command should use the resolved netsuke_executable(), not rely on PATH lookup. diff --git a/tests/bdd/steps/ninja.rs b/tests/bdd/steps/ninja.rs index cf8e86e8f..97af8edd0 100644 --- a/tests/bdd/steps/ninja.rs +++ b/tests/bdd/steps/ninja.rs @@ -53,9 +53,7 @@ fn compare_tokens(actual: &[String], expected: &TokenList) -> Result<()> { let expected_vec = expected.to_vec(); ensure!( actual == expected_vec, - "expected tokens {:?}, got {:?}", - expected_vec, - actual + "expected tokens {expected_vec:?}, got {actual:?}" ); Ok(()) } diff --git a/tests/bdd/steps/stdlib/workspace.rs b/tests/bdd/steps/stdlib/workspace.rs index db5a6cb9f..cd569e8eb 100644 --- a/tests/bdd/steps/stdlib/workspace.rs +++ b/tests/bdd/steps/stdlib/workspace.rs @@ -34,8 +34,12 @@ pub(crate) fn ensure_workspace(world: &TestWorld) -> Result { return Ok(root); } let temp = tempfile::tempdir().context("create stdlib workspace")?; - let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("stdlib workspace path is not valid UTF-8: {path:?}"))?; + let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()).map_err(|path| { + anyhow!( + "stdlib workspace path is not valid UTF-8: {}", + path.display() + ) + })?; let handle = Dir::open_ambient_dir(&root, ambient_authority()) .context("open stdlib workspace directory")?; handle diff --git a/tests/cli_tests/merge.rs b/tests/cli_tests/merge.rs index eeae5c55d..45ffe457d 100644 --- a/tests/cli_tests/merge.rs +++ b/tests/cli_tests/merge.rs @@ -152,7 +152,7 @@ fn cli_merge_layers_respects_precedence_and_appends_lists( merged.progress == ProgressPolicy::Never, "CLI layer should override progress setting", ); - ensure!(merged.json, "CLI layer should override json setting",); + ensure!(merged.json, "CLI layer should override json setting"); ensure!( merged.locale.as_deref() == Some("en-US"), "file layer should populate locale when CLI does not override", diff --git a/tests/cli_tests/parsing.rs b/tests/cli_tests/parsing.rs index 3b8426d55..e17ffdf68 100644 --- a/tests/cli_tests/parsing.rs +++ b/tests/cli_tests/parsing.rs @@ -192,7 +192,7 @@ fn parse_cli(#[case] case: CliCase) -> Result<()> { cli.locale.as_deref() == case.locale, "locale should match input", ); - ensure!(cli.json == case.json, "json flag should match input",); + ensure!(cli.json == case.json, "json flag should match input"); ensure!(cli.no_input(), "no-input should remain enabled"); ensure!( cli.fetch_allow_scheme == case.allow_scheme, @@ -226,8 +226,8 @@ fn parse_cli(#[case] case: CliCase) -> Result<()> { cli.fetch_default_deny == case.default_deny, "default-deny flag should match input", ); - ensure!(cli.color == case.color, "color policy should match input",); - ensure!(cli.emoji == case.emoji, "emoji policy should match input",); + ensure!(cli.color == case.color, "color policy should match input"); + ensure!(cli.emoji == case.emoji, "emoji policy should match input"); ensure!( cli.progress == case.progress, "progress policy should match input", diff --git a/tests/documentation_examples_e2e_tests.rs b/tests/documentation_examples_e2e_tests.rs index 94f1f63b2..9ec3e3faa 100644 --- a/tests/documentation_examples_e2e_tests.rs +++ b/tests/documentation_examples_e2e_tests.rs @@ -313,7 +313,7 @@ fn stdlib_file_test_example_identifies_unix_fixture_types() -> Result<()> { assert_success(&run, "stdlib file-test example")?; ensure!( test_fs::read_to_string(workspace.path().join("stdlib-file-tests.txt"))? - == "true\ntrue\ntrue\ntrue\nfalse\ntrue\ntrue\n", + == "True\nTrue\nTrue\nTrue\nFalse\nTrue\nTrue\n", "stdlib file tests should report the documented Unix fixture types" ); Ok(()) @@ -386,7 +386,7 @@ fn stdlib_host_context_example_uses_controlled_process_state() -> Result<()> { ensure!( output == format!( - "HELLO\nbeta\n{stub_directory}/guide-tool\ntrue\nfrom-env\nfixtures/a.txt,fixtures/b.txt\n" + "HELLO\nbeta\n{stub_directory}/guide-tool\nTrue\nfrom-env\nfixtures/a.txt,fixtures/b.txt\n" ), "stdlib host-context output should use only controlled fixtures: {output}" ); diff --git a/tests/ir_from_manifest_tests.rs b/tests/ir_from_manifest_tests.rs index bd4a769c7..dad08caf9 100644 --- a/tests/ir_from_manifest_tests.rs +++ b/tests/ir_from_manifest_tests.rs @@ -481,9 +481,7 @@ fn manifest_error_cases( ) => { ensure!( outputs == exp_outputs, - "unexpected duplicate outputs: got {:?}, expected {:?}", - outputs, - exp_outputs + "unexpected duplicate outputs: got {outputs:?}, expected {exp_outputs:?}" ); } ( @@ -501,9 +499,7 @@ fn manifest_error_cases( ); ensure!( rules == exp_rules, - "unexpected rules: got {:?}, expected {:?}", - rules, - exp_rules + "unexpected rules: got {rules:?}, expected {exp_rules:?}" ); } (IrGenError::EmptyRule { target_name, .. }, ExpectedError::EmptyRule(exp_target)) => { @@ -537,9 +533,7 @@ fn manifest_error_cases( actual.sort(); ensure!( actual == expected_cycle, - "unexpected dependency cycle: got {:?}, expected {:?}", - actual, - expected_cycle + "unexpected dependency cycle: got {actual:?}, expected {expected_cycle:?}" ); } (other, exp) => bail!("expected {exp:?} but got {other:?}"), diff --git a/tests/locale_resolution_tests.rs b/tests/locale_resolution_tests.rs index 3d0120b18..690fd6048 100644 --- a/tests/locale_resolution_tests.rs +++ b/tests/locale_resolution_tests.rs @@ -27,10 +27,7 @@ fn normalize_locale_tag_handles_common_formats( let normalized = normalize_locale_tag(raw); ensure!( normalized.as_deref() == expected, - "expected {:?} to normalize to {:?}, got {:?}", - raw, - expected, - normalized + "expected {raw:?} to normalize to {expected:?}, got {normalized:?}" ); Ok(()) } diff --git a/tests/ninja_gen_integration_tests.rs b/tests/ninja_gen_integration_tests.rs index 222ecee7f..58017481c 100644 --- a/tests/ninja_gen_integration_tests.rs +++ b/tests/ninja_gen_integration_tests.rs @@ -135,7 +135,7 @@ fn ninja_integration_tests( return Ok(()); }; let dir_path = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()) - .map_err(|path| anyhow::anyhow!("temp dir path {:?} is not UTF-8", path))?; + .map_err(|path| anyhow::anyhow!("temp dir path {path:?} is not UTF-8"))?; let NinjaIntegrationCase { action, @@ -153,8 +153,7 @@ fn ninja_integration_tests( if !matches!(&assertion, AssertionType::FileExists) { ensure!( output == target_name, - "expected edge output '{}' to match test target '{target_name}'", - output + "expected edge output '{output}' to match test target '{target_name}'" ); } let mut graph = BuildGraph::default(); @@ -185,8 +184,7 @@ fn ninja_integration_tests( .with_context(|| format!("check existence of {target_name}"))?; ensure!( exists, - "expected {} to exist after ninja invocation", - &target_name + "expected {target_name} to exist after ninja invocation" ); } AssertionType::FileContent(expected) => { diff --git a/tests/std_filter_tests/command_filters/mod.rs b/tests/std_filter_tests/command_filters/mod.rs index 79de9d242..eade74612 100644 --- a/tests/std_filter_tests/command_filters/mod.rs +++ b/tests/std_filter_tests/command_filters/mod.rs @@ -37,8 +37,12 @@ impl CommandFixture { F: FnOnce(StdlibConfig) -> Result, { let temp = tempdir().context("create command fixture tempdir")?; - let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("command fixture root is not valid UTF-8: {path:?}"))?; + let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()).map_err(|path| { + anyhow!( + "command fixture root is not valid UTF-8: {}", + path.display() + ) + })?; let helper_dir = Dir::open_ambient_dir(&root, ambient_authority()) .context("open command fixture directory")?; let helper = compiler(&helper_dir, &root, binary)?; diff --git a/tests/std_filter_tests/support.rs b/tests/std_filter_tests/support.rs index a79c1c6b7..9392b3a7d 100644 --- a/tests/std_filter_tests/support.rs +++ b/tests/std_filter_tests/support.rs @@ -70,7 +70,7 @@ pub(crate) mod fallible { pub(crate) fn filter_workspace() -> Result { let temp = tempdir().context("create standard filter workspace")?; let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("workspace path is not valid UTF-8: {path:?}"))?; + .map_err(|path| anyhow!("workspace path is not valid UTF-8: {}", path.display()))?; let dir = Dir::open_ambient_dir(&root, ambient_authority()) .context("open filter workspace directory")?; dir.write("file", b"data") diff --git a/tests/stdlib_expanduser_filter_tests.rs b/tests/stdlib_expanduser_filter_tests.rs index 95b2d9892..bd9f193ce 100644 --- a/tests/stdlib_expanduser_filter_tests.rs +++ b/tests/stdlib_expanduser_filter_tests.rs @@ -24,7 +24,7 @@ struct StdlibWorkspace { fn stdlib_workspace() -> Result { let temp = tempfile::tempdir().context("create temp workspace")?; let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("temp path should be UTF-8: {path:?}"))?; + .map_err(|path| anyhow!("temp path should be UTF-8: {}", path.display()))?; Ok(StdlibWorkspace { _temp: temp, root }) } diff --git a/tests/stdlib_which_tests.rs b/tests/stdlib_which_tests.rs index 58b96997e..adbbd3bdc 100644 --- a/tests/stdlib_which_tests.rs +++ b/tests/stdlib_which_tests.rs @@ -29,7 +29,7 @@ fn stdlib_env(root: &Utf8Path, path_override: OsString) -> Result Result { let temp = tempfile::tempdir().context("create temp workspace")?; let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("temp path should be UTF-8: {path:?}"))?; + .map_err(|path| anyhow!("temp path should be UTF-8: {}", path.display()))?; Ok(StdlibWorkspace { _temp: temp, root }) } @@ -117,7 +117,7 @@ fn command_available_returns_true_for_path_match( let output = env.render_str("{{ command_available('helper') }}", context! {})?; - ensure!(output == "true", "expected true, got {output}"); + ensure!(output == "True", "expected True, got {output}"); Ok(()) } @@ -133,7 +133,7 @@ fn command_available_returns_false_for_missing_command( context! {}, )?; - ensure!(output == "false", "expected false, got {output}"); + ensure!(output == "False", "expected False, got {output}"); Ok(()) } @@ -147,7 +147,7 @@ fn command_available_returns_false_for_missing_absolute_path( let output = render_command_available(&env, missing.as_path(), "")?; - ensure!(output == "false", "expected false, got {output}"); + ensure!(output == "False", "expected False, got {output}"); Ok(()) } @@ -160,7 +160,7 @@ fn command_available_returns_false_for_missing_relative_path( let output = env.render_str("{{ command_available('./missing') }}", context! {})?; - ensure!(output == "false", "expected false, got {output}"); + ensure!(output == "False", "expected False, got {output}"); Ok(()) } @@ -213,7 +213,7 @@ fn command_available_returns_true_for_absolute_path( let output = render_command_available(&env, tool.as_path(), "")?; - ensure!(output == "true", "expected true, got {output}"); + ensure!(output == "True", "expected True, got {output}"); Ok(()) } @@ -231,13 +231,13 @@ fn command_available_returns_true_for_canonical_symlink( let output = render_command_available(&env, link.as_path(), ", canonical=true")?; - ensure!(output == "true", "expected true, got {output}"); + ensure!(output == "True", "expected True, got {output}"); Ok(()) } #[rstest] -#[case::present("workspace-helper", "true")] -#[case::absent("missing-helper", "false")] +#[case::present("workspace-helper", "True")] +#[case::absent("missing-helper", "False")] fn command_available_uses_workspace_fallback_when_path_is_empty( #[case] command: &str, #[case] expected: &str, @@ -272,19 +272,19 @@ fn command_available_fresh_bypasses_cached_success( context! {}, )?; - ensure!(first == "true", "expected initial true, got {first}"); - ensure!(cached == "true", "expected cached true, got {cached}"); - ensure!(fresh == "false", "expected fresh false, got {fresh}"); + ensure!(first == "True", "expected initial True, got {first}"); + ensure!(cached == "True", "expected cached True, got {cached}"); + ensure!(fresh == "False", "expected fresh False, got {fresh}"); Ok(()) } #[rstest] -#[case::auto_present("auto", true, "true")] -#[case::auto_absent("auto", false, "false")] -#[case::always_present("always", true, "true")] -#[case::always_absent("always", false, "false")] -#[case::never_present("never", true, "false")] -#[case::never_absent("never", false, "false")] +#[case::auto_present("auto", true, "True")] +#[case::auto_absent("auto", false, "False")] +#[case::always_present("always", true, "True")] +#[case::always_absent("always", false, "False")] +#[case::never_present("never", true, "False")] +#[case::never_absent("never", false, "False")] fn command_available_honours_cwd_mode( #[case] cwd_mode: &str, #[case] present: bool, @@ -305,10 +305,10 @@ fn command_available_honours_cwd_mode( } #[rstest] -#[case::present(false, "true")] -#[case::present_all(true, "true")] -#[case::absent(false, "false")] -#[case::absent_all(true, "false")] +#[case::present(false, "True")] +#[case::present_all(true, "True")] +#[case::absent(false, "False")] +#[case::absent_all(true, "False")] fn command_available_all_kwarg_does_not_affect_bool( #[case] all: bool, #[case] expected: &str, @@ -320,7 +320,7 @@ fn command_available_all_kwarg_does_not_affect_bool( &workspace_fixture.root, path_override(&[workspace_fixture.root.join("bin")])?, )?; - let command = if expected == "true" { + let command = if expected == "True" { "all-helper" } else { "missing-helper" diff --git a/tests/stdlib_workspace_switch_tests.rs b/tests/stdlib_workspace_switch_tests.rs index 3a45f5735..4aa0ce13c 100644 --- a/tests/stdlib_workspace_switch_tests.rs +++ b/tests/stdlib_workspace_switch_tests.rs @@ -61,13 +61,13 @@ fn the_switch_gates_the_workspace_fallback_end_to_end() -> Result<()> { let enabled = probe_verdict(&temp, None)?; ensure!( - enabled.contains("probe-true.txt"), + enabled.contains("probe-True.txt"), "with the switch unset the workspace fallback should find the probe:\n{enabled}" ); let disabled = probe_verdict(&temp, Some("0"))?; ensure!( - disabled.contains("probe-false.txt"), + disabled.contains("probe-False.txt"), "with the switch set to 0 the fallback must not run:\n{disabled}" ); Ok(()) diff --git a/tests/which_diagnostic_snapshot_tests.rs b/tests/which_diagnostic_snapshot_tests.rs index 303a5f784..8ab9dcf6c 100644 --- a/tests/which_diagnostic_snapshot_tests.rs +++ b/tests/which_diagnostic_snapshot_tests.rs @@ -18,7 +18,7 @@ struct SnapshotWorkspace { fn snapshot_workspace() -> Result { let temp = tempfile::tempdir().context("create snapshot workspace")?; let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("temp path should be UTF-8: {path:?}"))?; + .map_err(|path| anyhow!("temp path should be UTF-8: {}", path.display()))?; Ok(SnapshotWorkspace { _temp: temp, root }) } From 71cab4746424202569f54c54bfc793ed6e917223 Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 17 Aug 2026 00:24:59 +0200 Subject: [PATCH 17/34] Refresh rebase dependencies Retain the current development dependency declarations, regenerate `Cargo.lock`, and make the resulting test diagnostics compatible with the current lint rules. --- Cargo.toml | 2 -- tests/ninja_gen_command_list_integration_tests.rs | 2 +- tests/ninja_gen_command_list_process_integration_tests.rs | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3b9488ec1..2fc6d6400 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,8 +149,6 @@ thiserror = "1" time = { version = "0.3.44", features = ["formatting"] } tracing = "0.1" [dev-dependencies] -googletest = "0.14" -pretty_assertions = "1.4" rstest = "0.26.1" rstest-bdd = "0.5.0" rstest-bdd-macros = { version = "0.5.0", features = ["strict-compile-time-validation"] } diff --git a/tests/ninja_gen_command_list_integration_tests.rs b/tests/ninja_gen_command_list_integration_tests.rs index 7d618ae15..421d85fd4 100644 --- a/tests/ninja_gen_command_list_integration_tests.rs +++ b/tests/ninja_gen_command_list_integration_tests.rs @@ -156,7 +156,7 @@ fn open_temp_workspace(dir: &TempDir) -> Result { fn temp_workspace_path(dir: &TempDir) -> Result { let dir_path = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()) - .map_err(|path| anyhow::anyhow!("temp dir path {path:?} is not UTF-8"))?; + .map_err(|path| anyhow::anyhow!("temp dir path {} is not UTF-8", path.display()))?; Ok(dir_path) } diff --git a/tests/ninja_gen_command_list_process_integration_tests.rs b/tests/ninja_gen_command_list_process_integration_tests.rs index 7cefe992d..862a22baa 100644 --- a/tests/ninja_gen_command_list_process_integration_tests.rs +++ b/tests/ninja_gen_command_list_process_integration_tests.rs @@ -36,7 +36,7 @@ fn run_command_list( return Ok(None); }; let path = Utf8PathBuf::from_path_buf(tempdir.path().to_path_buf()) - .map_err(|path| anyhow::anyhow!("temp dir path {:?} is not UTF-8", path))?; + .map_err(|path| anyhow::anyhow!("temp dir path {} is not UTF-8", path.display()))?; let action = Action { recipe: Recipe::Command { command: StringOrList::List(entries.iter().map(|entry| (*entry).into()).collect()), From f4e046d2a95ceeb03765f50b65c951e5017fd97c Mon Sep 17 00:00:00 2001 From: leynos Date: Fri, 21 Aug 2026 22:59:19 +0200 Subject: [PATCH 18/34] Parameterize manifest graph tests Cover the minimal and duplicate-rule fixtures through one shared test so their expected graph cardinalities remain explicit and consistent. --- tests/ir_from_manifest_tests.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/ir_from_manifest_tests.rs b/tests/ir_from_manifest_tests.rs index dad08caf9..450cc0855 100644 --- a/tests/ir_from_manifest_tests.rs +++ b/tests/ir_from_manifest_tests.rs @@ -543,3 +543,26 @@ fn manifest_error_cases( #[path = "ir_from_manifest_tests/dependency_order.rs"] mod dependency_order; + +#[rstest] +#[case::minimal_manifest("tests/data/minimal.yml", 1, 1)] +#[case::duplicate_rules("tests/data/duplicate_rules.yml", 2, 2)] +fn manifest_file_generates_expected_graph( + #[case] manifest_path: &str, + #[case] expected_actions: usize, + #[case] expected_targets: usize, +) -> Result<()> { + let manifest = manifest::from_path(manifest_path)?; + let graph = BuildGraph::from_manifest(&manifest).context("expected graph generation")?; + let actual_actions = graph.actions.len(); + ensure!( + actual_actions == expected_actions, + "expected {expected_actions} actions, got {actual_actions}" + ); + let actual_targets = graph.targets.len(); + ensure!( + actual_targets == expected_targets, + "expected {expected_targets} targets, got {actual_targets}" + ); + Ok(()) +} From 70939de4de523bf9c6437eb18548c5d1e721ff12 Mon Sep 17 00:00:00 2001 From: leynos Date: Fri, 21 Aug 2026 23:13:06 +0200 Subject: [PATCH 19/34] Refresh rebase lockfile Regenerate `Cargo.lock` from the merged manifest and keep the refreshed toolchain's diagnostic assertions lint-clean. --- tests/advanced_usage/config_precedence.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/advanced_usage/config_precedence.rs b/tests/advanced_usage/config_precedence.rs index 1aff48ee8..947feb497 100644 --- a/tests/advanced_usage/config_precedence.rs +++ b/tests/advanced_usage/config_precedence.rs @@ -106,16 +106,14 @@ fn assert_config_metrics_snapshot(stderr: &str) -> Result<()> { let expected_record = expected_counter.record; ensure!( contains_metric_record(snapshot, expected_counter), - "expected counter {:?} in snapshot: {snapshot}", - expected_record, + "expected counter {expected_record:?} in snapshot: {snapshot}", ); } for expected_histogram in expected_histograms { let expected_record = expected_histogram.record; ensure!( contains_non_empty_histogram_record(snapshot, expected_histogram), - "expected histogram {:?} in snapshot: {snapshot}", - expected_record, + "expected histogram {expected_record:?} in snapshot: {snapshot}", ); } Ok(()) From 28bb8ce3c809b139b771839a8260c4703a007412 Mon Sep 17 00:00:00 2001 From: leynos Date: Fri, 21 Aug 2026 23:19:11 +0200 Subject: [PATCH 20/34] Split conditional Ninja validation phases Keep the real-Ninja regression workflow intact while isolating workspace, command, selection, timestamp, and no-op responsibilities. --- tests/ninja_snapshot_tests.rs | 54 ++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/tests/ninja_snapshot_tests.rs b/tests/ninja_snapshot_tests.rs index a9c1d423b..056df6a31 100644 --- a/tests/ninja_snapshot_tests.rs +++ b/tests/ninja_snapshot_tests.rs @@ -9,11 +9,18 @@ use cap_std::{ambient_authority, fs_utf8::Dir}; use insta::{Settings, assert_snapshot}; use netsuke::{ir::BuildGraph, manifest, ninja_gen, stdlib::StdlibConfig}; use std::process::Command; -use std::{fs, io::Write, process::Command, time::Duration}; -use tempfile::tempdir; use test_support::ensure_binaries_available; use test_support::fs; + +//! End-to-end validation of Ninja file generation. +//! +//! These tests generate a Ninja file from a manifest, snapshot the +//! output using `insta`, and validate it with the real `ninja` +//! executable. The manifest uses a simple TOUCH rule so the build is +//! fast and deterministic. +}; + #[cfg(unix)] fn run_ok(cmd: &mut Command) -> Result { let out = cmd.output().context("failed to spawn command")?; @@ -338,6 +345,13 @@ fn validate_conditional_ninja(ninja_content: &str) -> Result<()> { return Ok(()); } + let (dir, build_file) = prepare_conditional_ninja_workspace(ninja_content)?; + assert_conditional_ninja_selection(&dir, &build_file)?; + mark_conditional_ninja_output_up_to_date(&dir)?; + assert_conditional_ninja_no_op(&dir, &build_file) +} + +fn prepare_conditional_ninja_workspace(ninja_content: &str) -> Result<(TempDir, PathBuf)> { let dir = tempdir().context("create temp dir for conditional Ninja validation")?; let build_file = dir.path().join("build.ninja"); fs::write(&build_file, ninja_content) @@ -353,20 +367,29 @@ fn validate_conditional_ninja(ninja_content: &str) -> Result<()> { .with_context(|| format!("write Ninja dependency {}", dependency_path.display()))?; } - let ninja_cmd = |args: &[&str]| { - let mut cmd = Command::new("ninja"); - cmd.arg("-f").arg(&build_file).args(args); - cmd.current_dir(dir.path()); - run_ok(&mut cmd) - }; - ninja_cmd(&["-t", "query", "fallback-alpha"])?; - ninja_cmd(&["-t", "query", "out/fallback"])?; - let first = ninja_cmd(&["-n", "out/fallback"])?; + Ok((dir, build_file)) +} + +fn run_conditional_ninja(dir: &TempDir, build_file: &Path, args: &[&str]) -> Result { + let mut cmd = Command::new("ninja"); + cmd.arg("-f").arg(build_file).args(args); + cmd.current_dir(dir.path()); + run_ok(&mut cmd) +} + +fn assert_conditional_ninja_selection(dir: &TempDir, build_file: &Path) -> Result<()> { + run_conditional_ninja(dir, build_file, &["-t", "query", "fallback-alpha"])?; + run_conditional_ninja(dir, build_file, &["-t", "query", "out/fallback"])?; + let first = run_conditional_ninja(dir, build_file, &["-n", "out/fallback"])?; ensure!( first.contains("echo fallback"), "expected dry run to reach selected target, got:\n{first}" ); + Ok(()) +} + +fn mark_conditional_ninja_output_up_to_date(dir: &TempDir) -> Result<()> { let output = dir.path().join("out/fallback"); let output_dir = output.parent().context("output path should have parent")?; fs::create_dir_all(output_dir) @@ -385,8 +408,13 @@ fn validate_conditional_ninja(ninja_content: &str) -> Result<()> { .with_context(|| format!("open output {}", output.display()))? .set_times(fs::FileTimes::new().set_modified(output_modified)) .with_context(|| format!("set output time {}", output.display()))?; - ninja_cmd(&["out/fallback"])?; - let second = ninja_cmd(&["-n", "out/fallback"])?; + + Ok(()) +} + +fn assert_conditional_ninja_no_op(dir: &TempDir, build_file: &Path) -> Result<()> { + run_conditional_ninja(dir, build_file, &["out/fallback"])?; + let second = run_conditional_ninja(dir, build_file, &["-n", "out/fallback"])?; ensure!( second.contains("no work to do"), "expected no-op second pass, got:\n{second}" From 33426ff655b1e9e497d18d1ed5a5d4e6b8bf11df Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 24 Aug 2026 02:09:51 +0200 Subject: [PATCH 21/34] Reconcile Ninja tests after rebase Regenerate `Cargo.lock` from the merged manifest and retain `main`'s Unix-only policy for native Ninja execution while preserving the conditional dependency regression coverage. --- Cargo.lock | 967 +++++++++++++++++----------------- tests/ninja_snapshot_tests.rs | 47 +- 2 files changed, 516 insertions(+), 498 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d057abcf6..942220b56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] @@ -19,9 +19,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -40,9 +40,9 @@ checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" [[package]] name = "anstream" -version = "0.6.19" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -55,50 +55,53 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] [[package]] name = "arraydeque" @@ -115,7 +118,7 @@ dependencies = [ "anstyle", "bstr", "libc", - "predicates 3.1.3", + "predicates 3.1.4", "predicates-core", "predicates-tree", "wait-timeout", @@ -132,15 +135,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -148,7 +151,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -192,18 +195,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "block-buffer" @@ -216,41 +210,41 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" dependencies = [ "memchr", "regex-automata", - "serde", + "serde_core", ] [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" [[package]] name = "camino" -version = "1.2.4" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" dependencies = [ "serde_core", ] [[package]] name = "cap-primitives" -version = "3.4.4" +version = "3.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1e394ed14f39f8bc26f59d4c0c010dbe7f0a1b9bafff451b1f98b67c8af62a" +checksum = "8e0bf07d379916947be6c4a07f43684153d710a2896c31f9e97781362895596c" dependencies = [ "ambient-authority", "fs-set-times", @@ -266,9 +260,9 @@ dependencies = [ [[package]] name = "cap-primitives" -version = "4.0.2" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdadbd7c002d3a484b35243669abdae85a0ebaded5a61117169dc3400f9a7ff0" +checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18" dependencies = [ "ambient-authority", "fs-set-times", @@ -284,12 +278,12 @@ dependencies = [ [[package]] name = "cap-std" -version = "3.4.4" +version = "3.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c0355ca583dd58f176c3c12489d684163861ede3c9efa6fd8bba314c984189" +checksum = "a59e59fa26472d29680ece6a9f8ee8b0551a719a33df2f5240bde065ecbddfd7" dependencies = [ "camino", - "cap-primitives 3.4.4", + "cap-primitives 3.4.6", "io-extras 0.18.4", "io-lifetimes 2.0.4", "rustix", @@ -297,12 +291,12 @@ dependencies = [ [[package]] name = "cap-std" -version = "4.0.2" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7281235d6e96d3544ca18bba9049be92f4190f8d923e3caef1b5f66cfa752608" +checksum = "c1ec78e242cfa2cfe276807ac2ecc00315a6c97786977414bcd1c3963b6c91b8" dependencies = [ "camino", - "cap-primitives 4.0.2", + "cap-primitives 4.0.3", "io-extras 0.19.0", "io-lifetimes 3.0.1", "rustix", @@ -310,25 +304,25 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.40" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", - "shlex 1.3.0", + "shlex", ] [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.5.60" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -348,9 +342,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -369,14 +363,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -387,9 +381,9 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clap_mangen" -version = "0.3.0" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82842b45bf9f6a3be090dd860095ac30728042c08e0d6261ca7259b5d850f07" +checksum = "211d617eaa4b735c96c9e0228fcbdb5120ef623f2b8cb67ffb84c3e02dbc28a4" dependencies = [ "clap", "roff", @@ -397,9 +391,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "console" @@ -409,7 +403,7 @@ checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", - "unicode-width 0.2.1", + "unicode-width 0.2.2", "windows-sys 0.61.2", ] @@ -434,15 +428,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.3.0" @@ -454,9 +439,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" dependencies = [ "cfg-if", ] @@ -476,16 +461,6 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "crypto-common" version = "0.2.2" @@ -507,12 +482,11 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.3" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -540,25 +514,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.6", -] - [[package]] name = "digest" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.1", + "block-buffer", "const-oid", - "crypto-common 0.2.2", + "crypto-common", ] [[package]] @@ -605,13 +569,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -628,9 +592,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.15.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "encode_unicode" @@ -652,19 +616,19 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "fastrand" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "figment" @@ -693,15 +657,15 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.3" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "flate2" -version = "1.1.4" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -747,7 +711,7 @@ dependencies = [ "intl_pluralrules", "rustc-hash", "self_cell", - "smallvec 1.15.1", + "smallvec 1.15.2", "unic-langid", ] @@ -767,7 +731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" dependencies = [ "memchr", - "thiserror 2.0.17", + "thiserror 2.0.20", ] [[package]] @@ -799,9 +763,12 @@ dependencies = [ [[package]] name = "fragile" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" +checksum = "8878864ba14bb86e818a412bfd6f18f9eabd4ec0f008a28e8f7eb61db532fcf9" +dependencies = [ + "futures-core", +] [[package]] name = "fs-set-times" @@ -826,15 +793,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -843,72 +810,72 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", "futures-macro", "futures-task", "pin-project-lite", - "pin-utils", "slab", ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "getrandom" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ - "typenum", - "version_check", + "cfg-if", + "libc", + "wasi", ] [[package]] name = "getrandom" -version = "0.2.16" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "r-efi 5.3.0", + "wasip2", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "r-efi 6.0.0", ] [[package]] @@ -930,9 +897,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "glob" @@ -965,9 +932,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash 0.1.5", ] @@ -1000,7 +967,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.15.5", ] [[package]] @@ -1017,9 +984,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ "typenum", ] @@ -1073,12 +1040,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1086,9 +1054,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -1099,57 +1067,53 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", - "smallvec 1.15.1", + "smallvec 1.15.2", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1164,15 +1128,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", - "smallvec 1.15.1", + "smallvec 1.15.2", "utf8_iter", ] [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1198,7 +1162,7 @@ checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" dependencies = [ "console", "portable-atomic", - "unicode-width 0.2.1", + "unicode-width 0.2.2", "unit-prefix", "web-time", ] @@ -1245,9 +1209,9 @@ dependencies = [ [[package]] name = "inventory" -version = "0.3.20" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" dependencies = [ "rustversion", ] @@ -1269,7 +1233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" dependencies = [ "io-lifetimes 3.0.1", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1284,22 +1248,11 @@ version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" -[[package]] -name = "io-uring" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is_ci" @@ -1309,9 +1262,9 @@ checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -1339,11 +1292,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -1361,11 +1315,10 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" dependencies = [ - "bitflags", "libc", ] @@ -1377,31 +1330,30 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" +checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a" dependencies = [ "hashbrown 0.17.1", ] @@ -1419,14 +1371,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ "cfg-if", - "digest 0.11.3", + "digest", ] [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memo-map" @@ -1495,11 +1447,27 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minijinja" -version = "2.21.0" +version = "2.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3d648e68cea56d9858d535ee28f9538404e2dd8cb08ed0bd05dca379477f39" +checksum = "86886cf6dbf4e614b19c9a1eec9775f021869d7eadde0fc73921a81b90c9b4c9" dependencies = [ "memo-map", "serde", @@ -1515,17 +1483,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "mio" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" -dependencies = [ - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", -] - [[package]] name = "mockable" version = "3.0.0" @@ -1576,12 +1533,12 @@ dependencies = [ "anyhow", "assert_cmd", "camino", - "cap-primitives 3.4.4", - "cap-std 3.4.4", + "cap-primitives 3.4.6", + "cap-std 3.4.6", "clap", "clap_complete", "clap_mangen", - "digest 0.11.3", + "digest", "dunce", "fluent-bundle", "fs4", @@ -1603,7 +1560,7 @@ dependencies = [ "mockall", "monotony", "ortho_config", - "predicates 3.1.3", + "predicates 3.1.4", "pretty_assertions", "proptest", "regex", @@ -1619,9 +1576,9 @@ dependencies = [ "serde_yaml", "serial_test", "sha1", - "sha2 0.11.0", + "sha2", "shell-quote", - "shlex 2.0.1", + "shlex", "strip-ansi-escapes", "sys-locale", "tempfile", @@ -1632,7 +1589,7 @@ dependencies = [ "toml 0.8.23", "tracing", "tracing-subscriber", - "unicode-width 0.2.1", + "unicode-width 0.2.2", "ureq", "url", "wait-timeout", @@ -1651,7 +1608,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" dependencies = [ - "smallvec 1.15.1", + "smallvec 1.15.2", ] [[package]] @@ -1677,9 +1634,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-traits" @@ -1692,24 +1649,24 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "option-ext" @@ -1719,9 +1676,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ordered-float" -version = "5.3.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +checksum = "c860fd3227ca4ac3cc032e2cd20cba3f02ccdf4b610538f8ee6584d56bb62e96" dependencies = [ "num-traits", ] @@ -1733,7 +1690,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1e1275151857af166ec59cab125a6fec5d2d0ddceda70a8c50c83c45456b953" dependencies = [ "camino", - "cap-std 4.0.2", + "cap-std 4.0.3", "clap", "clap-dispatch", "directories", @@ -1745,8 +1702,8 @@ dependencies = [ "ortho_config_macros", "serde", "serde_json", - "thiserror 2.0.17", - "toml 1.1.3+spec-1.1.0", + "thiserror 2.0.20", + "toml 1.1.4+spec-1.1.0", "tracing", "uncased", "unic-langid", @@ -1767,15 +1724,15 @@ dependencies = [ [[package]] name = "owo-colors" -version = "4.2.2" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1783,15 +1740,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", - "smallvec 1.15.1", - "windows-targets 0.52.6", + "smallvec 1.15.2", + "windows-link", ] [[package]] @@ -1852,27 +1809,21 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -1908,9 +1859,9 @@ dependencies = [ [[package]] name = "predicates" -version = "3.1.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" dependencies = [ "anstyle", "difflib", @@ -1922,15 +1873,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", @@ -1948,11 +1899,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.10+spec-1.0.0", + "toml_edit 0.25.13+spec-1.1.0", ] [[package]] @@ -2036,7 +1987,7 @@ dependencies = [ "libc", "once_cell", "raw-cpuid", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "web-sys", "winapi", ] @@ -2062,6 +2013,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radix_trie" version = "0.2.1" @@ -2074,9 +2031,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha", "rand_core", @@ -2098,7 +2055,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -2121,9 +2078,9 @@ dependencies = [ [[package]] name = "rapidhash" -version = "4.4.1" +version = "4.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e" dependencies = [ "rustversion", ] @@ -2139,9 +2096,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] @@ -2152,7 +2109,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", "thiserror 1.0.69", ] @@ -2163,16 +2120,16 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.20", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -2182,9 +2139,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -2193,9 +2150,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "relative-path" @@ -2211,7 +2168,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -2265,7 +2222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe104196f61dc8911a8da1b10005e9401e7c2e14ad9302e2de6688311c0beec7" dependencies = [ "camino", - "cap-std 3.4.4", + "cap-std 3.4.6", "cfg-if", "convert_case 0.6.0", "gherkin", @@ -2319,9 +2276,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.9.0" +version = "8.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" +checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -2330,10 +2287,11 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.9.0" +version = "8.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" +checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097" dependencies = [ + "mime_guess", "proc-macro2", "quote", "rust-embed-utils", @@ -2343,25 +2301,25 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.9.0" +version = "8.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" +checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0" dependencies = [ - "sha2 0.10.8", + "sha2", "walkdir", ] [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -2397,9 +2355,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "log", "once_cell", @@ -2412,18 +2370,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.7" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -2450,15 +2408,15 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "ryu-js" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd29631678d6fb0903b69223673e122c32e9ae559d0960a38d574695ebc0ea15" +checksum = "04d056b875a9d2e6cb9a61d127afee9ac5999b9f87bcb32079d1318e505be714" [[package]] name = "same-file" @@ -2487,9 +2445,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "self_cell" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" [[package]] name = "semver" @@ -2524,7 +2482,7 @@ dependencies = [ "saphyr-parser", "serde", "serde_json", - "smallvec 2.0.0-alpha.11", + "smallvec 2.0.0-alpha.12", ] [[package]] @@ -2549,9 +2507,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "indexmap", "itoa", @@ -2563,9 +2521,9 @@ dependencies = [ [[package]] name = "serde_json_canonicalizer" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18032888bfda612a88f6b9c7c7a12e8168686936702fe584e3f1b1fc7848443a" +checksum = "fe52319a927259afbfa5180c5157cd8167edfd3e8c254f9558c7fef44c5649f2" dependencies = [ "ryu-js", "serde", @@ -2635,19 +2593,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", + "cpufeatures", + "digest", ] [[package]] @@ -2657,8 +2604,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", + "cpufeatures", + "digest", ] [[package]] @@ -2676,12 +2623,6 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb502615975ae2365825521fa1529ca7648fd03ce0b0746604e0683856ecd7e4" -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "shlex" version = "2.0.1" @@ -2690,9 +2631,9 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "similar" @@ -2708,27 +2649,27 @@ checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "smallvec" -version = "2.0.0-alpha.11" +version = "2.0.0-alpha.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87b96efa4bd6bdd2ff0c6615cc36fc4970cbae63cfd46ddff5cee35a1b4df570" +checksum = "ef784004ca8777809dcdad6ac37629f0a97caee4c685fcea805278d81dd8b857" [[package]] name = "smawk" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" [[package]] name = "stable_deref_trait" @@ -2768,9 +2709,9 @@ dependencies = [ [[package]] name = "supports-hyperlinks" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" [[package]] name = "supports-unicode" @@ -2838,7 +2779,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -2846,12 +2787,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2867,12 +2808,12 @@ dependencies = [ "anyhow", "assert_cmd", "camino", - "cap-std 3.4.4", + "cap-std 3.4.6", "mockable", "netsuke-build", "proptest", "rstest", - "sha2 0.11.0", + "sha2", "tempfile", "thiserror 1.0.69", "tracing", @@ -2887,7 +2828,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width 0.2.1", + "unicode-width 0.2.2", ] [[package]] @@ -2901,11 +2842,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.20", ] [[package]] @@ -2921,50 +2862,49 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] [[package]] name = "time" -version = "0.3.44" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -2972,38 +2912,34 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] [[package]] name = "tokio" -version = "1.46.1" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ - "backtrace", - "io-uring", - "libc", - "mio", "pin-project-lite", - "slab", "tokio-macros", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -3029,9 +2965,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.3+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", @@ -3039,7 +2975,7 @@ dependencies = [ "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -3051,15 +2987,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -3080,28 +3007,28 @@ dependencies = [ "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", - "winnow 0.7.14", + "winnow 0.7.15", ] [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 0.7.14", + "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -3167,7 +3094,7 @@ checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "nu-ansi-term", "sharded-slab", - "smallvec 1.15.1", + "smallvec 1.15.2", "thread_local", "tracing-core", "tracing-log", @@ -3267,11 +3194,17 @@ dependencies = [ "unic-langid-impl", ] +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-linebreak" @@ -3281,9 +3214,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -3293,9 +3226,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unit-prefix" @@ -3402,19 +3335,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -3425,9 +3358,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3435,9 +3368,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", @@ -3448,18 +3381,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -3481,14 +3414,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.3", + "webpki-roots 1.0.9", ] [[package]] name = "webpki-roots" -version = "1.0.3" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -3511,11 +3444,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3557,6 +3490,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -3590,13 +3532,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3609,6 +3568,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3621,6 +3586,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3633,12 +3604,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3651,6 +3634,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3663,6 +3652,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3675,6 +3670,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -3687,20 +3688,29 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] [[package]] name = "winx" @@ -3713,19 +3723,16 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "xdg" @@ -3741,11 +3748,10 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3753,9 +3759,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -3765,18 +3771,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", @@ -3785,18 +3791,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -3806,15 +3812,15 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -3823,10 +3829,11 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -3834,13 +3841,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] diff --git a/tests/ninja_snapshot_tests.rs b/tests/ninja_snapshot_tests.rs index 056df6a31..1b57b9100 100644 --- a/tests/ninja_snapshot_tests.rs +++ b/tests/ninja_snapshot_tests.rs @@ -8,19 +8,21 @@ use anyhow::{Context, Result, ensure}; use cap_std::{ambient_authority, fs_utf8::Dir}; use insta::{Settings, assert_snapshot}; use netsuke::{ir::BuildGraph, manifest, ninja_gen, stdlib::StdlibConfig}; -use std::process::Command; +#[cfg(unix)] +use std::{ + fs as std_fs, + io::Write, + path::{Path, PathBuf}, + process::Command, + time::Duration, +}; +#[cfg(unix)] +use tempfile::{TempDir, tempdir}; +#[cfg(unix)] use test_support::ensure_binaries_available; +#[cfg(unix)] use test_support::fs; - -//! End-to-end validation of Ninja file generation. -//! -//! These tests generate a Ninja file from a manifest, snapshot the -//! output using `insta`, and validate it with the real `ninja` -//! executable. The manifest uses a simple TOUCH rule so the build is -//! fast and deterministic. -}; - #[cfg(unix)] fn run_ok(cmd: &mut Command) -> Result { let out = cmd.output().context("failed to spawn command")?; @@ -261,6 +263,7 @@ fn implicit_deps_manifest_ninja_snapshot() -> Result<()> { Ok(()) } +#[cfg(unix)] #[test] fn conditional_action_deps_ninja_snapshot() -> Result<()> { let manifest = manifest::from_path("tests/data/conditional_action_deps.yml")?; @@ -311,6 +314,7 @@ fn conditional_action_deps_ninja_snapshot() -> Result<()> { validate_conditional_ninja(&ninja_content) } +#[cfg(unix)] struct ExpectedNinjaEdge<'a> { output: &'a str, input: &'a str, @@ -318,6 +322,7 @@ struct ExpectedNinjaEdge<'a> { order_only_deps: &'a str, } +#[cfg(unix)] fn assert_dependency_classes(ninja_content: &str, expected: &ExpectedNinjaEdge<'_>) -> Result<()> { let build_line = ninja_content .lines() @@ -334,6 +339,7 @@ fn assert_dependency_classes(ninja_content: &str, expected: &ExpectedNinjaEdge<' Ok(()) } +#[cfg(unix)] fn validate_conditional_ninja(ninja_content: &str) -> Result<()> { if let Err(err) = ensure_binaries_available(&[("ninja", &["--version"])]) { tracing::warn!("skipping real Ninja validation: {}", err); @@ -351,25 +357,27 @@ fn validate_conditional_ninja(ninja_content: &str) -> Result<()> { assert_conditional_ninja_no_op(&dir, &build_file) } +#[cfg(unix)] fn prepare_conditional_ninja_workspace(ninja_content: &str) -> Result<(TempDir, PathBuf)> { let dir = tempdir().context("create temp dir for conditional Ninja validation")?; let build_file = dir.path().join("build.ninja"); - fs::write(&build_file, ninja_content) + std_fs::write(&build_file, ninja_content) .with_context(|| format!("write Ninja file to {}", build_file.display()))?; for relative_path in ["src/target.in", "include/fallback.h", "order/target.stamp"] { let dependency_path = dir.path().join(relative_path); let parent = dependency_path .parent() .context("dependency path should have parent")?; - fs::create_dir_all(parent) + std_fs::create_dir_all(parent) .with_context(|| format!("create dependency directory {}", parent.display()))?; - fs::write(&dependency_path, "") + std_fs::write(&dependency_path, "") .with_context(|| format!("write Ninja dependency {}", dependency_path.display()))?; } Ok((dir, build_file)) } +#[cfg(unix)] fn run_conditional_ninja(dir: &TempDir, build_file: &Path, args: &[&str]) -> Result { let mut cmd = Command::new("ninja"); cmd.arg("-f").arg(build_file).args(args); @@ -377,6 +385,7 @@ fn run_conditional_ninja(dir: &TempDir, build_file: &Path, args: &[&str]) -> Res run_ok(&mut cmd) } +#[cfg(unix)] fn assert_conditional_ninja_selection(dir: &TempDir, build_file: &Path) -> Result<()> { run_conditional_ninja(dir, build_file, &["-t", "query", "fallback-alpha"])?; run_conditional_ninja(dir, build_file, &["-t", "query", "out/fallback"])?; @@ -389,29 +398,31 @@ fn assert_conditional_ninja_selection(dir: &TempDir, build_file: &Path) -> Resul Ok(()) } +#[cfg(unix)] fn mark_conditional_ninja_output_up_to_date(dir: &TempDir) -> Result<()> { let output = dir.path().join("out/fallback"); let output_dir = output.parent().context("output path should have parent")?; - fs::create_dir_all(output_dir) + std_fs::create_dir_all(output_dir) .with_context(|| format!("create output directory {}", output_dir.display()))?; - fs::write(&output, "") + std_fs::write(&output, "") .with_context(|| format!("write up-to-date output {}", output.display()))?; let latest_dependency = dir.path().join("order/target.stamp"); - let output_modified = fs::metadata(&latest_dependency) + let output_modified = std_fs::metadata(&latest_dependency) .with_context(|| format!("stat dependency {}", latest_dependency.display()))? .modified() .context("read dependency modification time")? + Duration::from_secs(1); - fs::File::options() + std_fs::File::options() .write(true) .open(&output) .with_context(|| format!("open output {}", output.display()))? - .set_times(fs::FileTimes::new().set_modified(output_modified)) + .set_times(std_fs::FileTimes::new().set_modified(output_modified)) .with_context(|| format!("set output time {}", output.display()))?; Ok(()) } +#[cfg(unix)] fn assert_conditional_ninja_no_op(dir: &TempDir, build_file: &Path) -> Result<()> { run_conditional_ninja(dir, build_file, &["out/fallback"])?; let second = run_conditional_ninja(dir, build_file, &["-n", "out/fallback"])?; From 43454294b41b7bed50dcb413b78718914dfc59a5 Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 24 Aug 2026 02:18:06 +0200 Subject: [PATCH 22/34] Correct precedence test path diagnostic Use the path display representation in the upstream ladder assertion so the target's strict Clippy configuration accepts the rebased test suite. --- tests/cli_tests/config_precedence_ladder.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cli_tests/config_precedence_ladder.rs b/tests/cli_tests/config_precedence_ladder.rs index c4d6e90a1..f7646edd3 100644 --- a/tests/cli_tests/config_precedence_ladder.rs +++ b/tests/cli_tests/config_precedence_ladder.rs @@ -151,9 +151,9 @@ impl LadderExpectation { fn assert_ladder(merged: &Cli, expected: LadderExpectation) -> Result<()> { ensure!( merged.file.as_path() == Path::new(expected.file), - "manifest path should be {:?}, got {:?}", + "manifest path should be {:?}, got {}", expected.file, - merged.file + merged.file.display() ); ensure!( merged.emoji == expected.emoji, From d3e30b8bff4553b4815aac8015ce2bcc92771bff Mon Sep 17 00:00:00 2001 From: leynos Date: Thu, 27 Aug 2026 03:34:58 +0200 Subject: [PATCH 23/34] Format Windows setup paths for display Preserve the existing UTF-8 conversion context while rendering invalid temporary paths with their display representation for strict Clippy. --- tests/std_filter_tests/command_filters/windows_filter_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/std_filter_tests/command_filters/windows_filter_tests.rs b/tests/std_filter_tests/command_filters/windows_filter_tests.rs index cb07b62a6..8a44ccf03 100644 --- a/tests/std_filter_tests/command_filters/windows_filter_tests.rs +++ b/tests/std_filter_tests/command_filters/windows_filter_tests.rs @@ -90,7 +90,7 @@ fn windows_command_setup( ) -> Result<(tempfile::TempDir, OsString, Utf8PathBuf)> { let temp = tempdir().context(ctx.tempdir)?; let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("{}: {path:?}", ctx.root))?; + .map_err(|path| anyhow!("{}: {}", ctx.root, path.display()))?; let dir = Dir::open_ambient_dir(&root, ambient_authority()).context(ctx.dir)?; let helper = compile_rust_helper(&dir, &root, helper_name, helper_source).context(ctx.compile)?; From ac613293ec901c86dc5ac73e0b39d313cd021008 Mon Sep 17 00:00:00 2001 From: leynos Date: Fri, 28 Aug 2026 03:32:08 +0200 Subject: [PATCH 24/34] Refresh rebase compatibility Regenerate `Cargo.lock` from the merged manifest and update the upstream runner test diagnostic for the target branch's strict Clippy policy. --- Cargo.lock | 34 +++++++++++++++++----------------- src/runner/tests.rs | 3 +-- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 942220b56..3ad70583b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,7 +370,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -430,9 +430,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] @@ -575,7 +575,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -816,7 +816,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -1315,9 +1315,9 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libredox" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" +checksum = "d7955dfc218a8afb29dfeffd540e3a6e96baeb94fe7138228dd7cc6937fbbf96" dependencies = [ "libc", ] @@ -1351,9 +1351,9 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru" -version = "0.18.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a" +checksum = "0d317b4b9eb398e6acce275758ec6125535505e7a146fb1a9b8bda2451b0ff4c" dependencies = [ "hashbrown 0.17.1", ] @@ -1724,9 +1724,9 @@ dependencies = [ [[package]] name = "owo-colors" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" +checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" [[package]] name = "parking_lot" @@ -2502,7 +2502,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -2743,9 +2743,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -2868,7 +2868,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -2939,7 +2939,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -3847,7 +3847,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] diff --git a/src/runner/tests.rs b/src/runner/tests.rs index 4689bc607..8f0c598e9 100644 --- a/src/runner/tests.rs +++ b/src/runner/tests.rs @@ -97,8 +97,7 @@ fn generation_steps_run_without_reporter() -> anyhow::Result<()> { ); anyhow::ensure!( ninja_text.contains("build hello:"), - "expected generated Ninja to contain the hello build edge:\n{}", - ninja_text + "expected generated Ninja to contain the hello build edge:\n{ninja_text}" ); Ok(()) } From 0e17135329299f9dd408a57bf72a2548c4b8af91 Mon Sep 17 00:00:00 2001 From: leynos Date: Fri, 28 Aug 2026 20:17:24 +0200 Subject: [PATCH 25/34] Fix Windows Clippy test helpers Format non-UTF-8 temporary paths through `Display` and compile the executable-permission helper only where the test needs it. --- .../command_available_selection_cases.rs | 11 ++++------- tests/stdlib_which_pathext_tests.rs | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/manifest/expand_test_cases/command_available_selection_cases.rs b/src/manifest/expand_test_cases/command_available_selection_cases.rs index 33c006dfd..502417881 100644 --- a/src/manifest/expand_test_cases/command_available_selection_cases.rs +++ b/src/manifest/expand_test_cases/command_available_selection_cases.rs @@ -52,7 +52,7 @@ impl ResolverWorld { fn new(has_nextest: bool) -> Result { let temp = tempfile::tempdir().context("create resolver workspace")?; let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("temporary path should be UTF-8: {path:?}"))?; + .map_err(|path| anyhow!("temporary path should be UTF-8: {}", path.display()))?; if has_nextest { write_tool(&root, "cargo-nextest")?; } @@ -136,7 +136,9 @@ fn write_tool(root: &Utf8Path, name: &str) -> Result<()> { let path = Utf8PathBuf::from(tool_filename(name)); dir.write(&path, script_contents()) .with_context(|| format!("write fixture tool {path}"))?; - mark_executable(&dir, &path) + #[cfg(unix)] + mark_executable(&dir, &path)?; + Ok(()) } fn tool_filename(name: &str) -> String { @@ -168,8 +170,3 @@ fn mark_executable(dir: &Dir, path: &Utf8Path) -> Result<()> { dir.set_permissions(path, permissions) .with_context(|| format!("chmod {path}")) } - -#[cfg(not(unix))] -fn mark_executable(_dir: &Dir, _path: &Utf8Path) -> Result<()> { - Ok(()) -} diff --git a/tests/stdlib_which_pathext_tests.rs b/tests/stdlib_which_pathext_tests.rs index 7dc98fa15..027a08dd9 100644 --- a/tests/stdlib_which_pathext_tests.rs +++ b/tests/stdlib_which_pathext_tests.rs @@ -55,7 +55,7 @@ impl ToolWorkspace { fn tool_workspace() -> Result { let temp = tempfile::tempdir().context("create temp workspace")?; let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) - .map_err(|path| anyhow!("temp path should be UTF-8: {path:?}"))?; + .map_err(|path| anyhow!("temp path should be UTF-8: {}", path.display()))?; let dir = Dir::open_ambient_dir(&root, ambient_authority()) .with_context(|| format!("open workspace {root}"))?; let bin = root.join("bin"); From 018113930c9e8f3afb5a0f4cbbe7cd655503749e Mon Sep 17 00:00:00 2001 From: leynos Date: Sat, 29 Aug 2026 13:54:45 +0200 Subject: [PATCH 26/34] Regenerate lockfile after rebase Rebuild the dependency lockfile from the main-side manifest after the rebase so it records the current compatible resolver output. --- Cargo.lock | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ad70583b..ac4d5ec4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,7 +148,7 @@ dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.8.9", "object", "rustc-demangle", "windows-link", @@ -663,12 +663,13 @@ checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.9.1", + "zlib-rs", ] [[package]] @@ -1144,9 +1145,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -1478,6 +1479,15 @@ name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", "simd-adler32", @@ -3850,6 +3860,12 @@ dependencies = [ "syn 3.0.4", ] +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + [[package]] name = "zmij" version = "1.0.23" From 6a6341aa53b0ddeffd71413506ad1a6c0d13a2ad Mon Sep 17 00:00:00 2001 From: leynos Date: Sat, 29 Aug 2026 23:38:52 +0200 Subject: [PATCH 27/34] Correct PATHEXT Boolean rendering assertion Expect MiniJinja's rendered Boolean spelling while retaining the assertion that the excluded command is unavailable. --- tests/stdlib_which_pathext_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/stdlib_which_pathext_tests.rs b/tests/stdlib_which_pathext_tests.rs index 027a08dd9..6e2aad62b 100644 --- a/tests/stdlib_which_pathext_tests.rs +++ b/tests/stdlib_which_pathext_tests.rs @@ -107,7 +107,7 @@ fn assert_command_unavailable(env: &Environment<'_>, command: &str) -> Result<() context! {}, )?; ensure!( - rendered == "false", + rendered == "False", "expected command_available('{command}') to be false, got {rendered}" ); Ok(()) From 3a4f74c135cecd3a625220f7cd7255418e8f1471 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 30 Aug 2026 16:35:33 +0200 Subject: [PATCH 28/34] Render test paths with Display Preserve the path-conversion errors while satisfying Clippy's portable formatting requirement after the rebase. --- tests/bdd/steps/ninja.rs | 2 +- tests/ninja_dollar_escaping_tests.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/bdd/steps/ninja.rs b/tests/bdd/steps/ninja.rs index 97af8edd0..d675983f8 100644 --- a/tests/bdd/steps/ninja.rs +++ b/tests/bdd/steps/ninja.rs @@ -114,7 +114,7 @@ fn run_generated_ninja_target(world: &TestWorld, target: &str, value: &str) -> R .context("host PATH is required to run Ninja")?; let workspace = ninja_integration_workspace().context("Ninja is required for BDD execution")?; let path = Utf8PathBuf::from_path_buf(workspace.path().to_path_buf()) - .map_err(|non_utf8| anyhow!("non-UTF-8 temporary path: {non_utf8:?}"))?; + .map_err(|non_utf8| anyhow!("non-UTF-8 temporary path: {}", non_utf8.display()))?; let directory = Dir::open_ambient_dir(&path, ambient_authority()) .with_context(|| format!("open Ninja workspace {path}"))?; directory diff --git a/tests/ninja_dollar_escaping_tests.rs b/tests/ninja_dollar_escaping_tests.rs index d9496b3c4..caf9c6d51 100644 --- a/tests/ninja_dollar_escaping_tests.rs +++ b/tests/ninja_dollar_escaping_tests.rs @@ -81,8 +81,9 @@ impl NinjaWorkspace { /// Creates an isolated workspace and writes the generated Ninja file into it. fn create(ninja_file: &str) -> anyhow::Result { let temporary_directory = required_ninja_workspace()?; - let path = Utf8PathBuf::from_path_buf(temporary_directory.path().to_path_buf()) - .map_err(|non_utf8| anyhow::anyhow!("non-UTF-8 temporary path: {non_utf8:?}"))?; + let path = Utf8PathBuf::from_path_buf(temporary_directory.path().to_path_buf()).map_err( + |non_utf8| anyhow::anyhow!("non-UTF-8 temporary path: {}", non_utf8.display()), + )?; let directory = Dir::open_ambient_dir(&path, ambient_authority()) .with_context(|| format!("open Ninja workspace {path}"))?; let workspace = Self { From d80f73a37cc351693580538bf7ca18111e9b6084 Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 30 Aug 2026 16:53:48 +0200 Subject: [PATCH 29/34] Preserve lowercase Boolean manifest rendering Keep existing manifests stable across the MiniJinja formatter change and make the Windows command resolver fixture independent of host PATHEXT. --- docs/developers-guide.md | 7 +++ docs/netsuke-design.md | 7 +++ .../command_available_selection_cases.rs | 3 +- src/stdlib/register.rs | 17 ++++++- tests/documentation_examples_e2e_tests.rs | 4 +- tests/stdlib_which_pathext_tests.rs | 2 +- tests/stdlib_which_tests.rs | 44 +++++++++---------- tests/stdlib_workspace_switch_tests.rs | 4 +- 8 files changed, 58 insertions(+), 30 deletions(-) diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 75e2ff560..13d4f9b87 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -116,6 +116,13 @@ script recipe bodies untouched. This boundary is what permits a recipe to contain a build-only helper without causing `help targets` to execute or otherwise evaluate that helper; it does not alter full-render behaviour. +The standard-library registration boundary owns MiniJinja's value formatter. +It preserves the historical lowercase `true` and `false` spelling when a +Boolean helper result is interpolated into a string field, while delegating +all non-Boolean values to MiniJinja's `escape_formatter`. Keep this as one +registration-wide policy: do not add per-helper or per-call formatter +variants. + Helpers excluded from the query allowlist are registered as deliberate query-disabled stubs by the standard-library adapter. The stubs return a stable, classified MiniJinja operation error. Manifest expansion recognizes diff --git a/docs/netsuke-design.md b/docs/netsuke-design.md index ae8851473..b5078a861 100644 --- a/docs/netsuke-design.md +++ b/docs/netsuke-design.md @@ -1249,6 +1249,13 @@ filters, and functions are available to every template and give concise access to common filesystem queries, path manipulations, collection utilities, and network operations. +The standard-library registration boundary installs one MiniJinja value +formatter for all helpers. Boolean results interpolated into string fields +retain the historical lowercase `true`/`false` spelling; every non-Boolean +value continues through MiniJinja's `escape_formatter`. This formatter is +owned by standard-library registration and is reused across calls, so helpers +must not introduce per-helper or per-call formatter variants. + #### File-system tests | Test | True when the operand… | diff --git a/src/manifest/expand_test_cases/command_available_selection_cases.rs b/src/manifest/expand_test_cases/command_available_selection_cases.rs index 502417881..933fa328c 100644 --- a/src/manifest/expand_test_cases/command_available_selection_cases.rs +++ b/src/manifest/expand_test_cases/command_available_selection_cases.rs @@ -69,7 +69,8 @@ impl ResolverWorld { }; let config = StdlibConfig::new(workspace)? .with_workspace_root_path(self.root.clone())? - .with_path_override(path_override); + .with_path_override(path_override) + .with_pathext_override(".CMD"); let mut env = Environment::new(); stdlib::register_with_config(&mut env, config)?; Ok(env) diff --git a/src/stdlib/register.rs b/src/stdlib/register.rs index 4f63c7bee..4f0f4acc6 100644 --- a/src/stdlib/register.rs +++ b/src/stdlib/register.rs @@ -16,8 +16,8 @@ use camino::Utf8Path; use cap_std::fs::FileTypeExt; use cap_std::{ambient_authority, fs, fs_utf8::Dir}; use minijinja::{ - Environment, Error, ErrorKind, State, - value::{Kwargs, Value}, + Environment, Error, ErrorKind, State, escape_formatter, + value::{Kwargs, Value, ValueKind}, }; use std::sync::Arc; @@ -102,6 +102,7 @@ pub fn register_with_config( env: &mut Environment<'_>, config: StdlibConfig, ) -> anyhow::Result { + register_legacy_boolean_formatter(env); let state = StdlibState::default(); register_read_only_helpers(env, &config); time::register_functions(env); @@ -112,6 +113,18 @@ pub fn register_with_config( Ok(state) } +/// Preserve lowercase Boolean interpolation for existing manifests. +fn register_legacy_boolean_formatter(env: &mut Environment<'_>) { + env.set_formatter(|out, state, value| { + if value.kind() == ValueKind::Bool { + out.write_str(if value.is_true() { "true" } else { "false" }) + .map_err(Error::from) + } else { + escape_formatter(out, state, value) + } + }); +} + /// Register helpers suitable for manifest queries that must avoid side effects. /// /// The registration preserves only lexical path filters, collection helpers, diff --git a/tests/documentation_examples_e2e_tests.rs b/tests/documentation_examples_e2e_tests.rs index 9ec3e3faa..94f1f63b2 100644 --- a/tests/documentation_examples_e2e_tests.rs +++ b/tests/documentation_examples_e2e_tests.rs @@ -313,7 +313,7 @@ fn stdlib_file_test_example_identifies_unix_fixture_types() -> Result<()> { assert_success(&run, "stdlib file-test example")?; ensure!( test_fs::read_to_string(workspace.path().join("stdlib-file-tests.txt"))? - == "True\nTrue\nTrue\nTrue\nFalse\nTrue\nTrue\n", + == "true\ntrue\ntrue\ntrue\nfalse\ntrue\ntrue\n", "stdlib file tests should report the documented Unix fixture types" ); Ok(()) @@ -386,7 +386,7 @@ fn stdlib_host_context_example_uses_controlled_process_state() -> Result<()> { ensure!( output == format!( - "HELLO\nbeta\n{stub_directory}/guide-tool\nTrue\nfrom-env\nfixtures/a.txt,fixtures/b.txt\n" + "HELLO\nbeta\n{stub_directory}/guide-tool\ntrue\nfrom-env\nfixtures/a.txt,fixtures/b.txt\n" ), "stdlib host-context output should use only controlled fixtures: {output}" ); diff --git a/tests/stdlib_which_pathext_tests.rs b/tests/stdlib_which_pathext_tests.rs index 6e2aad62b..027a08dd9 100644 --- a/tests/stdlib_which_pathext_tests.rs +++ b/tests/stdlib_which_pathext_tests.rs @@ -107,7 +107,7 @@ fn assert_command_unavailable(env: &Environment<'_>, command: &str) -> Result<() context! {}, )?; ensure!( - rendered == "False", + rendered == "false", "expected command_available('{command}') to be false, got {rendered}" ); Ok(()) diff --git a/tests/stdlib_which_tests.rs b/tests/stdlib_which_tests.rs index adbbd3bdc..a30d8ceca 100644 --- a/tests/stdlib_which_tests.rs +++ b/tests/stdlib_which_tests.rs @@ -117,7 +117,7 @@ fn command_available_returns_true_for_path_match( let output = env.render_str("{{ command_available('helper') }}", context! {})?; - ensure!(output == "True", "expected True, got {output}"); + ensure!(output == "true", "expected true, got {output}"); Ok(()) } @@ -133,7 +133,7 @@ fn command_available_returns_false_for_missing_command( context! {}, )?; - ensure!(output == "False", "expected False, got {output}"); + ensure!(output == "false", "expected false, got {output}"); Ok(()) } @@ -147,7 +147,7 @@ fn command_available_returns_false_for_missing_absolute_path( let output = render_command_available(&env, missing.as_path(), "")?; - ensure!(output == "False", "expected False, got {output}"); + ensure!(output == "false", "expected false, got {output}"); Ok(()) } @@ -160,7 +160,7 @@ fn command_available_returns_false_for_missing_relative_path( let output = env.render_str("{{ command_available('./missing') }}", context! {})?; - ensure!(output == "False", "expected False, got {output}"); + ensure!(output == "false", "expected false, got {output}"); Ok(()) } @@ -213,7 +213,7 @@ fn command_available_returns_true_for_absolute_path( let output = render_command_available(&env, tool.as_path(), "")?; - ensure!(output == "True", "expected True, got {output}"); + ensure!(output == "true", "expected true, got {output}"); Ok(()) } @@ -231,13 +231,13 @@ fn command_available_returns_true_for_canonical_symlink( let output = render_command_available(&env, link.as_path(), ", canonical=true")?; - ensure!(output == "True", "expected True, got {output}"); + ensure!(output == "true", "expected true, got {output}"); Ok(()) } #[rstest] -#[case::present("workspace-helper", "True")] -#[case::absent("missing-helper", "False")] +#[case::present("workspace-helper", "true")] +#[case::absent("missing-helper", "false")] fn command_available_uses_workspace_fallback_when_path_is_empty( #[case] command: &str, #[case] expected: &str, @@ -272,19 +272,19 @@ fn command_available_fresh_bypasses_cached_success( context! {}, )?; - ensure!(first == "True", "expected initial True, got {first}"); - ensure!(cached == "True", "expected cached True, got {cached}"); - ensure!(fresh == "False", "expected fresh False, got {fresh}"); + ensure!(first == "true", "expected initial true, got {first}"); + ensure!(cached == "true", "expected cached true, got {cached}"); + ensure!(fresh == "false", "expected fresh false, got {fresh}"); Ok(()) } #[rstest] -#[case::auto_present("auto", true, "True")] -#[case::auto_absent("auto", false, "False")] -#[case::always_present("always", true, "True")] -#[case::always_absent("always", false, "False")] -#[case::never_present("never", true, "False")] -#[case::never_absent("never", false, "False")] +#[case::auto_present("auto", true, "true")] +#[case::auto_absent("auto", false, "false")] +#[case::always_present("always", true, "true")] +#[case::always_absent("always", false, "false")] +#[case::never_present("never", true, "false")] +#[case::never_absent("never", false, "false")] fn command_available_honours_cwd_mode( #[case] cwd_mode: &str, #[case] present: bool, @@ -305,10 +305,10 @@ fn command_available_honours_cwd_mode( } #[rstest] -#[case::present(false, "True")] -#[case::present_all(true, "True")] -#[case::absent(false, "False")] -#[case::absent_all(true, "False")] +#[case::present(false, "true")] +#[case::present_all(true, "true")] +#[case::absent(false, "false")] +#[case::absent_all(true, "false")] fn command_available_all_kwarg_does_not_affect_bool( #[case] all: bool, #[case] expected: &str, @@ -320,7 +320,7 @@ fn command_available_all_kwarg_does_not_affect_bool( &workspace_fixture.root, path_override(&[workspace_fixture.root.join("bin")])?, )?; - let command = if expected == "True" { + let command = if expected == "true" { "all-helper" } else { "missing-helper" diff --git a/tests/stdlib_workspace_switch_tests.rs b/tests/stdlib_workspace_switch_tests.rs index 4aa0ce13c..3a45f5735 100644 --- a/tests/stdlib_workspace_switch_tests.rs +++ b/tests/stdlib_workspace_switch_tests.rs @@ -61,13 +61,13 @@ fn the_switch_gates_the_workspace_fallback_end_to_end() -> Result<()> { let enabled = probe_verdict(&temp, None)?; ensure!( - enabled.contains("probe-True.txt"), + enabled.contains("probe-true.txt"), "with the switch unset the workspace fallback should find the probe:\n{enabled}" ); let disabled = probe_verdict(&temp, Some("0"))?; ensure!( - disabled.contains("probe-False.txt"), + disabled.contains("probe-false.txt"), "with the switch set to 0 the fallback must not run:\n{disabled}" ); Ok(()) From d90224d070824ddac78b48171868d59dc11ddcaf Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 30 Aug 2026 22:00:43 +0200 Subject: [PATCH 30/34] Format rebase documentation updates Apply the enforced Markdown table and paragraph formatting to the overlapping documentation changes after rebasing onto the new PR base. --- docs/developers-guide.md | 11 +++++------ docs/netsuke-design.md | 6 +++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 13d4f9b87..d89a285f6 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -116,12 +116,11 @@ script recipe bodies untouched. This boundary is what permits a recipe to contain a build-only helper without causing `help targets` to execute or otherwise evaluate that helper; it does not alter full-render behaviour. -The standard-library registration boundary owns MiniJinja's value formatter. -It preserves the historical lowercase `true` and `false` spelling when a -Boolean helper result is interpolated into a string field, while delegating -all non-Boolean values to MiniJinja's `escape_formatter`. Keep this as one -registration-wide policy: do not add per-helper or per-call formatter -variants. +The standard-library registration boundary owns MiniJinja's value formatter. It +preserves the historical lowercase `true` and `false` spelling when a Boolean +helper result is interpolated into a string field, while delegating all +non-Boolean values to MiniJinja's `escape_formatter`. Keep this as one +registration-wide policy: do not add per-helper or per-call formatter variants. Helpers excluded from the query allowlist are registered as deliberate query-disabled stubs by the standard-library adapter. The stubs return a diff --git a/docs/netsuke-design.md b/docs/netsuke-design.md index b5078a861..29353dcf5 100644 --- a/docs/netsuke-design.md +++ b/docs/netsuke-design.md @@ -1252,9 +1252,9 @@ network operations. The standard-library registration boundary installs one MiniJinja value formatter for all helpers. Boolean results interpolated into string fields retain the historical lowercase `true`/`false` spelling; every non-Boolean -value continues through MiniJinja's `escape_formatter`. This formatter is -owned by standard-library registration and is reused across calls, so helpers -must not introduce per-helper or per-call formatter variants. +value continues through MiniJinja's `escape_formatter`. This formatter is owned +by standard-library registration and is reused across calls, so helpers must +not introduce per-helper or per-call formatter variants. #### File-system tests From 5919bfb0d4472a58c010f025b38f0e3cc6466c66 Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 31 Aug 2026 00:32:01 +0200 Subject: [PATCH 31/34] Address conditional dependency review findings Preserve the conditional manifest contracts while removing redundant coverage, reusing shared test fixtures, and documenting the offset boundary and Ninja generation behaviour. --- ...conditional-action-dependency-manifests.md | 65 +++-- docs/netsuke-design.md | 275 +----------------- docs/roadmap.md | 2 +- docs/users-guide.md | 4 + .../command_available_selection_cases.rs | 68 ++--- src/stdlib/time/tests.rs | 27 ++ tests/ir_from_manifest_tests.rs | 23 -- tests/ninja_dollar_escaping_tests.rs | 16 + tests/ninja_snapshot_tests.rs | 185 +----------- .../conditional_action_deps.rs | 188 ++++++++++++ ..._deps__conditional_action_deps_ninja.snap} | 3 +- 11 files changed, 314 insertions(+), 542 deletions(-) create mode 100644 tests/ninja_snapshot_tests/conditional_action_deps.rs rename tests/snapshots/ninja/{ninja_snapshot_tests__conditional_action_deps_ninja.snap => ninja_snapshot_tests__conditional_action_deps__conditional_action_deps_ninja.snap} (90%) diff --git a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md index 638c04aba..03bf7ebaf 100644 --- a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md +++ b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md @@ -39,20 +39,27 @@ if any of the following regress: including for conditionally-selected actions. The deliverable is test code plus small deterministic fixtures and -documentation. No production behaviour changes. +documentation. The branch also carries the `now(offset=...)` compatibility +guard in `parse_offset`: after the dependency update accepted offsets beyond a +civil day, the parser rejects offsets whose absolute whole-hour component is 24 +or greater. This preserves the documented ISO 8601 input contract while leaving +valid offsets unchanged. ## Constraints Hard invariants that must hold throughout implementation. Violation requires escalation, not workarounds. -- This is a **test-and-documentation-only** change. Do not modify production - semantics in `src/manifest/`, `src/ir/`, `src/ninja_gen.rs`, or - `src/stdlib/`. The single permitted exception is adding a narrowly-scoped, - test-only seam (for example a `pub(crate)` constructor or `#[cfg(test)]` - helper) *if and only if* an existing public or `pub(crate)` surface cannot - drive a required scenario; any such seam must be recorded in the Decision Log - and must not change runtime behaviour. +- This is primarily a **test-and-documentation** change. The one production + compatibility guard in `src/stdlib/time/mod.rs` is in scope: `parse_offset` + must reject parsed offsets whose absolute whole-hour component is 24 or + greater, because the dependency update accepts values beyond a civil day. Do + not modify other production semantics in `src/manifest/`, `src/ir/`, + `src/ninja_gen.rs`, or `src/stdlib/`. Any further production change requires + escalation. The single test-only seam exception remains available only if an + existing public or `pub(crate)` surface cannot drive a required scenario; any + such seam must be recorded in the Decision Log and must not change runtime + behaviour. - Do not weaken or delete existing tests. New tests are additive. Existing passing tests must continue to pass unchanged. - Preserve the existing implicit `phony: true` behaviour for actions; tests @@ -171,6 +178,10 @@ escalation, not workarounds. component design, and roadmap; recorded why no ADR is warranted. - [x] (2026-07-29 00:55Z) Stage D: passed the full gate stack and final CodeRabbit review; roadmap item 3.14.5 is complete. +- [x] (2026-08-31) Reviewed the `now(offset=...)` compatibility guard after + review feedback; recorded its production scope, documented the accepted + and rejected offset forms, and added the boundary to the validation + scope. (Timestamps to be added as work proceeds.) @@ -345,12 +356,20 @@ escalation, not workarounds. developers' guides are the appropriate homes for this test contract. Date/Author: 2026-07-28, implementation agent. +- Decision: Retain the `parse_offset` absolute-hour guard as a compatibility + boundary. Rationale: the dependency update accepts offsets beyond a civil + day, but the user-facing `now(offset=...)` contract is ISO 8601 and accepts + only offsets below 24 hours in absolute value. The guard therefore belongs at + parsing, where both `+24:00` and `-24:00` are rejected before timestamp + conversion. Date/Author: 2026-08-31, review follow-up. + ## Outcomes & retrospective -Stage A completed without requiring a new seam or changing production code. The -approved dev dependencies resolve to googletest 0.14.3 and pretty_assertions -1.4.1. The real resolver injection, expansion entry point, and deterministic -Ninja dependency ordering all remain available as planned. +Stage A completed without requiring a new seam or changing production code +outside the pre-existing `parse_offset` compatibility guard. The approved dev +dependencies resolve to googletest 0.14.3 and pretty_assertions 1.4.1. The real +resolver injection, expansion entry point, and deterministic Ninja dependency +ordering all remain available as planned. B.1 now pins complementary action selection through the real resolver in both deterministic worlds. The present case also combines action `foreach` with @@ -385,12 +404,14 @@ states the manifest/stdlib port boundary in the design, and marks roadmap item 3.14.5 and its four sub-items complete. No ADR is warranted because runtime architecture and behaviour are unchanged. -The completed change is test-and-documentation only. It adds deterministic -real-resolver unit coverage, selection-time impurity coverage, conditional IR -dependency assertions, a reviewed Ninja snapshot with real-Ninja validation, -and one combined BDD scenario. All five focused sabotage checks failed at the -intended assertion. The final branch-wide formatting, documentation, type, -lint, and test gates passed, and CodeRabbit reported no actionable findings. +The completed change is primarily test-and-documentation work, with the +`parse_offset` compatibility guard explicitly retained as the sole production +behaviour boundary. It adds deterministic real-resolver unit coverage, +selection-time impurity coverage, conditional IR dependency assertions, a +reviewed Ninja snapshot with real-Ninja validation, and one combined BDD +scenario. All five focused sabotage checks failed at the intended assertion. +The final branch-wide formatting, documentation, type, lint, and test gates +passed, and CodeRabbit reported no actionable findings. ## Context and orientation @@ -1016,8 +1037,7 @@ Skills to load while implementing: - 2026-07-28 — Marked the approved plan in progress and recorded Stage A. Confirmed the existing seams, deterministic dependency emission order, and googletest/rstest interoperability with the approver-specified attribute - order. Remaining work starts at Stage B.1; no production change or new seam - is required. + order. Remaining work starts at Stage B.1; no new test seam is required. - 2026-07-28 — Completed Stage B.1 implementation and its non-vacuity check. Added deterministic present/absent real-resolver cases, including the @@ -1058,3 +1078,8 @@ Skills to load while implementing: - 2026-07-29 — Completed Stage D and closed the ExecPlan. The final branch-wide gate stack passed with snapshot mutation disabled, and CodeRabbit returned zero actionable findings across the complete committed diff. + +- 2026-08-31 — Recorded the `now(offset=...)` compatibility guard and its + user-facing contract after review. Validation scope now includes `Z`/`z`, + signed offsets below 24 hours, and rejection of both signs at `24:00` and + beyond; the full gate stack remains the required final validation. diff --git a/docs/netsuke-design.md b/docs/netsuke-design.md index 29353dcf5..e19db1bf4 100644 --- a/docs/netsuke-design.md +++ b/docs/netsuke-design.md @@ -2114,7 +2114,7 @@ This transformation involves several steps: traversal are logged, collected, and returned alongside any cycle to aid diagnostics. -### 5.4 Ninja file synthesis (`src/ninja_gen/mod.rs`) — direct generation +### 5.4 Ninja file synthesis (`src/ninja_gen/mod.rs`) The final step is to synthesize the `build.ninja` file from the `BuildGraph` IR. This process is a straightforward, mechanical translation from the IR data @@ -2182,278 +2182,7 @@ structures to the Ninja file syntax. build my_app: link foo.o bar.o | lib_dependency.a ``` - A `BuildEdge` whose `dependency_order` is `serial` and has more than one - implicit dependency is an exception to this direct rendering. The generator - lowers it into staged phony gates, with one content-addressed Ninja dyndep - sidecar per dependency. A gate can reveal exactly one real dependency; the - gate edge associated with the next sidecar depends on the preceding gate. - This makes each later dependency unavailable to Ninja until the previous one - succeeds, while preserving one Ninja scheduler and its shared-work - memoization. The runner materializes every sidecar file before Ninja starts; - no Ninja edge produces sidecar content. - - The generated result is a bundle, not merely a string: generation is an - effect-free query that returns the main Ninja text and its `.netsuke/dyndep` - sidecars. Each runner command then materializes those sidecars through an - injected effective-working-directory capability before it writes or runs the - main file. The main file declares `ninja_required_version = 1.10` only when - it contains such staged serial ordering. `.netsuke/serial` and - `.netsuke/dyndep` are reserved for generated state. `serial` applies only to - direct implicit dependencies; it does not delay an independently reachable - node elsewhere in the graph. - -Figure: Runner-owned serial dyndep bundle generation and execution. - -```mermaid -sequenceDiagram - accTitle: Runner-owned serial dependency generation and execution - accDescr { - The runner generates a Ninja bundle and materializes its dyndep sidecars. - Generate writes the manifest without invoking Ninja. Build invokes Ninja - for execution, and clean invokes Ninja in clean tool mode. - } - actor User - participant Runner as runner.generate_ninja - participant NinjaGen as ninja_gen.generate_bundle - participant Dyndep as runner.materialize_dyndep_bundle - participant Ninja - - User->>Runner: netsuke build / clean / generate - Runner->>NinjaGen: generate_bundle(graph) - NinjaGen-->>Runner: GeneratedNinja (build_file, dyndep_files) - Runner->>Dyndep: materialize_dyndep_bundle(cli, bundle) - Dyndep-->>Runner: dyndep sidecars materialized - alt generate - Runner-->>User: write generated Ninja manifest without invoking Ninja - else build - Runner->>Ninja: invoke with bundle.build_file() - Ninja-->>User: serial deps run in order, parallel elsewhere - else clean - Runner->>Ninja: invoke with bundle.build_file() in clean tool mode - Ninja-->>User: clean completed - end -``` - -The runner holds a capability-scoped exclusive lease on the dyndep directory -from sidecar materialization through Ninja consumption or generated-output -consumption. While the lease is held, stale `.tmp` files are removed and -retention preserves the current bundle plus at most 32 obsolete `.dd` files and -1 MiB of obsolete `.dd` bytes. `build` and `generate` prune after -materialization; `clean` prunes only after successful `ninja -t clean` and not -on failure. Sidecars remain immutable and content-addressed. Consequently, an -older arbitrary `generate --output` manifest may lose its sidecars after a -later command and must be regenerated. See -[ADR-012](adr-012-bound-dyndep-sidecar-retention.md) for this policy. - -4\. **Write Defaults:** Finally, write the `default` statement, listing all -paths from `graph.default_targets`. - -```ninja -default my_app -``` - -### 5.4 Ninja file synthesis (`src/ninja_gen/mod.rs`) — serial generation - -The final step is to synthesize the `build.ninja` file from the `BuildGraph` -IR. This process is a straightforward, mechanical translation from the IR data -structures to the Ninja file syntax. - -1. **Write Variables:** Any global variables that need to be passed to Ninja can - be written at the top of the file (e.g., `msvc_deps_prefix` for Windows - -2. **Write Rules:** Sort `graph.actions` by action ID and write a corresponding - Ninja `rule` statement for each executable `ir::Action`. Dependency-only - actions are omitted because they have no command to execute. Their edges - select Ninja's built-in `phony` rule. The IR already contains ordinary - command text: its input and output paths have replaced Netsuke's `ins`/ - `outs` and `$in`/`$out` placeholders during lowering. Scalar commands are - emitted as-is. List commands are emitted as the brace-group, `eval`, and - `&&` chain described in §2.3, including the bounded failure marker for each - one-based entry. - - When an action's `recipe` is a script, the generated rule wraps the script - in the configured platform-specific interpreter. Unix uses `/bin/sh -e -c`, - while Windows defaults to `powershell -Command`; an explicit manifest - `interpreter` overrides the platform default. - - Command and script text must be converted from IR text to backend text at - this stage. After Netsuke placeholders have been resolved, remaining literal - dollar signs are escaped as `$$` for Ninja so shell variables survive to the - shell. Structured `exec` recipes are rendered by quoting each argv element - as one argument for the selected backend. - - Resolved environment bindings are emitted as backend-specific command - prefixes or generated wrapper script assignments. The implementation must - avoid exposing Ninja variables as the user-facing environment API. - - Code snippet - - ```ninja - # Generated from an ir::Action - rule cc - command = gcc -c -o $out $in - description = CC $out - ``` - - The planned `deps_from` manifest field will populate `ir::Action.depfile` and - `ir::Action.deps_format`, allowing this rule writer to emit Ninja's - `depfile` and `deps` attributes without overloading target prerequisites. - -3. **Write Build Edges:** Iterate through the `graph.targets` map. For each - `ir::BuildEdge`, write a corresponding Ninja `build` statement. This - involves formatting the lists of explicit outputs, implicit outputs, inputs, - implicit dependencies, and order-only dependencies using the correct Ninja - syntax (`:`, `|`, and `||`).[^7] Use Ninja's built-in `phony` rule when - `phony` is `true`. For an `always` edge, either generate a `phony` build - with no outputs or emit a dummy output marked `restat = 1` and depend on a - permanently dirty target so the command runs on each invocation. - - Code snippet - - ```ninja - # Generated from an ir::BuildEdge - build foo.o: cc foo.c - build bar.o: cc bar.c - build my_app: link foo.o bar.o | lib_dependency.a - ``` - - A `BuildEdge` whose `dependency_order` is `serial` and has more than one - implicit dependency is an exception to this direct rendering. The generator - lowers it into staged phony gates, with one content-addressed Ninja dyndep - sidecar per dependency. A gate can reveal exactly one real dependency; the - gate edge associated with the next sidecar depends on the preceding gate. - This makes each later dependency unavailable to Ninja until the previous one - succeeds, while preserving one Ninja scheduler and its shared-work - memoization. The runner materializes every sidecar file before Ninja starts; - no Ninja edge produces sidecar content. - - The generated result is a bundle, not merely a string: generation is an - effect-free query that returns the main Ninja text and its `.netsuke/dyndep` - sidecars. Each runner command then materializes those sidecars through an - injected effective-working-directory capability before it writes or runs the - main file. The main file declares `ninja_required_version = 1.10` only when - it contains such staged serial ordering. `.netsuke/serial` and - `.netsuke/dyndep` are reserved for generated state. `serial` applies only to - direct implicit dependencies; it does not delay an independently reachable - node elsewhere in the graph. - -Figure: Runner-owned serial dyndep bundle generation and execution. - -```mermaid -sequenceDiagram - accTitle: Runner-owned serial dependency generation and execution - accDescr { - The runner generates a Ninja bundle and materializes its dyndep sidecars. - Generate writes the manifest without invoking Ninja. Build invokes Ninja - for execution, and clean invokes Ninja in clean tool mode. - } - actor User - participant Runner as runner.generate_ninja - participant NinjaGen as ninja_gen.generate_bundle - participant Dyndep as runner.materialize_dyndep_bundle - participant Ninja - - User->>Runner: netsuke build / clean / generate - Runner->>NinjaGen: generate_bundle(graph) - NinjaGen-->>Runner: GeneratedNinja (build_file, dyndep_files) - Runner->>Dyndep: materialize_dyndep_bundle(cli, bundle) - Dyndep-->>Runner: dyndep sidecars materialized - alt generate - Runner-->>User: write generated Ninja manifest without invoking Ninja - else build - Runner->>Ninja: invoke with bundle.build_file() - Ninja-->>User: serial deps run in order, parallel elsewhere - else clean - Runner->>Ninja: invoke with bundle.build_file() in clean tool mode - Ninja-->>User: clean completed - end -``` - -The runner holds a capability-scoped exclusive lease on the dyndep directory -from sidecar materialization through Ninja consumption or generated-output -consumption. While the lease is held, stale `.tmp` files are removed and -retention preserves the current bundle plus at most 32 obsolete `.dd` files and -1 MiB of obsolete `.dd` bytes. `build` and `generate` prune after -materialization; `clean` prunes only after successful `ninja -t clean` and not -on failure. Sidecars remain immutable and content-addressed. Consequently, an -older arbitrary `generate --output` manifest may lose its sidecars after a -later command and must be regenerated. See -[ADR-012](adr-012-bound-dyndep-sidecar-retention.md) for this policy. - -4\. **Write Defaults:** Finally, write the `default` statement, listing all -paths from `graph.default_targets`. - -```ninja -default my_app -``` - -### 5.4 Ninja file synthesis (`src/ninja_gen/mod.rs`) — generated bundles - -The final step is to synthesize the `build.ninja` file from the `BuildGraph` -IR. This process is a straightforward, mechanical translation from the IR data -structures to the Ninja file syntax. - -1. **Write Variables:** Any global variables that need to be passed to Ninja can - be written at the top of the file (e.g., `msvc_deps_prefix` for Windows - -2. **Write Rules:** Sort `graph.actions` by action ID and write a corresponding - Ninja `rule` statement for each executable `ir::Action`. Dependency-only - actions are omitted because they have no command to execute. Their edges - select Ninja's built-in `phony` rule. The IR already contains ordinary - command text: its input and output paths have replaced Netsuke's `ins`/ - `outs` and `$in`/`$out` placeholders during lowering. Scalar commands are - emitted as-is. List commands are emitted as the brace-group, `eval`, and - `&&` chain described in §2.3, including the bounded failure marker for each - one-based entry. - - When an action's `recipe` is a script, the generated rule wraps the script - in the configured platform-specific interpreter. Unix uses `/bin/sh -e -c`, - while Windows defaults to `powershell -Command`; an explicit manifest - `interpreter` overrides the platform default. - - Command and script text must be converted from IR text to backend text at - this stage. After Netsuke placeholders have been resolved, remaining literal - dollar signs are escaped as `$$` for Ninja so shell variables survive to the - shell. The conversion rejects newline, carriage-return, and NUL characters - and will accept a completed shell-text value once. Structured `exec` recipes - are rendered by quoting each argv element as one argument for the selected - backend. Metadata fields are escaped at their Ninja emission boundary, while - the IR remains backend-neutral. - - Resolved environment bindings are emitted as backend-specific command - prefixes or generated wrapper script assignments. The implementation must - avoid exposing Ninja variables as the user-facing environment API. - - Code snippet - - ```ninja - # Generated from an ir::Action - rule cc - command = gcc -c -o $out $in - description = CC $out - ``` - - The planned `deps_from` manifest field will populate `ir::Action.depfile` and - `ir::Action.deps_format`, allowing this rule writer to emit Ninja's - `depfile` and `deps` attributes without overloading target prerequisites. - -3. **Write Build Edges:** Iterate through the `graph.targets` map. For each - `ir::BuildEdge`, write a corresponding Ninja `build` statement. This - involves formatting the lists of explicit outputs, implicit outputs, inputs, - implicit dependencies, and order-only dependencies using the correct Ninja - syntax (`:`, `|`, and `||`).[^7] Use Ninja's built-in `phony` rule when - `phony` is `true`. For an `always` edge, either generate a `phony` build - with no outputs or emit a dummy output marked `restat = 1` and depend on a - permanently dirty target so the command runs on each invocation. - - Code snippet - - ```ninja - # Generated from an ir::BuildEdge - build foo.o: cc foo.c - build bar.o: cc bar.c - build my_app: link foo.o bar.o | lib_dependency.a - ``` +#### Serial dependency ordering and generated bundles A `BuildEdge` whose `dependency_order` is `serial` and has more than one implicit dependency is an exception to this direct rendering. The generator diff --git a/docs/roadmap.md b/docs/roadmap.md index 0a7434a8b..241be7a0f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -233,7 +233,7 @@ and agents. `netsuke::jinja::which::not_found`. - [x] Preserve argument validation diagnostics for invalid options. - [x] 3.14.5. Add regression coverage for conditional action dependency - manifests. + manifests. Depends on 3.14.2, 3.14.3, and 3.14.4. - [x] Test action-level `when` and action-level `foreach`. - [x] Test complementary nextest and legacy branches select exactly one action. diff --git a/docs/users-guide.md b/docs/users-guide.md index fef74bb85..2fc9f4f94 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -824,6 +824,10 @@ every helper's signature, defaults, purity, platform caveats, and executable examples. Host-observing helpers belong only in trusted manifests: Netsuke bounds command and network output, but does not sandbox template evaluation. +The `now(offset=...)` helper accepts `Z` or `z` for UTC and signed ISO 8601 +offsets whose absolute hour component is below 24. Offsets such as `+24:00`, +`-24:00`, and larger absolute hour values are rejected as invalid. + One helper deserves a note here because its result depends on the host's environment. `path | expanduser` expands a leading `~` against the home directory, resolved from `HOME` then `USERPROFILE` on POSIX hosts, and from diff --git a/src/manifest/expand_test_cases/command_available_selection_cases.rs b/src/manifest/expand_test_cases/command_available_selection_cases.rs index 933fa328c..6f90a7285 100644 --- a/src/manifest/expand_test_cases/command_available_selection_cases.rs +++ b/src/manifest/expand_test_cases/command_available_selection_cases.rs @@ -3,13 +3,12 @@ use std::ffi::OsString; use anyhow::{Context, Result, anyhow}; -use camino::{Utf8Path, Utf8PathBuf}; -#[cfg(unix)] -use cap_std::fs::PermissionsExt; +use camino::Utf8PathBuf; use cap_std::{ambient_authority, fs_utf8::Dir}; use googletest::prelude::*; use minijinja::Environment; use rstest::rstest; +use test_support::write_exec_with_content; use super::{actions, expand_foreach}; use crate::{ @@ -19,6 +18,16 @@ use crate::{ const ABSENT_COMMAND: &str = "netsuke-guaranteed-absent-command-3-14-5"; +#[cfg(windows)] +const NEXTEST_TOOL_NAME: &str = "cargo-nextest.cmd"; +#[cfg(not(windows))] +const NEXTEST_TOOL_NAME: &str = "cargo-nextest"; +#[cfg(windows)] +const NEXTEST_TOOL_CONTENT: &str = "@echo off\r\n"; +#[cfg(not(windows))] +const NEXTEST_TOOL_CONTENT: &str = "#!/bin/sh\nexit 0\n"; + +/// Capture the selected action's observable expansion result. struct ExpectedAction { name: &'static str, command: &'static str, @@ -26,6 +35,7 @@ struct ExpectedAction { } impl ExpectedAction { + /// Build the expected action selected when `cargo-nextest` is available. const fn nextest() -> Self { Self { name: "run-tests-nextest", @@ -34,6 +44,7 @@ impl ExpectedAction { } } + /// Build the expected fallback action when `cargo-nextest` is unavailable. const fn legacy() -> Self { Self { name: "run-tests-legacy", @@ -43,22 +54,26 @@ impl ExpectedAction { } } +/// Hold the temporary workspace used to exercise command resolution. struct ResolverWorld { _temp: tempfile::TempDir, root: Utf8PathBuf, } impl ResolverWorld { + /// Create an isolated resolver workspace with an optional `cargo-nextest` stub. fn new(has_nextest: bool) -> Result { let temp = tempfile::tempdir().context("create resolver workspace")?; let root = Utf8PathBuf::from_path_buf(temp.path().to_path_buf()) .map_err(|path| anyhow!("temporary path should be UTF-8: {}", path.display()))?; if has_nextest { - write_tool(&root, "cargo-nextest")?; + write_exec_with_content(root.as_std_path(), NEXTEST_TOOL_NAME, NEXTEST_TOOL_CONTENT) + .with_context(|| format!("write fixture tool {}", root.join(NEXTEST_TOOL_NAME)))?; } Ok(Self { _temp: temp, root }) } + /// Build a configured template environment for the selected resolver state. fn environment(&self, has_nextest: bool) -> Result> { let workspace = Dir::open_ambient_dir(&self.root, ambient_authority()) .with_context(|| format!("open resolver workspace {}", self.root))?; @@ -68,7 +83,7 @@ impl ResolverWorld { OsString::new() }; let config = StdlibConfig::new(workspace)? - .with_workspace_root_path(self.root.clone())? + .with_workspace_root_path(&self.root)? .with_path_override(path_override) .with_pathext_override(".CMD"); let mut env = Environment::new(); @@ -77,6 +92,7 @@ impl ResolverWorld { } } +/// Verify that complementary command guards select exactly one action. #[googletest::test] #[rstest] #[case::nextest_present(true, "cargo-nextest", ExpectedAction::nextest())] @@ -122,6 +138,7 @@ targets: [] Ok(()) } +/// Read a named string field from an expanded action. fn string_field<'a>(value: &'a ManifestValue, field: &str) -> Result<&'a str> { value .as_object() @@ -130,44 +147,3 @@ fn string_field<'a>(value: &'a ManifestValue, field: &str) -> Result<&'a str> { .and_then(ManifestValue::as_str) .with_context(|| format!("selected action {field}")) } - -fn write_tool(root: &Utf8Path, name: &str) -> Result<()> { - let dir = Dir::open_ambient_dir(root, ambient_authority()) - .with_context(|| format!("open fixture directory {root}"))?; - let path = Utf8PathBuf::from(tool_filename(name)); - dir.write(&path, script_contents()) - .with_context(|| format!("write fixture tool {path}"))?; - #[cfg(unix)] - mark_executable(&dir, &path)?; - Ok(()) -} - -fn tool_filename(name: &str) -> String { - if cfg!(windows) { - format!("{name}.cmd") - } else { - name.to_owned() - } -} - -const fn script_contents() -> &'static [u8] { - #[cfg(windows)] - { - b"@echo off\r\n" - } - #[cfg(not(windows))] - { - b"#!/bin/sh\nexit 0\n" - } -} - -#[cfg(unix)] -fn mark_executable(dir: &Dir, path: &Utf8Path) -> Result<()> { - let mut permissions = dir - .metadata(path) - .with_context(|| format!("stat {path}"))? - .permissions(); - permissions.set_mode(0o755); - dir.set_permissions(path, permissions) - .with_context(|| format!("chmod {path}")) -} diff --git a/src/stdlib/time/tests.rs b/src/stdlib/time/tests.rs index 57766cea0..dbef14ea5 100644 --- a/src/stdlib/time/tests.rs +++ b/src/stdlib/time/tests.rs @@ -6,6 +6,7 @@ use super::*; use anyhow::{Context, Result, anyhow, ensure}; use minijinja::{Environment, ErrorKind, context, value::Value}; +use proptest::prelude::*; use rstest::{fixture, rstest}; use time::{Duration, OffsetDateTime, UtcOffset, macros::datetime}; @@ -90,6 +91,32 @@ fn now_rejects_invalid_offset(env: Environment<'static>, #[case] offset: &str) - } } +proptest! { + /// Accept signed offsets whose hour component stays within one civil day. + #[test] + fn parse_offset_accepts_hours_below_a_civil_day( + sign in prop_oneof![Just("+"), Just("-")], + hour in 0_u8..24, + minute in 0_u8..60, + second in 0_u8..60, + ) { + let offset = format!("{sign}{hour:02}:{minute:02}:{second:02}"); + prop_assert!(parse_offset(&offset).is_ok(), "expected {offset} to be accepted"); + } + + /// Reject signed offsets at or beyond the one-civil-day boundary. + #[test] + fn parse_offset_rejects_a_civil_day_or_more( + sign in prop_oneof![Just("+"), Just("-")], + hour in 24_u8..=48, + minute in 0_u8..60, + second in 0_u8..60, + ) { + let offset = format!("{sign}{hour:02}:{minute:02}:{second:02}"); + prop_assert!(parse_offset(&offset).is_err(), "expected {offset} to be rejected"); + } +} + #[rstest] fn timedelta_defaults_to_zero(env: Environment<'static>) -> Result<()> { let value = eval_expression(&env, "timedelta()")?; diff --git a/tests/ir_from_manifest_tests.rs b/tests/ir_from_manifest_tests.rs index 450cc0855..dad08caf9 100644 --- a/tests/ir_from_manifest_tests.rs +++ b/tests/ir_from_manifest_tests.rs @@ -543,26 +543,3 @@ fn manifest_error_cases( #[path = "ir_from_manifest_tests/dependency_order.rs"] mod dependency_order; - -#[rstest] -#[case::minimal_manifest("tests/data/minimal.yml", 1, 1)] -#[case::duplicate_rules("tests/data/duplicate_rules.yml", 2, 2)] -fn manifest_file_generates_expected_graph( - #[case] manifest_path: &str, - #[case] expected_actions: usize, - #[case] expected_targets: usize, -) -> Result<()> { - let manifest = manifest::from_path(manifest_path)?; - let graph = BuildGraph::from_manifest(&manifest).context("expected graph generation")?; - let actual_actions = graph.actions.len(); - ensure!( - actual_actions == expected_actions, - "expected {expected_actions} actions, got {actual_actions}" - ); - let actual_targets = graph.targets.len(); - ensure!( - actual_targets == expected_targets, - "expected {expected_targets} targets, got {actual_targets}" - ); - Ok(()) -} diff --git a/tests/ninja_dollar_escaping_tests.rs b/tests/ninja_dollar_escaping_tests.rs index caf9c6d51..85d435632 100644 --- a/tests/ninja_dollar_escaping_tests.rs +++ b/tests/ninja_dollar_escaping_tests.rs @@ -33,6 +33,7 @@ fn host_path() -> Result { .context("host PATH is required to run Ninja") } +/// Build the action used by a generated single-edge test graph. const fn action(recipe: Recipe) -> Action { Action { recipe, @@ -44,6 +45,7 @@ const fn action(recipe: Recipe) -> Action { } } +/// Build a single-action graph for a command-escaping scenario. fn graph(recipe: Recipe, input: &str, output: &str) -> BuildGraph { let edge = BuildEdge { action_id: "action".into(), @@ -63,6 +65,7 @@ fn graph(recipe: Recipe, input: &str, output: &str) -> BuildGraph { graph } +/// Create an isolated workspace after confirming that Ninja is available. fn required_ninja_workspace() -> Result { ninja_integration_workspace().context("Ninja is required for dollar-escaping tests") } @@ -98,6 +101,7 @@ impl NinjaWorkspace { Ok(workspace) } } +/// Query Ninja's effective commands for a generated target. fn ninja_commands(ninja_file: &str, target: &str) -> Result { let workspace = NinjaWorkspace::create(ninja_file)?; @@ -118,6 +122,7 @@ fn ninja_commands(ninja_file: &str, target: &str) -> Result { String::from_utf8(output.stdout).context("Ninja command output was not UTF-8") } +/// Run a generated Ninja target and read its shell-produced output. #[cfg(unix)] fn ninja_output( ninja_file: &str, @@ -154,6 +159,7 @@ fn ninja_output( .context("read shell output from generated target") } +/// Verify that remaining shell dollars are doubled for Ninja. #[rstest] #[case::shell_variable("echo $NETSUKE_TEST_SENTINEL", "echo $$NETSUKE_TEST_SENTINEL")] #[case::shell_default( @@ -179,6 +185,7 @@ fn backend_doubles_every_residual_shell_dollar(#[case] command: &str, #[case] ex ); } +/// Verify that Netsuke placeholders lower before residual dollars are escaped. #[rstest] #[case::scalar(Recipe::Command { command: "echo $NETSUKE_TEST_SENTINEL".into() })] #[case::command_list(Recipe::Command { @@ -238,6 +245,8 @@ fn command_list_default_reaches_the_child_shell( ); Ok(()) } + +/// Verify that manifest placeholder syntax lowers inside scalar and list recipes. #[rstest] #[case::scalar("command: 'cat $in > $out'")] #[case::command_list("command:\n - 'cat $in > $out'")] @@ -296,6 +305,8 @@ fn script_default_reaches_the_child_shell( ); Ok(()) } + +/// Verify that script placeholders run against their real input and output paths. #[cfg(unix)] #[rstest] fn script_placeholders_execute_against_real_paths() -> Result<()> { @@ -311,6 +322,8 @@ fn script_placeholders_execute_against_real_paths() -> Result<()> { ); Ok(()) } + +/// Verify that placeholder-looking text inside backticks is rejected before escaping. #[rstest] fn placeholders_inside_backticks_are_rejected_before_backend_escaping() -> Result<()> { let manifest = manifest::from_str( @@ -324,6 +337,7 @@ fn placeholders_inside_backticks_are_rejected_before_backend_escaping() -> Resul Ok(()) } +/// Verify that command control characters cannot inject Ninja syntax. #[rstest] #[case::newline("echo safe\nbuild injected: action")] #[case::carriage_return("echo safe\rbuild injected: action")] @@ -341,6 +355,7 @@ fn command_control_characters_are_rejected(#[case] command: &str) { ); } +/// Verify that Ninja-unsafe path characters are rejected during generation. #[rstest] #[case::dollar("input$file")] #[case::space("input file")] @@ -364,6 +379,7 @@ fn unsafe_paths_are_rejected(#[case] input: &str) { ); } +/// Verify that commands without dollars retain their exact text. #[rstest] fn dollar_free_commands_remain_byte_identical() -> Result<()> { let ninja = generate(&graph( diff --git a/tests/ninja_snapshot_tests.rs b/tests/ninja_snapshot_tests.rs index 1b57b9100..bd94ecaf7 100644 --- a/tests/ninja_snapshot_tests.rs +++ b/tests/ninja_snapshot_tests.rs @@ -9,20 +9,19 @@ use cap_std::{ambient_authority, fs_utf8::Dir}; use insta::{Settings, assert_snapshot}; use netsuke::{ir::BuildGraph, manifest, ninja_gen, stdlib::StdlibConfig}; #[cfg(unix)] -use std::{ - fs as std_fs, - io::Write, - path::{Path, PathBuf}, - process::Command, - time::Duration, -}; +use std::process::Command; #[cfg(unix)] -use tempfile::{TempDir, tempdir}; +use tempfile::tempdir; #[cfg(unix)] use test_support::ensure_binaries_available; #[cfg(unix)] use test_support::fs; +#[cfg(unix)] +#[path = "ninja_snapshot_tests/conditional_action_deps.rs"] +mod conditional_action_deps; + +/// Run a command and return its UTF-8 standard output when it succeeds. #[cfg(unix)] fn run_ok(cmd: &mut Command) -> Result { let out = cmd.output().context("failed to spawn command")?; @@ -263,176 +262,6 @@ fn implicit_deps_manifest_ninja_snapshot() -> Result<()> { Ok(()) } -#[cfg(unix)] -#[test] -fn conditional_action_deps_ninja_snapshot() -> Result<()> { - let manifest = manifest::from_path("tests/data/conditional_action_deps.yml")?; - let ir = BuildGraph::from_manifest(&manifest)?; - let ninja_content = ninja_gen::generate(&ir)?; - - assert_dependency_classes( - &ninja_content, - &ExpectedNinjaEdge { - output: "fallback-alpha", - input: "src/alpha.in", - implicit_deps: "build/alpha.o shared/action.cfg", - order_only_deps: "order/alpha.stamp", - }, - )?; - assert_dependency_classes( - &ninja_content, - &ExpectedNinjaEdge { - output: "fallback-beta", - input: "src/beta.in", - implicit_deps: "build/beta.o shared/action.cfg", - order_only_deps: "order/beta.stamp", - }, - )?; - assert_dependency_classes( - &ninja_content, - &ExpectedNinjaEdge { - output: "out/fallback", - input: "src/target.in", - implicit_deps: "include/fallback.h", - order_only_deps: "order/target.stamp", - }, - )?; - ensure!( - !ninja_content.contains("preferred"), - "filtered branches should not appear in Ninja output:\n{ninja_content}" - ); - - let mut settings = Settings::new(); - settings.set_snapshot_path(concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/snapshots/ninja" - )); - settings.bind(|| { - assert_snapshot!("conditional_action_deps_ninja", ninja_content); - }); - - validate_conditional_ninja(&ninja_content) -} - -#[cfg(unix)] -struct ExpectedNinjaEdge<'a> { - output: &'a str, - input: &'a str, - implicit_deps: &'a str, - order_only_deps: &'a str, -} - -#[cfg(unix)] -fn assert_dependency_classes(ninja_content: &str, expected: &ExpectedNinjaEdge<'_>) -> Result<()> { - let build_line = ninja_content - .lines() - .find(|line| line.starts_with(&format!("build {}:", expected.output))) - .with_context(|| format!("expected build line for {}", expected.output))?; - ensure!( - build_line.contains(&format!( - " {} | {} || {}", - expected.input, expected.implicit_deps, expected.order_only_deps - )), - "unexpected dependency classes for {}: {build_line}", - expected.output - ); - Ok(()) -} - -#[cfg(unix)] -fn validate_conditional_ninja(ninja_content: &str) -> Result<()> { - if let Err(err) = ensure_binaries_available(&[("ninja", &["--version"])]) { - tracing::warn!("skipping real Ninja validation: {}", err); - writeln!( - std::io::stderr().lock(), - "skipping real Ninja validation: {err}" - ) - .context("write real Ninja skip reason")?; - return Ok(()); - } - - let (dir, build_file) = prepare_conditional_ninja_workspace(ninja_content)?; - assert_conditional_ninja_selection(&dir, &build_file)?; - mark_conditional_ninja_output_up_to_date(&dir)?; - assert_conditional_ninja_no_op(&dir, &build_file) -} - -#[cfg(unix)] -fn prepare_conditional_ninja_workspace(ninja_content: &str) -> Result<(TempDir, PathBuf)> { - let dir = tempdir().context("create temp dir for conditional Ninja validation")?; - let build_file = dir.path().join("build.ninja"); - std_fs::write(&build_file, ninja_content) - .with_context(|| format!("write Ninja file to {}", build_file.display()))?; - for relative_path in ["src/target.in", "include/fallback.h", "order/target.stamp"] { - let dependency_path = dir.path().join(relative_path); - let parent = dependency_path - .parent() - .context("dependency path should have parent")?; - std_fs::create_dir_all(parent) - .with_context(|| format!("create dependency directory {}", parent.display()))?; - std_fs::write(&dependency_path, "") - .with_context(|| format!("write Ninja dependency {}", dependency_path.display()))?; - } - - Ok((dir, build_file)) -} - -#[cfg(unix)] -fn run_conditional_ninja(dir: &TempDir, build_file: &Path, args: &[&str]) -> Result { - let mut cmd = Command::new("ninja"); - cmd.arg("-f").arg(build_file).args(args); - cmd.current_dir(dir.path()); - run_ok(&mut cmd) -} - -#[cfg(unix)] -fn assert_conditional_ninja_selection(dir: &TempDir, build_file: &Path) -> Result<()> { - run_conditional_ninja(dir, build_file, &["-t", "query", "fallback-alpha"])?; - run_conditional_ninja(dir, build_file, &["-t", "query", "out/fallback"])?; - let first = run_conditional_ninja(dir, build_file, &["-n", "out/fallback"])?; - ensure!( - first.contains("echo fallback"), - "expected dry run to reach selected target, got:\n{first}" - ); - - Ok(()) -} - -#[cfg(unix)] -fn mark_conditional_ninja_output_up_to_date(dir: &TempDir) -> Result<()> { - let output = dir.path().join("out/fallback"); - let output_dir = output.parent().context("output path should have parent")?; - std_fs::create_dir_all(output_dir) - .with_context(|| format!("create output directory {}", output_dir.display()))?; - std_fs::write(&output, "") - .with_context(|| format!("write up-to-date output {}", output.display()))?; - let latest_dependency = dir.path().join("order/target.stamp"); - let output_modified = std_fs::metadata(&latest_dependency) - .with_context(|| format!("stat dependency {}", latest_dependency.display()))? - .modified() - .context("read dependency modification time")? - + Duration::from_secs(1); - std_fs::File::options() - .write(true) - .open(&output) - .with_context(|| format!("open output {}", output.display()))? - .set_times(std_fs::FileTimes::new().set_modified(output_modified)) - .with_context(|| format!("set output time {}", output.display()))?; - - Ok(()) -} - -#[cfg(unix)] -fn assert_conditional_ninja_no_op(dir: &TempDir, build_file: &Path) -> Result<()> { - run_conditional_ninja(dir, build_file, &["out/fallback"])?; - let second = run_conditional_ninja(dir, build_file, &["-n", "out/fallback"])?; - ensure!( - second.contains("no work to do"), - "expected no-op second pass, got:\n{second}" - ); - Ok(()) -} - #[test] fn dependency_only_manifest_ninja_snapshot() -> Result<()> { let manifest_yaml = r#" diff --git a/tests/ninja_snapshot_tests/conditional_action_deps.rs b/tests/ninja_snapshot_tests/conditional_action_deps.rs new file mode 100644 index 000000000..06f90e879 --- /dev/null +++ b/tests/ninja_snapshot_tests/conditional_action_deps.rs @@ -0,0 +1,188 @@ +//! Snapshot and executable-Ninja coverage for conditional action dependencies. +//! +//! Keeps the conditional dependency fixture and its real-Ninja validation +//! isolated from the general snapshot cases while sharing their command runner. + +use super::run_ok; +use anyhow::{Context, Result, ensure}; +use insta::{Settings, assert_snapshot}; +use netsuke::{ir::BuildGraph, manifest, ninja_gen}; +use std::{ + fs as std_fs, + io::Write, + path::{Path, PathBuf}, + process::Command, + time::Duration, +}; +use tempfile::{TempDir, tempdir}; +use test_support::ensure_binaries_available; + +/// Snapshot and execute the selected conditional-action dependency graph. +#[test] +fn conditional_action_deps_ninja_snapshot() -> Result<()> { + let manifest = manifest::from_path("tests/data/conditional_action_deps.yml")?; + let ir = BuildGraph::from_manifest(&manifest)?; + let ninja_content = ninja_gen::generate(&ir)?; + + assert_dependency_classes( + &ninja_content, + &ExpectedNinjaEdge { + output: "fallback-alpha", + input: "src/alpha.in", + implicit_deps: "build/alpha.o shared/action.cfg", + order_only_deps: "order/alpha.stamp", + }, + )?; + assert_dependency_classes( + &ninja_content, + &ExpectedNinjaEdge { + output: "fallback-beta", + input: "src/beta.in", + implicit_deps: "build/beta.o shared/action.cfg", + order_only_deps: "order/beta.stamp", + }, + )?; + assert_dependency_classes( + &ninja_content, + &ExpectedNinjaEdge { + output: "out/fallback", + input: "src/target.in", + implicit_deps: "include/fallback.h", + order_only_deps: "order/target.stamp", + }, + )?; + ensure!( + !ninja_content.contains("preferred"), + "filtered branches should not appear in Ninja output:\n{ninja_content}" + ); + + let mut settings = Settings::new(); + settings.set_snapshot_path(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/snapshots/ninja" + )); + settings.bind(|| { + assert_snapshot!("conditional_action_deps_ninja", ninja_content); + }); + + validate_conditional_ninja(&ninja_content) +} + +/// Describe the expected Ninja dependency classes for a generated output. +struct ExpectedNinjaEdge<'a> { + output: &'a str, + input: &'a str, + implicit_deps: &'a str, + order_only_deps: &'a str, +} + +/// Assert that a generated edge retains explicit, implicit, and order-only inputs. +fn assert_dependency_classes(ninja_content: &str, expected: &ExpectedNinjaEdge<'_>) -> Result<()> { + let build_line = ninja_content + .lines() + .find(|line| line.starts_with(&format!("build {}:", expected.output))) + .with_context(|| format!("expected build line for {}", expected.output))?; + ensure!( + build_line.contains(&format!( + " {} | {} || {}", + expected.input, expected.implicit_deps, expected.order_only_deps + )), + "unexpected dependency classes for {}: {build_line}", + expected.output + ); + Ok(()) +} + +/// Validate the generated conditional Ninja file with a real Ninja installation. +fn validate_conditional_ninja(ninja_content: &str) -> Result<()> { + if let Err(err) = ensure_binaries_available(&[("ninja", &["--version"])]) { + tracing::warn!("skipping real Ninja validation: {}", err); + writeln!( + std::io::stderr().lock(), + "skipping real Ninja validation: {err}" + ) + .context("write real Ninja skip reason")?; + return Ok(()); + } + + let (dir, build_file) = prepare_conditional_ninja_workspace(ninja_content)?; + assert_conditional_ninja_selection(&dir, &build_file)?; + mark_conditional_ninja_output_up_to_date(&dir)?; + assert_conditional_ninja_no_op(&dir, &build_file) +} + +/// Create the files required to run the conditional Ninja fixture. +fn prepare_conditional_ninja_workspace(ninja_content: &str) -> Result<(TempDir, PathBuf)> { + let dir = tempdir().context("create temp dir for conditional Ninja validation")?; + let build_file = dir.path().join("build.ninja"); + std_fs::write(&build_file, ninja_content) + .with_context(|| format!("write Ninja file to {}", build_file.display()))?; + for relative_path in ["src/target.in", "include/fallback.h", "order/target.stamp"] { + let dependency_path = dir.path().join(relative_path); + let parent = dependency_path + .parent() + .context("dependency path should have parent")?; + std_fs::create_dir_all(parent) + .with_context(|| format!("create dependency directory {}", parent.display()))?; + std_fs::write(&dependency_path, "") + .with_context(|| format!("write Ninja dependency {}", dependency_path.display()))?; + } + + Ok((dir, build_file)) +} + +/// Run Ninja with the conditional fixture as its build file. +fn run_conditional_ninja(dir: &TempDir, build_file: &Path, args: &[&str]) -> Result { + let mut cmd = Command::new("ninja"); + cmd.arg("-f").arg(build_file).args(args); + cmd.current_dir(dir.path()); + run_ok(&mut cmd) +} + +/// Confirm that Ninja selects the fallback branch and schedules its command. +fn assert_conditional_ninja_selection(dir: &TempDir, build_file: &Path) -> Result<()> { + run_conditional_ninja(dir, build_file, &["-t", "query", "fallback-alpha"])?; + run_conditional_ninja(dir, build_file, &["-t", "query", "out/fallback"])?; + let first = run_conditional_ninja(dir, build_file, &["-n", "out/fallback"])?; + ensure!( + first.contains("echo fallback"), + "expected dry run to reach selected target, got:\n{first}" + ); + + Ok(()) +} + +/// Mark the fallback output newer than its order-only dependency. +fn mark_conditional_ninja_output_up_to_date(dir: &TempDir) -> Result<()> { + let output = dir.path().join("out/fallback"); + let output_dir = output.parent().context("output path should have parent")?; + std_fs::create_dir_all(output_dir) + .with_context(|| format!("create output directory {}", output_dir.display()))?; + std_fs::write(&output, "") + .with_context(|| format!("write up-to-date output {}", output.display()))?; + let latest_dependency = dir.path().join("order/target.stamp"); + let output_modified = std_fs::metadata(&latest_dependency) + .with_context(|| format!("stat dependency {}", latest_dependency.display()))? + .modified() + .context("read dependency modification time")? + + Duration::from_secs(1); + std_fs::File::options() + .write(true) + .open(&output) + .with_context(|| format!("open output {}", output.display()))? + .set_times(std_fs::FileTimes::new().set_modified(output_modified)) + .with_context(|| format!("set output time {}", output.display()))?; + + Ok(()) +} + +/// Confirm that the prepared conditional output has no pending Ninja work. +fn assert_conditional_ninja_no_op(dir: &TempDir, build_file: &Path) -> Result<()> { + run_conditional_ninja(dir, build_file, &["out/fallback"])?; + let second = run_conditional_ninja(dir, build_file, &["-n", "out/fallback"])?; + ensure!( + second.contains("no work to do"), + "expected no-op second pass, got:\n{second}" + ); + Ok(()) +} diff --git a/tests/snapshots/ninja/ninja_snapshot_tests__conditional_action_deps_ninja.snap b/tests/snapshots/ninja/ninja_snapshot_tests__conditional_action_deps__conditional_action_deps_ninja.snap similarity index 90% rename from tests/snapshots/ninja/ninja_snapshot_tests__conditional_action_deps_ninja.snap rename to tests/snapshots/ninja/ninja_snapshot_tests__conditional_action_deps__conditional_action_deps_ninja.snap index e82befe75..8cfddcabf 100644 --- a/tests/snapshots/ninja/ninja_snapshot_tests__conditional_action_deps_ninja.snap +++ b/tests/snapshots/ninja/ninja_snapshot_tests__conditional_action_deps__conditional_action_deps_ninja.snap @@ -1,5 +1,6 @@ --- -source: tests/ninja_snapshot_tests.rs +source: tests/ninja_snapshot_tests/conditional_action_deps.rs +assertion_line: 65 expression: ninja_content --- rule dfd2ff5921f146a85d62fcfeb725508df48b315134db4f325585bd6981940de6 From 9b6b07a36e3a603c5ac20d29963c76c9d17e56be Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 31 Aug 2026 00:44:34 +0200 Subject: [PATCH 32/34] Record Markdown rebase decision Record the target formatter contract and the target-baseline lockfile regeneration so the completed ExecPlan remains reproducible. --- ...r-conditional-action-dependency-manifests.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md index 03bf7ebaf..7169774f7 100644 --- a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md +++ b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md @@ -182,6 +182,10 @@ escalation, not workarounds. review feedback; recorded its production scope, documented the accepted and rejected offset forms, and added the boundary to the validation scope. +- [x] (2026-08-31) Rebased onto + `origin/chore/enforce-markdown-table-formatting`; retained its canonical + non-mutating Markdown check and regenerated the lockfile from that + target baseline before running the full rebase validation stack. (Timestamps to be added as work proceeds.) @@ -363,6 +367,14 @@ escalation, not workarounds. parsing, where both `+24:00` and `-24:00` are rejected before timestamp conversion. Date/Author: 2026-08-31, review follow-up. +- Decision: Adopt the target branch's canonical Markdown-formatting contract + during the rebase. Rationale: `make check-fmt` now checks Rust, Python, and + Markdown formatting, while `scripts/check-markdown-format.sh` formats + isolated copies with `mdtablefix` and accepts only the exact LF or CRLF + rendering. Two replayed formatter commits were therefore superseded and + skipped: the target already includes that safer implementation and its CI + coverage. Date/Author: 2026-08-31, rebase follow-up. + ## Outcomes & retrospective Stage A completed without requiring a new seam or changing production code @@ -1083,3 +1095,8 @@ Skills to load while implementing: user-facing contract after review. Validation scope now includes `Z`/`z`, signed offsets below 24 hours, and rejection of both signs at `24:00` and beyond; the full gate stack remains the required final validation. + +- 2026-08-31 — Rebased onto `origin/chore/enforce-markdown-table-formatting`. + Kept the target's canonical `mdtablefix` checker instead of replaying older + duplicate formatter commits, then reset `Cargo.lock` to the target baseline + and regenerated it before the rebase gates. From a39654308088673329eb40967974d2ddb7850615 Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 31 Aug 2026 10:00:49 +0200 Subject: [PATCH 33/34] Complete conditional dependency regression coverage Document the reviewed compatibility boundaries and add focused UTC shorthand coverage. Keep executable fixtures shared, preserve lossless diagnostics, and make workflow validation structurally typed. --- AGENTS.md | 4 +- ...conditional-action-dependency-manifests.md | 138 ++++++++++++------ docs/stdlib-yaml-and-jinja-guide.md | 8 +- docs/users-guide.md | 16 ++ .../command_available_selection_cases.rs | 2 +- src/stdlib/time/tests.rs | 11 ++ tests/documentation_examples_tests.rs | 2 + tests/ir_from_manifest_tests.rs | 11 ++ tests/ninja_dollar_escaping_tests.rs | 18 ++- tests/ninja_snapshot_tests.rs | 6 + tests/workflow_contracts/ci_lint_test.py | 26 ++-- 11 files changed, 183 insertions(+), 59 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 707b3ecd7..24c684b64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -218,7 +218,9 @@ directive anywhere. ```sh RUSTDOCFLAGS="--cfg docsrs -D warnings" \ - uv run --no-project --python $(PYTHON_BASELINE) scripts/doc-coverage.py --threshold 80 + uv run --no-project --python $(PYTHON_BASELINE) \ + scripts/doc-coverage.py --toolchain "$DOC_COVERAGE_TOOLCHAIN" \ + --threshold "$DOC_COVERAGE_THRESHOLD" ``` measuring the share of documented items across every workspace library diff --git a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md index 7169774f7..b94c714fb 100644 --- a/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md +++ b/docs/execplans/3-14-5-regression-coverage-for-conditional-action-dependency-manifests.md @@ -39,27 +39,30 @@ if any of the following regress: including for conditionally-selected actions. The deliverable is test code plus small deterministic fixtures and -documentation. The branch also carries the `now(offset=...)` compatibility -guard in `parse_offset`: after the dependency update accepted offsets beyond a -civil day, the parser rejects offsets whose absolute whole-hour component is 24 -or greater. This preserves the documented ISO 8601 input contract while leaving -valid offsets unchanged. +documentation. The branch also carries two compatibility boundaries: the +`parse_offset` guard rejects offsets whose absolute whole-hour component is 24 +or greater after the dependency update accepted values beyond a civil day, and +`register_legacy_boolean_formatter` preserves lowercase Boolean interpolation +for existing manifests. Together these preserve the documented ISO 8601 input +and historical MiniJinja rendering contracts while leaving other behaviour +unchanged. ## Constraints Hard invariants that must hold throughout implementation. Violation requires escalation, not workarounds. -- This is primarily a **test-and-documentation** change. The one production - compatibility guard in `src/stdlib/time/mod.rs` is in scope: `parse_offset` +- This is primarily a test-and-documentation change, with two production + compatibility boundaries in scope. `parse_offset` in `src/stdlib/time/mod.rs` must reject parsed offsets whose absolute whole-hour component is 24 or - greater, because the dependency update accepts values beyond a civil day. Do - not modify other production semantics in `src/manifest/`, `src/ir/`, - `src/ninja_gen.rs`, or `src/stdlib/`. Any further production change requires - escalation. The single test-only seam exception remains available only if an - existing public or `pub(crate)` surface cannot drive a required scenario; any - such seam must be recorded in the Decision Log and must not change runtime - behaviour. + greater because the dependency update accepts values beyond a civil day. + `register_legacy_boolean_formatter` in `src/stdlib/register.rs` must preserve + lowercase Boolean interpolation for existing manifests. Do not modify other + production semantics in `src/manifest/`, `src/ir/`, `src/ninja_gen.rs`, or + `src/stdlib/`. Any further production change requires escalation. The single + test-only seam exception remains available only if an existing public or + `pub(crate)` surface cannot drive a required scenario; any such seam must be + recorded in the Decision Log and must not change runtime behaviour. - Do not weaken or delete existing tests. New tests are additive. Existing passing tests must continue to pass unchanged. - Preserve the existing implicit `phony: true` behaviour for actions; tests @@ -174,14 +177,17 @@ escalation, not workarounds. dependencies after reverting the throwaway spike. - [x] (2026-07-28 23:14Z) Stage B: added B.1–B.5 tests and fixtures with a passing focused run and assertion-level sabotage evidence for each. -- [x] (2026-07-29 00:49Z) Stage C: updated the users' guide, developers' guide, - component design, and roadmap; recorded why no ADR is warranted. -- [x] (2026-07-29 00:55Z) Stage D: passed the full gate stack and final +- [x] (2026-08-30) Stage C: updated the users' guide, developers' guide, + component design, and roadmap; recorded the Boolean formatter contract + and why no ADR is warranted. +- [x] (2026-08-30) Stage D: passed the full gate stack and final CodeRabbit review; roadmap item 3.14.5 is complete. - [x] (2026-08-31) Reviewed the `now(offset=...)` compatibility guard after review feedback; recorded its production scope, documented the accepted - and rejected offset forms, and added the boundary to the validation - scope. + and rejected offset forms, added focused property-based boundary + coverage, and added the boundary to the validation scope. +- [x] (2026-08-31) Added explicit example-based `parse_offset("Z")` and + `parse_offset("z")` regression cases, each asserting UTC. - [x] (2026-08-31) Rebased onto `origin/chore/enforce-markdown-table-formatting`; retained its canonical non-mutating Markdown check and regenerated the lockfile from that @@ -225,6 +231,12 @@ escalation, not workarounds. Evidence and inventory are in "Context and orientation". Impact: 3.14.5 is a *gap-fill*, not a green-field test suite. +- Observation: the MiniJinja dependency update changed Boolean interpolation + spelling in string fields. The registration-wide formatter is therefore the + narrow compatibility boundary for existing manifests; non-Boolean values + continue through MiniJinja's normal formatter. Date/Author: 2026-08-30, + review follow-up. + - Observation: `#[googletest::test]` and `#[rstest]` interoperate under the current dependency set when the googletest attribute comes first. Evidence: the throwaway Stage A spike ran exactly one generated case and reported @@ -270,9 +282,9 @@ escalation, not workarounds. ## Decision log -- Decision: Treat 3.14.5 as test-and-docs only; the implementation - (3.14.2–3.14.4) is already complete. Rationale: the roadmap marks - 3.14.2/3.14.3/3.14.4 done; recon confirms the behaviour is present. The +- Decision: Treat 3.14.5 as primarily test-and-documentation work; the + implementation (3.14.2–3.14.4) is already complete. Rationale: the roadmap + marks 3.14.2/3.14.3/3.14.4 done; recon confirms the behaviour is present. The roadmap bullet text is literally "Test …". Date/Author: 2026-06-15, planning agent. @@ -317,13 +329,13 @@ escalation, not workarounds. `fresh=true`. Recording this prevents future cargo-culted serialization. Date/Author: 2026-06-15, planning agent. -- Decision: Defer property-based and bounded-model coverage of the same - invariants to roadmap 4.2.x (Kani) and 4.3.2 (Proptest for manifest expansion - invariants). Rationale: 3.14.5 is scoped to example/behavioural/snapshot - regression coverage; 4.3.2 already owns "foreach preserves non-control - fields", "when is removed after evaluation", and "item/index injected". - Duplicating that here would create two owners for one invariant. Date/Author: - 2026-06-15, planning agent. +- Decision: Defer property-based and bounded-model coverage of the manifest + expansion invariants to roadmap 4.2.x (Kani) and 4.3.2 (Proptest for manifest + expansion invariants). Rationale: 3.14.5 is scoped to + example/behavioural/snapshot regression coverage; 4.3.2 already owns "foreach + preserves non-control fields", "when is removed after evaluation", and + "item/index injected". Duplicating that here would create two owners for one + invariant. Date/Author: 2026-06-15, planning agent. - Decision: `ortho_config` is out of scope for this item. Rationale: `ortho_config` governs layered CLI/configuration precedence @@ -367,6 +379,14 @@ escalation, not workarounds. parsing, where both `+24:00` and `-24:00` are rejected before timestamp conversion. Date/Author: 2026-08-31, review follow-up. +- Decision: Register `register_legacy_boolean_formatter` at the standard + library registration boundary as one global policy. Rationale: this keeps + Boolean values interpolated into string fields at the historical lowercase + `true`/`false` spelling across all helpers, while delegating non-Boolean + values to MiniJinja's normal formatter. Per-helper or per-call formatters + would create inconsistent manifest behaviour. Date/Author: 2026-08-30, review + follow-up. + - Decision: Adopt the target branch's canonical Markdown-formatting contract during the rebase. Rationale: `make check-fmt` now checks Rust, Python, and Markdown formatting, while `scripts/check-markdown-format.sh` formats @@ -378,10 +398,11 @@ escalation, not workarounds. ## Outcomes & retrospective Stage A completed without requiring a new seam or changing production code -outside the pre-existing `parse_offset` compatibility guard. The approved dev -dependencies resolve to googletest 0.14.3 and pretty_assertions 1.4.1. The real -resolver injection, expansion entry point, and deterministic Ninja dependency -ordering all remain available as planned. +outside the two in-scope compatibility boundaries: the `parse_offset` guard and +the registration-wide legacy Boolean formatter. The approved dev dependencies +resolve to googletest 0.14.3 and pretty_assertions 1.4.1. The real resolver +injection, expansion entry point, and deterministic Ninja dependency ordering +all remain available as planned. B.1 now pins complementary action selection through the real resolver in both deterministic worlds. The present case also combines action `foreach` with @@ -417,13 +438,20 @@ states the manifest/stdlib port boundary in the design, and marks roadmap item architecture and behaviour are unchanged. The completed change is primarily test-and-documentation work, with the -`parse_offset` compatibility guard explicitly retained as the sole production -behaviour boundary. It adds deterministic real-resolver unit coverage, -selection-time impurity coverage, conditional IR dependency assertions, a -reviewed Ninja snapshot with real-Ninja validation, and one combined BDD -scenario. All five focused sabotage checks failed at the intended assertion. -The final branch-wide formatting, documentation, type, lint, and test gates -passed, and CodeRabbit reported no actionable findings. +`parse_offset` guard and registration-wide legacy Boolean formatter explicitly +retained as the two production compatibility boundaries. It adds deterministic +real-resolver unit coverage, selection-time impurity coverage, conditional IR +dependency assertions, a reviewed Ninja snapshot with real-Ninja validation, +and one combined BDD scenario. It also adds property-based coverage showing +that signed offsets below 24 hours are accepted while both signs at 24 hours +and above are rejected. All five focused sabotage checks failed at the intended +assertion. The prior branch-wide formatting, documentation, type, lint, and +test gates passed, and CodeRabbit reported no actionable findings. The current +follow-up also passes all recorded validation gates: `make check-fmt`, +`make lint-clippy`, `make typecheck`, and `make lint`; the corrected +generated-name selection run passes 2/2 after the requested positional filter +selected 0 tests, time passes 31/31, Ninja passes 7/7, IR passes 23/23, doc +coverage is 99.04%, and `make test` passes 2,654/2,654 tests plus 33 doctests. ## Context and orientation @@ -764,6 +792,13 @@ and the existing suite unchanged. ## Validation and acceptance +Validation status for the current review follow-up is complete. +`make check-fmt`, `make lint-clippy`, `make typecheck`, and `make lint` passed; +the corrected generated-name selection run passed 2/2 after the requested +positional filter selected 0 tests, time passed 31/31, Ninja passed 7/7, IR +passed 23/23, doc coverage was 99.04%, and `make test` passed 2,654/2,654 tests +plus 33 doctests. + Acceptance is behavioural: 1. `make test` passes with the new tests present. @@ -780,6 +815,9 @@ Acceptance is behavioural: the generated file builds and a second pass reports "no work to do". 4. BDD scenarios for selection and deps emission pass via the `bdd_tests` harness. +5. Time parsing has explicit example-based regression cases for both `Z` and + `z`, each successfully producing UTC; property-based cases cover signed + numeric offsets below and at or beyond the 24-hour boundary. Quality criteria ("done"): @@ -789,7 +827,8 @@ Quality criteria ("done"): with `-D warnings`. - Docs: users-guide, developers-guide, component architecture (and ADR if created) updated; markdown lint clean. -- Review: `coderabbit review --agent` concerns all resolved. +- Review: `coderabbit review --agent` concerns all resolved after the current + documentation and test follow-up is validated. ## Idempotence and recovery @@ -1010,6 +1049,20 @@ Skills to load while implementing: ## Revision note +- 2026-08-30 — Recorded the registration-wide + `register_legacy_boolean_formatter` policy and its user-visible contract: + Boolean interpolation in string fields retains lowercase `true`/`false`, + while non-Boolean values use MiniJinja's normal formatter. Added the + corresponding users' guide example and retained the `parse_offset` guard as + the production compatibility boundary. + +- 2026-08-31 — Review follow-up records that `parse_offset` rejects parsed + offsets whose absolute whole-hour component is 24 or greater, aligning + `now(offset=...)` with the ISO 8601 civil-day limit. User documentation and + property-based boundary coverage are present. Explicit example-based `Z` and + `z` cases now assert UTC, and the updated documentation and test follow-up + passes its complete validation stack. + - 2026-06-15 — Revised after a community-of-experts review (Logisphere crew: structural, contract/correctness, alternatives/DX, reliability/ops lenses). What changed and why: @@ -1094,7 +1147,8 @@ Skills to load while implementing: - 2026-08-31 — Recorded the `now(offset=...)` compatibility guard and its user-facing contract after review. Validation scope now includes `Z`/`z`, signed offsets below 24 hours, and rejection of both signs at `24:00` and - beyond; the full gate stack remains the required final validation. + beyond; the explicit examples, property cases, and full gate stack now pass + as recorded under Validation and acceptance. - 2026-08-31 — Rebased onto `origin/chore/enforce-markdown-table-formatting`. Kept the target's canonical `mdtablefix` checker instead of replaying older diff --git a/docs/stdlib-yaml-and-jinja-guide.md b/docs/stdlib-yaml-and-jinja-guide.md index c7e1a5d23..13848c887 100644 --- a/docs/stdlib-yaml-and-jinja-guide.md +++ b/docs/stdlib-yaml-and-jinja-guide.md @@ -260,9 +260,11 @@ defaults: ## Work with time - `now([offset=...])` is host-observing and returns the current timestamp. - With no offset it uses UTC; `offset` accepts `Z` or a signed offset such as - `+02:00`. The value exposes `iso8601`, `unix_timestamp`, and `offset`. - Example: `{{ now(offset='+02:00').iso8601 }}`. + With no offset it uses UTC; `offset` accepts `Z` or `z` for UTC and signed + ISO 8601 numeric offsets such as `+02:00`. The absolute whole-hour component + must be below 24; `+24:00` and `-24:00` are invalid and return the existing + invalid-offset error. The value exposes `iso8601`, `unix_timestamp`, and + `offset`. Example: `{{ now(offset='+02:00').iso8601 }}`. - `timedelta(**components)` is pure. It accepts `weeks`, `days`, `hours`, `minutes`, `seconds`, `milliseconds`, `microseconds`, and `nanoseconds`. Every component defaults to zero and may be negative. The result exposes diff --git a/docs/users-guide.md b/docs/users-guide.md index 2fc9f4f94..23e95192c 100644 --- a/docs/users-guide.md +++ b/docs/users-guide.md @@ -824,6 +824,22 @@ every helper's signature, defaults, purity, platform caveats, and executable examples. Host-observing helpers belong only in trusted manifests: Netsuke bounds command and network output, but does not sandbox template evaluation. +When a Boolean is interpolated into a string field, Netsuke renders it as +lowercase `true` or `false`. For example, this writes `true` to `status.txt`: + + + +```yaml +netsuke_version: "1.0.0" + +vars: + enabled: true + +targets: + - name: status.txt + command: "printf '%s\\n' '{{ enabled }}' > {{ outs }}" +``` + The `now(offset=...)` helper accepts `Z` or `z` for UTC and signed ISO 8601 offsets whose absolute hour component is below 24. Offsets such as `+24:00`, `-24:00`, and larger absolute hour values are rejected as invalid. diff --git a/src/manifest/expand_test_cases/command_available_selection_cases.rs b/src/manifest/expand_test_cases/command_available_selection_cases.rs index 6f90a7285..22ee9521e 100644 --- a/src/manifest/expand_test_cases/command_available_selection_cases.rs +++ b/src/manifest/expand_test_cases/command_available_selection_cases.rs @@ -8,7 +8,7 @@ use cap_std::{ambient_authority, fs_utf8::Dir}; use googletest::prelude::*; use minijinja::Environment; use rstest::rstest; -use test_support::write_exec_with_content; +use test_support::exec::write_exec_with_content; use super::{actions, expand_foreach}; use crate::{ diff --git a/src/stdlib/time/tests.rs b/src/stdlib/time/tests.rs index dbef14ea5..0125ce86f 100644 --- a/src/stdlib/time/tests.rs +++ b/src/stdlib/time/tests.rs @@ -72,6 +72,17 @@ fn now_applies_custom_offset(env: Environment<'static>) -> Result<()> { Ok(()) } +/// Accept upper- and lower-case UTC shorthand offsets. +#[rstest] +#[case::uppercase("Z")] +#[case::lowercase("z")] +fn now_accepts_utc_shorthand(env: Environment<'static>, #[case] offset: &str) -> Result<()> { + let value = eval_expression(&env, &format!("now(offset='{offset}')"))?; + let captured = value_as_timestamp(&value)?; + ensure!(captured.offset() == UtcOffset::UTC); + Ok(()) +} + #[rstest] #[case::nonsense("bogus")] #[case::missing_sign("01:00")] diff --git a/tests/documentation_examples_tests.rs b/tests/documentation_examples_tests.rs index e8aa836a8..bf422fd23 100644 --- a/tests/documentation_examples_tests.rs +++ b/tests/documentation_examples_tests.rs @@ -18,6 +18,7 @@ use test_support::netsuke::{NetsukeRun, run_netsuke_in, run_netsuke_in_with_env} const EXPECTED_EXAMPLE_IDS: &[&str] = &[ "guide-accessible-output", "guide-binstall-install", + "guide-boolean-string-interpolation", "guide-cli-usage", "guide-command-available-manifest", "guide-command-list", @@ -158,6 +159,7 @@ fn every_documented_fence_has_a_known_unique_identifier() -> Result<()> { #[rstest] #[case("readme-first-build-manifest")] #[case("guide-first-build-manifest")] +#[case("guide-boolean-string-interpolation")] #[case("guide-complete-manifest")] #[case("guide-foreach-manifest")] #[case("guide-macro-manifest")] diff --git a/tests/ir_from_manifest_tests.rs b/tests/ir_from_manifest_tests.rs index dad08caf9..06c78545d 100644 --- a/tests/ir_from_manifest_tests.rs +++ b/tests/ir_from_manifest_tests.rs @@ -16,6 +16,7 @@ use netsuke::{ }; use rstest::rstest; +/// Generate the expected action and target counts for each manifest fixture. #[rstest] #[case::minimal_manifest("tests/data/minimal.yml", 1, 1)] #[case::duplicate_rules("tests/data/duplicate_rules.yml", 2, 2)] @@ -39,6 +40,7 @@ fn manifest_fixture_generates_expected_ir( Ok(()) } +/// Preserve declaration order while interpolating command-list entries. #[rstest] fn command_list_entries_are_interpolated_in_order() -> Result<()> { let yaml = r#" @@ -70,6 +72,7 @@ fn command_list_entries_are_interpolated_in_order() -> Result<()> { Ok(()) } +/// Report an IR error when a target names no declared rule. #[rstest] fn missing_rule_fails() -> Result<()> { let manifest = manifest::from_path("tests/data/missing_rule.yml")?; @@ -83,6 +86,7 @@ fn missing_rule_fails() -> Result<()> { Ok(()) } +/// Exclude skipped conditional entries from all IR validation and graph output. #[rstest] #[case::skipped_target_duplicate_output( concat!( @@ -146,6 +150,7 @@ fn skipped_manifest_conditions_do_not_contribute_to_ir( Ok(()) } +/// Lower manifest dependencies into the implicit Ninja dependency class. #[rstest] #[case::target_deps( concat!( @@ -212,6 +217,7 @@ fn manifest_deps_populate_implicit_deps( Ok(()) } +/// Exclude manifest dependencies from recipe input interpolation. #[rstest] fn manifest_deps_do_not_contribute_to_recipe_inputs() -> Result<()> { let yaml = concat!( @@ -258,6 +264,7 @@ fn manifest_deps_do_not_contribute_to_recipe_inputs() -> Result<()> { Ok(()) } +/// Preserve distinct explicit, implicit, and order-only dependency classes. #[rstest] fn conditional_action_deps_populate_distinct_ir_classes() -> Result<()> { let manifest = manifest::from_path("tests/data/conditional_action_deps.yml")?; @@ -321,6 +328,7 @@ struct ExpectedEdge<'a> { is_phony: bool, } +/// Assert one conditional action's IR edge and dependency classes. fn assert_conditional_edge( graph: &BuildGraph, output: &str, @@ -360,6 +368,7 @@ fn assert_conditional_edge( Ok(()) } +/// Lower dependency-only entries into deduplicated phony actions. #[test] fn dependency_only_entries_lower_to_deduplicated_phony_actions() -> Result<()> { let yaml = concat!( @@ -399,6 +408,7 @@ fn dependency_only_entries_lower_to_deduplicated_phony_actions() -> Result<()> { Ok(()) } +/// Keep a target description separate from its rule's progress text. #[rstest] fn target_descriptions_do_not_replace_rule_progress_text() -> Result<()> { let yaml = concat!( @@ -438,6 +448,7 @@ enum ExpectedError { CircularDependency(Vec), } +/// Map invalid manifest structures to their expected IR generation errors. #[rstest] #[case( "tests/data/duplicate_outputs.yml", diff --git a/tests/ninja_dollar_escaping_tests.rs b/tests/ninja_dollar_escaping_tests.rs index 85d435632..4abd2422e 100644 --- a/tests/ninja_dollar_escaping_tests.rs +++ b/tests/ninja_dollar_escaping_tests.rs @@ -14,7 +14,12 @@ use netsuke::{ ninja_gen::generate, }; use rstest::rstest; -use std::{ffi::OsString, process::Command}; +use std::{ + ffi::OsString, + fmt::{self, Debug, Display, Formatter}, + path::Path, + process::Command, +}; use tempfile::TempDir; use test_support::ninja::ninja_integration_workspace; @@ -70,6 +75,15 @@ fn required_ninja_workspace() -> Result { ninja_integration_workspace().context("Ninja is required for dollar-escaping tests") } +/// Render a path with its escaped debug representation for invalid UTF-8 diagnostics. +struct DebugPath<'path>(&'path Path); + +impl Display for DebugPath<'_> { + fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { + Debug::fmt(self.0, formatter) + } +} + /// An isolated workspace containing a generated Ninja file and its output. struct NinjaWorkspace { /// Keeps the temporary directory alive while Ninja uses its contents. @@ -85,7 +99,7 @@ impl NinjaWorkspace { fn create(ninja_file: &str) -> anyhow::Result { let temporary_directory = required_ninja_workspace()?; let path = Utf8PathBuf::from_path_buf(temporary_directory.path().to_path_buf()).map_err( - |non_utf8| anyhow::anyhow!("non-UTF-8 temporary path: {}", non_utf8.display()), + |non_utf8| anyhow::anyhow!("non-UTF-8 temporary path: {}", DebugPath(&non_utf8)), )?; let directory = Dir::open_ambient_dir(&path, ambient_authority()) .with_context(|| format!("open Ninja workspace {path}"))?; diff --git a/tests/ninja_snapshot_tests.rs b/tests/ninja_snapshot_tests.rs index bd94ecaf7..6acd2b5dc 100644 --- a/tests/ninja_snapshot_tests.rs +++ b/tests/ninja_snapshot_tests.rs @@ -35,6 +35,7 @@ fn run_ok(cmd: &mut Command) -> Result { Ok(stdout) } +/// Validate that generated Ninja executes and then becomes a no-op for a touch rule. #[test] #[cfg(unix)] fn touch_manifest_ninja_validation() -> Result<()> { @@ -100,6 +101,7 @@ fn touch_manifest_ninja_validation() -> Result<()> { Ok(()) } +/// Snapshot only the targets retained after conditional foreach expansion. #[test] fn conditional_manifest_ninja_snapshot() -> Result<()> { let manifest_yaml = r#" @@ -142,6 +144,7 @@ fn conditional_manifest_ninja_snapshot() -> Result<()> { Ok(()) } +/// Snapshot the deterministic fallback selected by an unavailable command. #[test] fn command_available_manifest_ninja_snapshot() -> Result<()> { // Pin the `command_available` resolver to an empty PATH through the @@ -197,6 +200,7 @@ fn command_available_manifest_ninja_snapshot() -> Result<()> { Ok(()) } +/// Snapshot fail-fast lowering for a multi-command manifest rule. #[test] fn multi_command_manifest_ninja_snapshot() -> Result<()> { let fixture_dir = Dir::open_ambient_dir(env!("CARGO_MANIFEST_DIR"), ambient_authority()) @@ -233,6 +237,7 @@ fn multi_command_manifest_ninja_snapshot() -> Result<()> { Ok(()) } +/// Snapshot Ninja's implicit-dependency separator for manifest dependencies. #[test] fn implicit_deps_manifest_ninja_snapshot() -> Result<()> { let fixture_dir = Dir::open_ambient_dir(env!("CARGO_MANIFEST_DIR"), ambient_authority()) @@ -262,6 +267,7 @@ fn implicit_deps_manifest_ninja_snapshot() -> Result<()> { Ok(()) } +/// Snapshot dependency-only actions as native Ninja phony nodes. #[test] fn dependency_only_manifest_ninja_snapshot() -> Result<()> { let manifest_yaml = r#" diff --git a/tests/workflow_contracts/ci_lint_test.py b/tests/workflow_contracts/ci_lint_test.py index cb0aae5a1..fb2388972 100644 --- a/tests/workflow_contracts/ci_lint_test.py +++ b/tests/workflow_contracts/ci_lint_test.py @@ -231,16 +231,22 @@ def test_mdtablefix_installers_require_the_pinned_version() -> None: job_steps(workflow, job_name), "Install mdtablefix", ) - run = step.get("run") - assert isinstance(run, str), f"{job_name} must configure mdtablefix" - assert expected_guard in run, f"{job_name} must pin the expected version" - assert "mdtablefix --version" in run, ( - f"{job_name} must inspect the installed version" - ) - assert "tr -d '\\r'" in run, f"{job_name} must normalise Windows version output" - assert expected_match in run, ( - f"{job_name} must replace a missing or mismatched formatter" - ) + match step.get("run"): + case str() as run: + assert expected_guard in run, ( + f"{job_name} must pin the expected version" + ) + assert "mdtablefix --version" in run, ( + f"{job_name} must inspect the installed version" + ) + assert "tr -d '\\r'" in run, ( + f"{job_name} must normalise Windows version output" + ) + assert expected_match in run, ( + f"{job_name} must replace a missing or mismatched formatter" + ) + case _: + pytest.fail(f"{job_name} must configure mdtablefix") def test_build_job_runs_markdown_formatter_checker_tests() -> None: From a2e9cd8f6c7e2afc7e0728c680dbafc2715b40c5 Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 31 Aug 2026 12:14:20 +0200 Subject: [PATCH 34/34] Guard Markdown formatting in check-fmt Keep the Makefile contract from silently losing Markdown discovery or format-checker batching. --- tests/workflow_contracts/ci_lint_test.py | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/workflow_contracts/ci_lint_test.py b/tests/workflow_contracts/ci_lint_test.py index fb2388972..754e095f8 100644 --- a/tests/workflow_contracts/ci_lint_test.py +++ b/tests/workflow_contracts/ci_lint_test.py @@ -153,6 +153,47 @@ def test_makefile_clippy_flags_stay_workspace_wide() -> None: ) +def test_makefile_check_fmt_runs_markdown_format_checker() -> None: + """Protect Markdown formatting from being silently removed from make check-fmt.""" + makefile_lines = MAKEFILE_PATH.read_text(encoding="utf-8").splitlines() + target_index = next( + ( + index + for index, line in enumerate(makefile_lines) + if line.startswith("check-fmt:") + ), + None, + ) + assert target_index is not None, "the Makefile must define a check-fmt target" + + top_level_target = re.compile(r"^[A-Za-z0-9_.%/-]+:") + recipe_lines = [] + for line in makefile_lines[target_index + 1 :]: + if top_level_target.match(line): + break + if line.startswith("\t"): + recipe_lines.append(line) + recipe = "\n".join(recipe_lines) + expected_pipeline = ( + "@$(MD_FILES_FIND) | xargs -0 -r scripts/check-markdown-format.sh" + ) + + required_fragments = { + "$(MD_FILES_FIND)": "discover Markdown files with $(MD_FILES_FIND)", + "scripts/check-markdown-format.sh": "invoke the Markdown format checker", + "xargs -0 -r": "batch Markdown paths with NUL delimiters and skip empty input", + } + missing_fragments = [ + description + for fragment, description in required_fragments.items() + if fragment not in recipe + ] + assert not missing_fragments, "check-fmt must " + "; ".join(missing_fragments) + assert recipe.count(expected_pipeline) == 1, ( + "check-fmt must contain exactly one Markdown format checker pipeline" + ) + + def test_nextest_version_declared_once_at_workflow_scope() -> None: r"""NEXTEST_VERSION is declared once, at workflow scope.