Skip to content

Implement manifest subcommand with stdout streaming and -C dir#250

Merged
leynos merged 10 commits intomainfrom
terragon/implement-manifest-subcommand-jbwg7f
Dec 25, 2025
Merged

Implement manifest subcommand with stdout streaming and -C dir#250
leynos merged 10 commits intomainfrom
terragon/implement-manifest-subcommand-jbwg7f

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Dec 19, 2025

Summary

  • Adds a fully functional netsuke manifest subcommand that can either write the generated Ninja file to a path or stream it to stdout when using -.
  • Introduces working-directory aware path resolution for output paths via the -C/--directory option.
  • Extends the CLI, runner, and IO layers to support stdout streaming and relative path resolution, with accompanying tests and cucumber scenarios.

Changes

CLI

  • Extend CLI with Manifest subcommand:
    • Command: netsuke manifest <FILE>
    • New behavior: if <FILE> is -, stream manifest to stdout; otherwise write to the specified path.
  • Documentation: Clarified in struct doc comments that - streams to stdout.

Runner

  • Manifest handling updated in src/runner/mod.rs:
    • Generate manifest as before, but now:
      • If the target path is stdout sentinel (-), write to stdout.
      • Otherwise, resolve the output path relative to the configured directory and write to disk.
  • Introduced resolve_output_path(cli, path) to apply -C/--directory semantics for output paths.
  • Build path emission continues to resolve relative paths via resolve_output_path for --emit as well.

IO Layer

  • src/runner/process/file_io.rs:
    • Added is_stdout_path(path: &Path) -> bool sentinel checker (dash -> stdout).
    • Added write_ninja_stdout(content) to stream manifest to stdout with proper error handling and flush.

Tests

  • Unit tests:
    • Added tests for resolve_output_path to ensure output paths are properly resolved relative to the configured directory.
    • Added tests for is_stdout_path to detect - sentinel.
    • Added tests for write_ninja_stdout to verify stdout streaming behavior (including broken pipe handling).
  • Integration tests (assert_cmd and cucumber):
    • Updated tests to cover manifest streaming to stdout and -C directory resolution:
      • manifest - should stream to stdout and not create a file named -
      • manifest with -C <dir> and out.ninja writes under <dir>
    • New test cases in tests/features/manifest_subcommand.feature for stdout streaming and directory-resolved output.
    • Added tests/steps/manifest_command_steps.rs to drive the new manifest scenarios.
    • Updated cli parsing tests to recognize the dash path for manifest.

Documentation

  • docs/netsuke-cli-design-document.md
    • Updated to reflect manifest can output to stdout with - in addition to a file path.
  • docs/netsuke-design.md
    • Clarified Manifest variant to mention stdout streaming via - and updated behavior description about working directory resolution for output paths.
  • docs/roadmap.md
    • Marked the manifest subcommand implementation as done.
  • docs/users-guide.md
    • Updated user guide to mention streaming via - for manifest and how -C/--directory affects manifest output paths.

Tests and CI

  • All changes accompanied by unit tests and end-to-end tests (via cucumber) to exercise stdout streaming and -C directory resolution.
  • Tests ensure no file is created when streaming to stdout, and that relative paths are resolved correctly when -C is used.

Test Plan

  • Compile and run unit tests for path resolution and stdout path detection.
  • Run integration tests for manifest subcommand with file output (disk write).
  • Run integration tests for manifest subcommand streaming to stdout (dash path).
  • Validate output path resolution under -C/--directory for manifest and build emits.
  • Verify that stdout is correctly captured in cucumber tests and that no file is created for -.
  • Ensure error handling remains informative when manifest output path is a directory or unwritable.

How to Test Locally

  • Build and run unit tests: cargo test
  • Run CLI tests: cargo test -p netsuke
  • Run cucumber tests (if configured in your environment): cargo test -p netsuke --features cucumber

If you’d like, I can add a short section to the docs with a quick example usage of netsuke manifest - and a note on directory resolution for the -C option.

🌿 Generated by Terry


ℹ️ Tag @terragon-labs to ask questions and address PR feedback

📎 Task: https://www.terragonlabs.com/task/5549d050-83cc-4282-8107-21683f8008c2

Add ability for the `netsuke manifest` subcommand to write the generated Ninja file to stdout when given `-` as the output path. This enhancement involves:

- Updating CLI docs and help messages to describe `-` as stdout.
- Adjusting path resolution to handle the stdout sentinel.
- Introducing `write_ninja_stdout` and `is_stdout_path` utilities.
- Modifying the runner to detect `-` and stream output instead of writing a file.
- Adding comprehensive tests and Cucumber step definitions for this behavior.

This makes manifest output more flexible and suitable for piping or integration workflows, improving usability for power users.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 19, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Enable netsuke manifest - to stream the generated Ninja file to stdout; resolve manifest/emit output paths relative to the CLI -C directory; add stdout-aware file‑I/O utilities with broken‑pipe handling; route manifest output in the runner to stdout vs file; add tests and test helpers covering these behaviours.

Changes

Cohort / File(s) Summary
Documentation Updates
docs/netsuke-cli-design-document.md, docs/netsuke-design.md, docs/users-guide.md, docs/roadmap.md
Document netsuke manifest - streaming to stdout; clarify -C directory path resolution semantics; mark manifest checklist items done.
Runner & I/O Implementation
src/runner/mod.rs, src/runner/process/file_io.rs
Add resolve_output_path(); add is_stdout_path() and write_ninja_stdout() with broken‑pipe handling; route manifest output to stdout when -, otherwise write to resolved path; update build emit handling to use resolved paths.
CLI Documentation
src/cli.rs
Expand inline doc comment for Manifest command to document - behaviour.
Test Support Library
test_support/Cargo.toml, test_support/src/lib.rs, test_support/src/netsuke.rs
Add assert_cmd dependency; export netsuke test module; add NetsukeRun and run_netsuke_in() to invoke the binary and capture stdout/stderr.
Integration & Parser Tests
tests/assert_cmd_tests.rs, tests/cli_tests.rs
Add tests for manifest - streaming and -C relative resolution; add PATH/workspace helpers and a CLI parser test for manifest -.
Cucumber Feature Specs
tests/features/cli.feature, tests/features/manifest_subcommand.feature
Add scenario for manifest streaming to stdout and a full manifest subcommand feature (file write, stdout streaming, directory error).
Cucumber Step Definitions
tests/steps/manifest_command_steps.rs, tests/steps/mod.rs, tests/cucumber.rs
Add manifest_command_steps module with step helpers; extend CliWorld with command_stdout and command_stderr fields; capture and assert command output in scenarios.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as User
  participant CLI as "CLI Parser"
  participant Runner as "Runner"
  participant IO as "File I/O / Stdout"
  Note over CLI,Runner: User runs `netsuke manifest FILE` (FILE may be "-"), optional `-C` dir
  User->>CLI: invoke manifest FILE, optional -C
  CLI->>Runner: pass parsed args (FILE, -C)
  Runner->>Runner: resolve_output_path(FILE, -C)
  alt FILE == "-"
    Runner->>IO: write_ninja_stdout(ninja_content)
    IO-->>Runner: success / BrokenPipe handled
  else FILE is path
    Runner->>IO: write_ninja_file(resolved_path, ninja_content)
    IO-->>Runner: success / error
  end
  Runner-->>CLI: exit status and captured stdout/stderr
  CLI-->>User: present results
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

A dash now channels Ninja's art,
Streams of text from manifest start,
Paths follow the -C chart,
Tests and helpers play their part,
Pipes survive — the build won't part. 🥷

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title clearly and concisely describes the primary change: implementing a manifest subcommand with stdout streaming and directory handling via -C.
Description check ✅ Passed The description comprehensively explains the manifest subcommand implementation, stdout streaming, -C directory handling, and changes across CLI, runner, IO, tests, and documentation.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch terragon/implement-manifest-subcommand-jbwg7f

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Dec 19, 2025

Reviewer's Guide

Implements a fully functional netsuke manifest subcommand that can stream the generated Ninja manifest to stdout using - and resolves manifest and build --emit output paths relative to the CLI -C/--directory working directory, with supporting IO helpers, CLI parsing, documentation, and tests (unit, assert_cmd, and Cucumber).

Sequence diagram for netsuke manifest subcommand with stdout and -C handling

sequenceDiagram
    actor User
    participant Cli
    participant Runner
    participant ProcessFileIO

    User->>Cli: netsuke manifest FILE [-C DIR]
    Cli->>Runner: run(cli)
    Runner->>Runner: generate_ninja(cli)
    alt FILE is "-"
        Runner->>ProcessFileIO: is_stdout_path(file)
        ProcessFileIO-->>Runner: true
        Runner->>ProcessFileIO: write_ninja_stdout(ninja)
        ProcessFileIO-->>Runner: Result
    else FILE is regular path
        Runner->>ProcessFileIO: is_stdout_path(file)
        ProcessFileIO-->>Runner: false
        Runner->>Runner: resolve_output_path(cli, file)
        Runner->>ProcessFileIO: write_ninja_file(resolved_path, ninja)
        ProcessFileIO-->>Runner: Result
    end
    Runner-->>Cli: Ok(())
    Cli-->>User: Command completes
Loading

Sequence diagram for build --emit path resolution with -C

sequenceDiagram
    actor User
    participant Cli
    participant Runner
    participant ProcessFileIO

    User->>Cli: netsuke build --emit FILE [-C DIR]
    Cli->>Runner: run(cli)
    Runner->>Runner: handle_build(cli, args)
    Runner->>Runner: generate_ninja(cli)
    alt args.emit is Some(FILE)
        Runner->>Runner: resolve_output_path(cli, FILE)
        Runner->>ProcessFileIO: write_ninja_file(emit_path, ninja)
        ProcessFileIO-->>Runner: Result
        Runner->>Runner: build_path = emit_path
    else args.emit is None
        Runner->>ProcessFileIO: create_temp_ninja_file(ninja)
        ProcessFileIO-->>Runner: temp_file
        Runner->>Runner: build_path = temp_file
    end
    Runner-->>Cli: Ok(())
    Cli-->>User: Build completes
Loading

Class diagram for CLI, runner, and IO structures updated in this PR

classDiagram
    class Cli {
        +directory: Option_PathBuf
        +default() Cli
    }

    class Commands {
        <<enum>>
        +Build(args: BuildArgs)
        +Manifest(file: PathBuf)
        +Clean
        +Graph
    }

    class RunnerModule {
        +run(cli: Cli) Result
        +handle_build(cli: Cli, args: BuildArgs) Result
        -resolve_manifest_path(cli: Cli) Utf8PathBuf
        -resolve_output_path(cli: Cli, path: Path) PathBuf
    }

    class ProcessFileIO {
        +create_temp_ninja_file(content: NinjaContent) NamedTempFile
        +write_ninja_file(path: Path, content: NinjaContent) Result
        +write_ninja_stdout(content: NinjaContent) Result
        +is_stdout_path(path: Path) bool
    }

    class BuildArgs {
        +emit: Option_PathBuf
    }

    class NinjaContent {
        +as_str() &str
    }

    Cli --> Commands : parses
    RunnerModule --> Cli : uses
    RunnerModule --> Commands : dispatches
    RunnerModule --> ProcessFileIO : calls
    RunnerModule --> NinjaContent : produces
    RunnerModule --> BuildArgs : consumes
    ProcessFileIO --> NinjaContent : writes
Loading

File-Level Changes

Change Details Files
Route manifest output either to stdout when FILE is '-' or to a directory-aware resolved path when writing to disk, and reuse the same resolution logic for build --emit outputs.
  • Update manifest command handling to detect the stdout sentinel path and choose between stdout streaming and file output.
  • Introduce resolve_output_path to map relative output paths under the configured CLI working directory when -C/--directory is set, and use it for both manifest output and build --emit paths.
  • Switch build --emit to use the resolved path as both the write target and the path passed on to Ninja execution, ensuring correct behavior under -C.
src/runner/mod.rs
Add IO helpers for detecting the stdout sentinel and writing Ninja content to stdout with broken pipe handling, plus unit tests for these helpers.
  • Add is_stdout_path to identify the '-' sentinel as meaning stdout, avoiding accidental file creation.
  • Add write_ninja_stdout to write and flush Ninja content to stdout and treat broken pipes as success while surfacing other IO errors with context.
  • Extend file_io unit tests to cover stdout sentinel detection and stdout writing behavior using rstest.
src/runner/process/file_io.rs
Extend CLI parsing and behavioural test infrastructure to support manifest stdout streaming, -C-aware outputs, and captured stdout/stderr.
  • Document in the Commands::Manifest variant that '-' writes the manifest to stdout and add CLI parsing tests to accept '-' as the manifest file argument.
  • Extend the Cucumber CliWorld with captured stdout/stderr fields and add new step definitions for running the manifest command and asserting on output and file existence.
  • Add a dedicated manifest_subcommand.feature plus new steps module to cover writing to a file, streaming to stdout without creating a '-' file, and failure when the output path is a directory.
  • Update CLI parsing feature tests to include the manifest '-' scenario.
src/cli.rs
tests/cli_tests.rs
tests/cucumber.rs
tests/steps/mod.rs
tests/steps/manifest_command_steps.rs
tests/features/cli.feature
tests/features/manifest_subcommand.feature
Add integration tests ensuring manifest stdout behavior and directory-relative output for both manifest and build --emit flows.
  • Add assert_cmd tests that verify netsuke manifest - prints Ninja content to stdout and does not create a '-' file.
  • Add assert_cmd tests that netsuke manifest out.ninja and build --emit emitted.ninja write outputs inside the -C work directory and not in the original cwd.
  • Ensure PATH is controlled in tests (including wiring fake ninja) to avoid external environment dependencies while running manifest/build scenarios.
tests/assert_cmd_tests.rs
Document stdout streaming semantics for manifest and -C-aware path resolution for manifest and build outputs, and mark the roadmap item as complete.
  • Update the CLI design doc and users guide to describe netsuke manifest - and how -C/--directory affects manifest and --emit output paths.
  • Clarify in the design doc that -C is implemented as a real working directory change and that relative output paths for manifest and build --emit are resolved under that directory.
  • Mark the manifest subcommand roadmap item as completed, explicitly noting that it covers both disk writes and stdout streaming.
docs/netsuke-design.md
docs/netsuke-cli-design-document.md
docs/users-guide.md
docs/roadmap.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 21, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/steps/manifest_command_steps.rs

Comment on lines +129 to +140

fn file_should_not_exist(world: &mut CliWorld, name: String) -> Result<()> {
    let temp = world
        .temp
        .as_ref()
        .context("temp dir has not been initialised")?;
    ensure!(
        !temp.path().join(&name).exists(),
        "expected file {} to not exist",
        temp.path().join(&name).display()
    );
    Ok(())
}

❌ New issue: Code Duplication
The module contains 5 functions with similar structure: directory_named_exists,file_should_exist,file_should_not_exist,stderr_should_contain and 1 more functions

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 21, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/steps/manifest_command_steps.rs

Comment on file

//! Step definitions for `netsuke manifest` behavioural tests.

❌ New issue: String Heavy Function Arguments
In this module, 46.2% of all arguments to its 7 functions are strings. The threshold for string arguments is 39.0%

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

…teps

- Replace string parameters with newtype wrappers (DirectoryName, FileName, ManifestOutput, OutputFragment) for better type safety and clarity.
- Introduce Deref implementations for newtypes to simplify usage.
- Add helper functions get_temp_path, assert_output_contains, and assert_file_existence to remove code duplication.
- Update step definitions in `manifest_command_steps.rs` to use newtypes and helper functions, improving readability and maintainability.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos marked this pull request as ready for review December 22, 2025 01:37
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @leynos, you have reached your weekly rate limit of 2500000 diff characters.

Please try again later or upgrade to continue using Sourcery

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 907d300 and 59d590b.

📒 Files selected for processing (14)
  • docs/netsuke-cli-design-document.md
  • docs/netsuke-design.md
  • docs/roadmap.md
  • docs/users-guide.md
  • src/cli.rs
  • src/runner/mod.rs
  • src/runner/process/file_io.rs
  • tests/assert_cmd_tests.rs
  • tests/cli_tests.rs
  • tests/cucumber.rs
  • tests/features/cli.feature
  • tests/features/manifest_subcommand.feature
  • tests/steps/manifest_command_steps.rs
  • tests/steps/mod.rs
🧰 Additional context used
📓 Path-based instructions (5)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Clippy warnings MUST be disallowed in Rust code.
Fix any warnings emitted during Rust tests in the code itself rather than silencing them.
In Rust, extract meaningfully named helper functions when a function is too long, adhering to separation of concerns and CQRS.
In Rust, group related parameters in meaningfully named structs when a function has too many parameters.
In Rust, consider using Arc to reduce data returned when a function is returning a large error.
Every Rust module must begin with a module level (//!) comment explaining the module's purpose and utility.
Document public APIs in Rust using Rustdoc comments (///) so documentation can be generated with cargo doc.
In Rust, prefer immutable data and avoid unnecessary mut bindings.
In Rust, handle errors with the Result type instead of panicking where feasible.
Avoid unsafe code in Rust unless absolutely necessary, and document any usage clearly.
In Rust, place function attributes after doc comments.
In Rust, do not use return in single-line functions.
In Rust, use predicate functions for conditional criteria with more than two branches.
Lints in Rust must not be silenced except as a last resort.
Lint rule suppressions in Rust must be tightly scoped and include a clear reason.
In Rust, prefer expect over allow.
In Rust tests, use rstest fixtures for shared setup.
In Rust tests, replace duplicated tests with #[rstest(...)] parameterized cases.
In Rust, prefer mockall for mocks and stubs.
In Rust, use concat!() to combine long string literals rather than escaping newlines with a backslash.
In Rust, prefer single line versions of functions where appropriate (e.g., pub fn new(id: u64) -> Self { Self(id) } instead of multi-line).
Use NewTypes in Rust to model domain values and eliminate 'integer soup'. Reach for newt-hype when introducing many homogeneous wrappers; add small shims for string-backed wrappers. For path-centric wrappers implement AsRef<Path> along...

Files:

  • tests/assert_cmd_tests.rs
  • tests/cucumber.rs
  • tests/cli_tests.rs
  • src/runner/mod.rs
  • src/runner/process/file_io.rs
  • src/cli.rs
  • tests/steps/mod.rs
  • tests/steps/manifest_command_steps.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cognitive complexity of functions no more than 9.

  • Adhere to single responsibility and CQRS
  • Place function attributes after doc comments.
  • Do not use return in single-line functions.
  • Move conditionals with >2 branches into a predicate function.
  • Avoid unsafe unless absolutely necessary.
  • Every module must begin with a //! doc comment that explains the module's purpose and utility.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lints must not be silenced except as a last resort.
    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.
  • Replace duplicated tests with #[rstest(...)] parameterised cases.
  • Prefer mockall for mocks/stubs.
  • Prefer .expect() over .unwrap() in tests.
  • .expect() and .unwrap() are forbidden outside of tests. Errors must be propagated.
  • Ensure that any API or behavioural changes are reflected in the documentation in docs/
  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/
  • Files must not exceed 400 lines in length
    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such
    • For testing of functionality depending upon environment variables, dependency injection and...

Files:

  • tests/assert_cmd_tests.rs
  • tests/cucumber.rs
  • tests/cli_tests.rs
  • src/runner/mod.rs
  • src/runner/process/file_io.rs
  • src/cli.rs
  • tests/steps/mod.rs
  • tests/steps/manifest_command_steps.rs
docs/**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

docs/**/*.md: When new decisions are made, requirements change, libraries are added/removed, or architectural patterns evolve, proactively update the relevant file(s) in the docs/ directory to reflect the latest state.
Documentation must use en-GB-oxendict spelling and grammar ("-ize" / "-yse" / "-our"), with exception for the LICENSE filename.

docs/**/*.md: Use British English (en-GB) spelling with -ize endings (realize, organization), -lyse endings (analyse, paralyse, catalyse), -our endings (colour, behaviour), -re endings (calibre, centre, fibre), double 'l' (cancelled, counsellor), maintained 'e' (likeable, liveable), and -ogue endings (analogue, catalogue)
Use the Oxford comma in documentation: 'ships, planes, and hovercraft'
Treat company names as collective nouns in documentation: 'Concordat Industries are expanding'
Write headings in sentence case in documentation
Use Markdown headings (#, ##, ###) in order without skipping levels
Follow markdownlint recommendations for Markdown formatting
Provide code blocks and lists using standard Markdown syntax
Always provide a language identifier for fenced code blocks; use plaintext for non-code text
Use - as the first level bullet and renumber lists when items change in documentation
Prefer inline links using [text](url) or angle brackets around the URL in documentation
Ensure blank lines before and after bulleted lists and fenced blocks in documentation
Ensure tables have a delimiter line below the header row in documentation
Expand any uncommon acronym on first use in documentation (e.g., Continuous Integration (CI))
Wrap paragraphs at 80 columns in documentation
Wrap code at 120 columns in documentation
Do not wrap tables in documentation
Use GitHub-flavoured numeric footnotes referenced as [^1] in documentation
Number footnotes in order of appearance in the document
Caption every table and every diagram in documentation
Use ![alt text](path/to/image) for embedding figures with brief alt te...

Files:

  • docs/roadmap.md
  • docs/netsuke-design.md
  • docs/netsuke-cli-design-document.md
  • docs/users-guide.md
**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

**/*.md: Markdown paragraphs and bullet points must be wrapped at 80 columns.
Code blocks in Markdown must be wrapped at 120 columns.
Tables and headings in Markdown must not be wrapped.
Use dashes (-) for list bullets in Markdown.
Use GitHub-flavoured Markdown footnotes ([^1]) for references and footnotes.

Files:

  • docs/roadmap.md
  • docs/netsuke-design.md
  • docs/netsuke-cli-design-document.md
  • docs/users-guide.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/roadmap.md
  • docs/netsuke-design.md
  • docs/netsuke-cli-design-document.md
  • docs/users-guide.md
docs/**/{README,readme,GUIDE,guide,*.md}

📄 CodeRabbit inference engine (docs/adr-001-replace-serde-yml-with-serde-saphyr.md)

Document in Netsuke's README and user guide that the project now supports YAML 1.2 (not YAML 1.1) and that duplicate keys in manifests are no longer permitted

Files:

  • docs/roadmap.md
  • docs/netsuke-design.md
  • docs/netsuke-cli-design-document.md
  • docs/users-guide.md
docs/**/!(README).md

📄 CodeRabbit inference engine (docs/documentation-style-guide.md)

Avoid first and second person personal pronouns outside the README.md file in documentation

Files:

  • docs/roadmap.md
  • docs/netsuke-design.md
  • docs/netsuke-cli-design-document.md
  • docs/users-guide.md
🧬 Code graph analysis (2)
tests/assert_cmd_tests.rs (2)
src/ninja_gen.rs (1)
  • join (155-157)
test_support/src/lib.rs (1)
  • fake_ninja (95-127)
src/runner/mod.rs (2)
src/runner/process/file_io.rs (3)
  • is_stdout_path (16-18)
  • write_ninja_stdout (91-100)
  • write_ninja_file (82-89)
src/cli.rs (1)
  • default (121-134)
🪛 LanguageTool
docs/users-guide.md

[typographical] ~495-~495: The word ‘When’ starts a question. Add a question mark (“?”) at the end of the sentence.
Context: ...--emitpaths are resolved under

. - manifest : Generates the bu...

(WRB_QUESTION_MARK)


[uncategorized] ~497-~497: Loose punctuation mark.
Context: ...lved under <DIR>. - manifest <FILE>: Generates the build.ninja file and wr...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~499-~499: Possible missing comma found.
Context: ...e generated Ninja file to stdout (for example netsuke manifest - | sed ...). When `...

(AI_HYDRA_LEO_MISSING_COMMA)


[style] ~499-~499: Consider using the typographical ellipsis character here instead.
Context: ...ted Ninja file to stdout (for example netsuke manifest - | sed ...). When -C/--directory is set, rela...

(ELLIPSIS)


[typographical] ~500-~500: The word ‘When’ starts a question. Add a question mark (“?”) at the end of the sentence.
Context: ... output paths are resolved under <DIR>. - clean: Removes build artefacts by ...

(WRB_QUESTION_MARK)

⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: release / build-windows (aarch64-pc-windows-msvc, arm64, arm64, windows-aarch64) / Build windows artefacts
  • GitHub Check: release / build-windows (x86_64-pc-windows-msvc, amd64, x64, windows-x86_64) / Build windows artefacts
  • GitHub Check: release / build-macos (x86_64-apple-darwin, macos-15-intel, macos-x86_64, macos-x86_64) / Build macos artefacts
  • GitHub Check: release / build-linux (aarch64-unknown-linux-gnu, linux-arm64, arm64) / Build linux artefacts
🔇 Additional comments (26)
docs/roadmap.md (1)

190-192: LGTM!

The roadmap accurately reflects the completion of the manifest subcommand implementation, including both disk and stdout output modes.

docs/users-guide.md (2)

494-495: LGTM!

The documentation clearly explains that relative --emit paths are resolved under the directory specified by -C/--directory.


497-500: LGTM!

The documentation clearly describes both stdout streaming via - and the -C directory resolution behaviour for the manifest subcommand. The ellipsis in the code example is appropriate to indicate piping to further commands.

tests/cli_tests.rs (1)

82-88: LGTM!

The test case correctly verifies that the CLI parser accepts - as the file argument for the manifest subcommand, following the established rstest pattern.

src/cli.rs (1)

163-164: LGTM!

The documentation clearly indicates that - can be used to stream output to stdout, providing users with a concise usage hint.

tests/steps/mod.rs (1)

10-10: LGTM!

The new module declaration correctly integrates the manifest command step definitions into the test suite.

tests/features/cli.feature (1)

39-43: LGTM!

The scenario comprehensively verifies that the CLI parser correctly handles the stdout streaming syntax for the manifest subcommand.

tests/features/manifest_subcommand.feature (1)

1-22: LGTM!

The feature specification provides comprehensive coverage of the manifest subcommand, including the critical verification that streaming to stdout doesn't create a file named - and that directory collision produces an appropriate error.

src/runner/process/file_io.rs (4)

14-18: LGTM!

The function correctly identifies the stdout sentinel path. The implementation is simple and the #[must_use] attribute ensures callers don't ignore the result.


91-100: LGTM!

The function correctly handles stdout writing with appropriate BrokenPipe handling (for piping to commands like head) and ensures the output is flushed. The stdout lock prevents interleaving with other output.


8-8: LGTM!

The std::io import is required for the new stdout writing functionality.


161-172: LGTM!

The parameterised test cases provide excellent coverage, including the important edge case that ./- is not treated as stdout, preventing potential path confusion.

docs/netsuke-cli-design-document.md (1)

940-947: Documentation accurately reflects stdout streaming behavior.

The updated text clearly documents that netsuke manifest - streams to stdout, whilst file paths persist the generated manifest to disk. The wording aligns with the implementation and maintains consistency with the rest of the design document.

tests/cucumber.rs (1)

40-43: LGTM: Test world state extended for CLI output capture.

The new command_stdout and command_stderr fields enable Cucumber scenarios to assert on CLI process output. Documentation is clear, visibility is appropriate, and the Option<String> type correctly handles scenarios without CLI invocation.

docs/netsuke-design.md (3)

965-968: Documentation correctly describes manifest stdout streaming.

The command description clearly states that - streams to stdout rather than persisting to disk. This aligns with the implementation and maintains consistency across documentation files.


2013-2013: Manifest stdout streaming documented in command behavior section.

The addition clearly states that - as the file argument streams to stdout. Placement in section 8.3 (Command Behaviour) is appropriate and the wording is concise.


2024-2028: Path resolution behavior clearly documented.

The explanation of -C path resolution is clear: relative output paths (for manifest and build --emit) are resolved under the specified directory whilst absolute paths remain unchanged. The reference to test coverage provides confidence in the implementation.

tests/assert_cmd_tests.rs (2)

63-91: LGTM: Test correctly verifies -C path resolution for manifest output.

The test properly exercises the -C directory option and confirms:

  • Relative output path out.ninja is resolved under the -C directory
  • Output is not written outside the -C directory

The dual assertions (positive and negative) provide strong confidence in the path resolution logic.


121-157: LGTM: Test correctly verifies -C path resolution for build --emit.

The test properly exercises the -C directory option for build --emit and confirms:

  • Relative emit path is resolved under the -C directory
  • Output is not written outside the -C directory

The use of fake_ninja appropriately isolates the test from external dependencies whilst still validating the path resolution behavior.

src/runner/mod.rs (3)

94-99: LGTM: Manifest command correctly handles stdout streaming and file output.

The implementation properly:

  • Detects stdout sentinel with is_stdout_path
  • Streams to stdout when appropriate
  • Resolves relative paths against -C directory for file output
  • Delegates I/O to process module helpers

The logic is clear and maintains separation of concerns.


141-143: Build emit path resolution consistent with manifest handling.

The code correctly applies resolve_output_path to emit paths, ensuring -C directory handling is consistent across commands. The owned PathBuf approach is appropriate given that relative paths are transformed.


306-327: LGTM: Unit tests comprehensively cover path resolution scenarios.

The parameterized test using rstest cleanly covers the three key cases:

  • No directory configured (path unchanged)
  • Directory configured with relative path (path joined)
  • Directory configured with absolute path (absolute path unchanged)

Test structure is clear and assertions are straightforward.

tests/steps/manifest_command_steps.rs (4)

115-147: LGTM: Helper functions effectively reduce duplication in step definitions.

The three helpers (get_temp_path, assert_output_contains, assert_file_existence) extract common patterns from the step functions, improving maintainability. Each returns Result with contextual error messages that will aid debugging when tests fail.


149-161: Given step correctly initialises workspace and resets state.

The step properly:

  • Creates an isolated temporary directory
  • Copies the minimal manifest fixture
  • Resets all scenario state including the new command_stdout and command_stderr fields

Explicit state reset at line 156-159 prevents test pollution between scenarios.


163-171: Given step correctly creates named directories in the workspace.

The step properly extracts the directory name via the DirectoryName newtype and creates it within the temporary workspace. Error context clearly identifies the directory path being created.


173-219: LGTM: Step functions correctly implement Cucumber test scenarios.

The when step (lines 173-195) properly:

  • Invokes the netsuke binary via assert_cmd
  • Captures stdout, stderr, and exit status
  • Stores all outputs in world state for subsequent assertions

The then steps (lines 197-219) cleanly delegate to the helper functions introduced earlier, maintaining consistency and readability. The destructuring pattern let TypeName(inner) = param; followed by helper(world, inner.as_str()) is uniform across all assertions.

Comment thread src/runner/mod.rs
Comment thread tests/steps/manifest_command_steps.rs Outdated
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 22, 2025

@sourcery-ai review

sourcery-ai[bot]

This comment was marked as resolved.

- Added handling of broken pipe errors when writing ninja manifest to stdout in `write_ninja_stdout` to avoid failure on broken pipe.
- Refactored writing and flushing logic to tolerate broken pipe errors.
- Updated tests to cover manifest streaming with working directory changes (-C).
- Improved path handling and code cleanup in runner module.
- Upgraded derive_more crate and simplified string wrapper types in test steps.
- Updated windows-sys and derive_more dependencies.

This improves robustness of manifest command output and adds tests for streaming output with changed directories.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 23, 2025

@coderabbitai Have the following now been resolved?

Please address the comments from this code review:

## Overall Comments
- The custom `DirectoryName`, `FileName`, `ManifestOutput`, and `OutputFragment` wrapper types in `manifest_command_steps.rs` look unnecessary since `String` already implements `FromStr`; you can simplify the step definitions by accepting `String` directly and drop this boilerplate.
- In `manifest_command_steps.rs`, the logic for capturing and storing `command_stdout`/`command_stderr` inside `run_manifest_subcommand` is likely reusable by other command steps; consider extracting a small helper on `CliWorld` (or a shared function) to centralize process execution and output capture.

## Individual Comments

### Comment 1
<location> `src/runner/process/file_io.rs:91-98` </location>
<code_context>
     Ok(())
 }

+pub fn write_ninja_stdout(content: &NinjaContent) -> AnyResult<()> {
+    let mut stdout = io::stdout().lock();
+    match stdout.write_all(content.as_str().as_bytes()) {
+        Ok(()) => {}
+        Err(err) if err.kind() == io::ErrorKind::BrokenPipe => return Ok(()),
+        Err(err) => return Err(err).context("failed to write Ninja manifest to stdout"),
+    }
+    stdout.flush().context("failed to flush stdout")?;
+    Ok(())
+}
</code_context>

<issue_to_address>
**issue (bug_risk):** Broken pipe is ignored on write but not on flush, which can lead to inconsistent behavior on closed pipes.

`write_all` intentionally treats `BrokenPipe` as success, but `stdout.flush()` will still propagate `BrokenPipe`. In scenarios like piping to `head`, this can cause spurious failures even though the earlier `BrokenPipe` was ignored. Please handle `BrokenPipe` consistently on `flush` as well (e.g., match on the error kind and return `Ok(())`).
</issue_to_address>

### Comment 2
<location> `tests/assert_cmd_tests.rs:34-43` </location>
<code_context>
+    use rstest::rstest;
     use std::io::{Read, Seek, SeekFrom};

     #[test]
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding a manifest `-` test that also uses `-C` to cover that edge case

That would ensure `manifest -` still streams to stdout when `-C` is used, and that no file named `-` is created either in the current directory or under the `-C` directory. A small variant of the existing test would explicitly cover this interaction between `-` and `-C`.

Suggested implementation:

```rust
    Ok(())
}

#[test]
fn manifest_subcommand_streams_to_stdout_when_dash_with_chdir() -> Result<()> {
    let workdir = tempdir().context("create temp dir for manifest stdout -C test workdir")?;
    let chdir = tempdir().context("create temp dir for manifest stdout -C test chdir")?;

    let netsukefile = chdir.path().join("Netsukefile");
    fs::copy("tests/data/minimal.yml", &netsukefile)
        .with_context(|| format!("copy manifest to {}", netsukefile.display()))?;

    let mut cmd = Command::cargo_bin("netsuke").context("locate netsuke binary")?;
    let output = cmd
        // Intentionally use a different working directory than the one we pass via -C
        .current_dir(workdir.path())
        .env("PATH", "")
        .arg("-C")
        .arg(chdir.path())
        .arg("manifest")
        .arg("-")
        .output()
        .context("run netsuke manifest - with -C")?;

    assert!(
        output.status.success(),
        "netsuke manifest - with -C did not succeed: status={:?}, stderr={}",
        output.status,
        String::from_utf8_lossy(&output.stderr),
    );
    assert!(
        !output.stdout.is_empty(),
        "netsuke manifest - with -C did not write anything to stdout"
    );

    // Ensure no file literally named "-" was created in either directory
    assert!(
        !workdir.path().join("-").exists(),
        "a file named '-' was unexpectedly created in the working directory"
    );
    assert!(
        !chdir.path().join("-").exists(),
        "a file named '-' was unexpectedly created in the -C directory"
    );

    Ok(())
}

#[test]

```

This implementation assumes:
1. `tempdir`, `Command`, `fs`, and `Result` are already imported in `tests/assert_cmd_tests.rs` (they are used by the existing `manifest_subcommand_streams_to_stdout_when_dash` test).
2. The CLI syntax for using `-C` is `netsuke -C <dir> manifest -`. If the actual syntax places `-C` elsewhere, adjust the `.arg(...)` sequence accordingly.
3. If there is a preferred assertion style (e.g., using `predicates` or custom helpers) elsewhere in the file, you may want to align the new assertions with that style.
</issue_to_address>

### Comment 3
<location> `src/runner/process/file_io.rs:93` </location>
<code_context>

+pub fn write_ninja_stdout(content: &NinjaContent) -> AnyResult<()> {
+    let mut stdout = io::stdout().lock();
+    match stdout.write_all(content.as_str().as_bytes()) {
+        Ok(()) => {}
+        Err(err) if err.kind() == io::ErrorKind::BrokenPipe => return Ok(()),
</code_context>

<issue_to_address>
**suggestion (review_instructions):** This `match` has three branches and should be refactored so the multi-branch condition is handled via a predicate function, per the review guideline.

The guideline asks that conditionals with more than two branches be moved into a predicate function. Here the `match` over `stdout.write_all` has three branches (success, broken pipe, other errors). Consider extracting the branch logic into a small helper, e.g. a predicate or dedicated function that encapsulates the broken-pipe vs other-error distinction, so the main function doesn’t directly embed the 3-way conditional.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.rs`

**Instructions:**
Move conditionals with >2 branches into a predicate function.

</details>
</issue_to_address>

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 23, 2025

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 23, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

This comment was marked as resolved.

@coderabbitai coderabbitai Bot added the codex label Dec 23, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/assert_cmd_tests.rs (1)

138-142: Extract PATH construction into a helper function.

The PATH construction logic is duplicated between build_with_emit_writes_file and build_with_emit_resolves_output_relative_to_directory. Extract this into a helper.

🔎 Suggested refactor

Add a helper function at module level:

fn path_with_fake_ninja(ninja_dir: &tempfile::TempDir) -> Result<std::ffi::OsString> {
    let original = std::env::var_os("PATH").unwrap_or_default();
    std::env::join_paths(
        std::iter::once(ninja_dir.path().to_path_buf())
            .chain(std::env::split_paths(&original)),
    )
    .context("construct PATH with fake ninja")
}

Then update both tests:

-    let original = std::env::var_os("PATH").unwrap_or_default();
-    let path = std::env::join_paths(
-        std::iter::once(ninja_dir.path().to_path_buf()).chain(std::env::split_paths(&original)),
-    )
-    .context("construct PATH with fake ninja")?;
+    let path = path_with_fake_ninja(&ninja_dir)?;

Also applies to: 168-172

📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59d590b and 237ffdc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • Cargo.toml
  • src/runner/mod.rs
  • src/runner/process/file_io.rs
  • tests/assert_cmd_tests.rs
  • tests/steps/manifest_command_steps.rs
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Clippy warnings MUST be disallowed in Rust code.
Fix any warnings emitted during Rust tests in the code itself rather than silencing them.
In Rust, extract meaningfully named helper functions when a function is too long, adhering to separation of concerns and CQRS.
In Rust, group related parameters in meaningfully named structs when a function has too many parameters.
In Rust, consider using Arc to reduce data returned when a function is returning a large error.
Every Rust module must begin with a module level (//!) comment explaining the module's purpose and utility.
Document public APIs in Rust using Rustdoc comments (///) so documentation can be generated with cargo doc.
In Rust, prefer immutable data and avoid unnecessary mut bindings.
In Rust, handle errors with the Result type instead of panicking where feasible.
Avoid unsafe code in Rust unless absolutely necessary, and document any usage clearly.
In Rust, place function attributes after doc comments.
In Rust, do not use return in single-line functions.
In Rust, use predicate functions for conditional criteria with more than two branches.
Lints in Rust must not be silenced except as a last resort.
Lint rule suppressions in Rust must be tightly scoped and include a clear reason.
In Rust, prefer expect over allow.
In Rust tests, use rstest fixtures for shared setup.
In Rust tests, replace duplicated tests with #[rstest(...)] parameterized cases.
In Rust, prefer mockall for mocks and stubs.
In Rust, use concat!() to combine long string literals rather than escaping newlines with a backslash.
In Rust, prefer single line versions of functions where appropriate (e.g., pub fn new(id: u64) -> Self { Self(id) } instead of multi-line).
Use NewTypes in Rust to model domain values and eliminate 'integer soup'. Reach for newt-hype when introducing many homogeneous wrappers; add small shims for string-backed wrappers. For path-centric wrappers implement AsRef<Path> along...

Files:

  • src/runner/process/file_io.rs
  • tests/steps/manifest_command_steps.rs
  • src/runner/mod.rs
  • tests/assert_cmd_tests.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cognitive complexity of functions no more than 9.

  • Adhere to single responsibility and CQRS
  • Place function attributes after doc comments.
  • Do not use return in single-line functions.
  • Move conditionals with >2 branches into a predicate function.
  • Avoid unsafe unless absolutely necessary.
  • Every module must begin with a //! doc comment that explains the module's purpose and utility.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lints must not be silenced except as a last resort.
    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.
  • Replace duplicated tests with #[rstest(...)] parameterised cases.
  • Prefer mockall for mocks/stubs.
  • Prefer .expect() over .unwrap() in tests.
  • .expect() and .unwrap() are forbidden outside of tests. Errors must be propagated.
  • Ensure that any API or behavioural changes are reflected in the documentation in docs/
  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/
  • Files must not exceed 400 lines in length
    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such
    • For testing of functionality depending upon environment variables, dependency injection and...

Files:

  • src/runner/process/file_io.rs
  • tests/steps/manifest_command_steps.rs
  • src/runner/mod.rs
  • tests/assert_cmd_tests.rs
Cargo.toml

📄 CodeRabbit inference engine (AGENTS.md)

Cargo.toml: In Rust Cargo.toml, use explicit version ranges and keep dependencies up-to-date.
All crate versions specified in Cargo.toml must use SemVer-compatible caret requirements (e.g., some-crate = "1.2.3") to allow safe non-breaking updates.
The use of wildcard (*) or open-ended inequality (>=) version requirements in Cargo.toml is strictly forbidden.
Tilde requirements (~) in Cargo.toml should only be used where a dependency must be locked to patch-level updates for a specific, documented reason.

Files:

  • Cargo.toml
🧬 Code graph analysis (2)
src/runner/mod.rs (2)
src/runner/process/file_io.rs (3)
  • is_stdout_path (16-18)
  • write_ninja_stdout (111-117)
  • write_ninja_file (82-89)
src/cli.rs (1)
  • default (121-134)
tests/assert_cmd_tests.rs (1)
test_support/src/lib.rs (1)
  • fake_ninja (95-127)
🔍 Remote MCP Deepwiki

Based on the context retrieved from the Netsuke repository, here is the additional relevant information for reviewing this PR:

Context on Manifest vs. Emit Functionality

The manifest subcommand is a standalone command for writing the Ninja manifest to a specified file without invoking Ninja, while emit functionality is an option within the build command that allows you to retain the generated Ninja file at a specified path while still proceeding with the build process. Both the manifest subcommand and the emit functionality utilize the common runner::generate_ninja function to produce the Ninja build file, which aligns with the PR's approach of using the same path resolution logic (resolve_output_path) for both features.

Existing Patterns for Stdout and Path Handling

The codebase has established patterns that are relevant to this PR's implementation:

  1. Stdout Output Handling: The run_ninja function configures the Command to pipe stdout and stderr, and the spawn_and_stream_output function creates separate threads to read from its stdout and stderr pipes, writing output to the parent process's stdout and stderr respectively. This provides context for the PR's new write_ninja_stdout utility function approach.

  2. Path Resolution Patterns: The resolve_manifest_path function determines the path to the Netsukefile, taking into account a --directory CLI option which, if present, is used as the base for joining with the manifest file name. The PR's new resolve_output_path function extends this established pattern to output files.

  3. UTF-8 Path Handling: The codebase uses canonicalize_utf8_path to convert paths to their canonical, absolute representation while ensuring all paths are valid UTF-8, which is consistent with how the PR handles path operations.

Key Code Review Considerations

Based on this context, reviewers should verify that:

  • The new is_stdout_path(path: &Path) -> bool check correctly identifies the - sentinel within the existing Netsuke design patterns
  • The resolve_output_path function properly integrates with the established -C/--directory handling approach used elsewhere in the codebase (e.g., in resolve_manifest_path)
  • BrokenPipe error handling in write_ninja_stdout is consistent with how the existing codebase manages subprocess output interruptions,
🔇 Additional comments (17)
Cargo.toml (2)

148-148: Inconsistency flagged: AI summary mentions "two separate sections".

The AI summary states the dependency was "added in two separate [dev-dependencies] sections", but only one addition appears at line 148 in the main [dev-dependencies] section. The [target.'cfg(unix)'.dev-dependencies] section at line 151 shows no changes.


148-148: Remove the overengineering concern—derive_more is in active use.

The search results confirm that derive_more is imported and actively used in tests/steps/manifest_command_steps.rs. The From and Deref derives are applied to the DirectoryName and FileName newtype wrappers, which are then used throughout the test step definitions.

The version format "1" is SemVer-compliant (equivalent to ^1.x.x). The dev-dependency is necessary for this particular test code pattern.

Note: The reviewer's concern about overengineering relates to whether the newtype wrapper pattern itself is the right design approach for tests, not whether this dependency is unused bloat. If the test refactor adopts a simpler approach without newtype wrappers, revisit this dependency.

src/runner/mod.rs (4)

92-105: LGTM!

The manifest command handling correctly branches between stdout streaming and file output. The use of is_stdout_path provides clear intent, and resolve_output_path is applied only for file paths. The Cow<Path> return type from resolve_output_path is correctly consumed via .as_ref().


138-149: LGTM!

The emit path resolution now correctly applies resolve_output_path to honour the -C directory option. The Cow<Path> is directly assigned to build_path, avoiding unnecessary allocation when the path is absolute.


289-304: LGTM!

The resolve_output_path function correctly implements -C semantics: relative paths are joined with the configured directory whilst absolute paths pass through unchanged. The Cow<'a, Path> return type avoids allocation when no transformation is needed. Documentation clearly explains the behaviour.


306-328: LGTM!

The parameterised test covers the three key cases:

  1. No directory configured (path unchanged)
  2. Relative path with directory (joined)
  3. Absolute path with directory (unchanged)

Good use of rstest per coding guidelines.

src/runner/process/file_io.rs (4)

14-18: LGTM!

The is_stdout_path function correctly identifies the - sentinel by comparing the OS string directly. This is the appropriate check since - as a path should only match the literal dash, not a file named - in a subdirectory.


91-109: Well-structured BrokenPipe handling.

The is_broken_pipe predicate satisfies the coding guideline requiring predicates for conditionals with more than two branches. The helper functions write_all_ignoring_broken_pipe and flush_ignoring_broken_pipe provide consistent BrokenPipe handling for both write and flush operations, addressing the previously flagged inconsistency.


111-117: LGTM!

The write_ninja_stdout function correctly composes the helpers to write content to stdout with consistent BrokenPipe handling. The contextual error messages are informative.


178-189: Good edge-case coverage.

The test correctly verifies:

  • "-"true (stdout sentinel)
  • "out.ninja"false (normal file)
  • "./-"false (relative path to file literally named -)

The ./- case is particularly important to ensure a file named - in the current directory is not mistaken for stdout.

tests/steps/manifest_command_steps.rs (3)

16-34: Good use of derive_more to reduce boilerplate.

The cucumber_string_wrapper! macro cleanly generates the From<String> and Deref implementations via derive_more, with only FromStr requiring manual implementation (since String does not implement FromStr). This addresses the previous review suggestion effectively.


36-68: LGTM!

The helper functions cleanly extract common patterns:

  • get_temp_path: retrieves temp directory with proper error context
  • assert_output_contains: validates output fragments with clear error messages
  • assert_file_existence: checks file presence with parametrised expectation

This reduces duplication across step definitions.


118-140: LGTM!

The assertion step definitions are concise and correctly delegate to the helper functions. The newtype destructuring pattern let FileName(name) = name; is clear.

tests/assert_cmd_tests.rs (4)

34-61: LGTM!

The test correctly verifies:

  1. Command succeeds with - argument
  2. Stdout contains Ninja content markers (rule and build )
  3. No file named - is created

The enhanced content validation addresses the previous review comment.


63-91: LGTM!

The test correctly verifies that -C work causes the manifest output to be written inside the work directory rather than the current directory. Both positive and negative assertions ensure the path resolution is working correctly.


93-128: LGTM!

This test addresses the previous review suggestion to verify the interaction between -C and - (stdout streaming). It correctly ensures no file named - is created in either the working directory or the -C directory.


158-193: LGTM!

The test correctly verifies that build --emit with -C writes the emitted manifest to the -C directory rather than the current working directory. The fake ninja setup ensures the build command completes successfully.

Comment thread tests/steps/manifest_command_steps.rs
…unction

Extracted duplicated command execution logic in manifest_command_steps.rs into a helper function run_netsuke_and_record. Also introduced a helper path_with_fake_ninja in assert_cmd_tests.rs to simplify PATH setup. These changes reduce code duplication and improve test code maintainability.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 23, 2025

@coderabbitai Have the following now been resolved?

Please address the comments from this code review:

## Overall Comments
- The custom `DirectoryName`, `FileName`, `ManifestOutput`, and `OutputFragment` wrapper types in `manifest_command_steps.rs` look unnecessary since `String` already implements `FromStr`; you can simplify the step definitions by accepting `String` directly and drop this boilerplate.
- In `manifest_command_steps.rs`, the logic for capturing and storing `command_stdout`/`command_stderr` inside `run_manifest_subcommand` is likely reusable by other command steps; consider extracting a small helper on `CliWorld` (or a shared function) to centralize process execution and output capture.

## Individual Comments

### Comment 1
<location> `src/runner/process/file_io.rs:91-98` </location>
<code_context>
     Ok(())
 }

+pub fn write_ninja_stdout(content: &NinjaContent) -> AnyResult<()> {
+    let mut stdout = io::stdout().lock();
+    match stdout.write_all(content.as_str().as_bytes()) {
+        Ok(()) => {}
+        Err(err) if err.kind() == io::ErrorKind::BrokenPipe => return Ok(()),
+        Err(err) => return Err(err).context("failed to write Ninja manifest to stdout"),
+    }
+    stdout.flush().context("failed to flush stdout")?;
+    Ok(())
+}
</code_context>

<issue_to_address>
**issue (bug_risk):** Broken pipe is ignored on write but not on flush, which can lead to inconsistent behavior on closed pipes.

`write_all` intentionally treats `BrokenPipe` as success, but `stdout.flush()` will still propagate `BrokenPipe`. In scenarios like piping to `head`, this can cause spurious failures even though the earlier `BrokenPipe` was ignored. Please handle `BrokenPipe` consistently on `flush` as well (e.g., match on the error kind and return `Ok(())`).
</issue_to_address>

### Comment 2
<location> `tests/assert_cmd_tests.rs:34-43` </location>
<code_context>
+    use rstest::rstest;
     use std::io::{Read, Seek, SeekFrom};

     #[test]
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding a manifest `-` test that also uses `-C` to cover that edge case

That would ensure `manifest -` still streams to stdout when `-C` is used, and that no file named `-` is created either in the current directory or under the `-C` directory. A small variant of the existing test would explicitly cover this interaction between `-` and `-C`.

Suggested implementation:

```rust
    Ok(())
}

#[test]
fn manifest_subcommand_streams_to_stdout_when_dash_with_chdir() -> Result<()> {
    let workdir = tempdir().context("create temp dir for manifest stdout -C test workdir")?;
    let chdir = tempdir().context("create temp dir for manifest stdout -C test chdir")?;

    let netsukefile = chdir.path().join("Netsukefile");
    fs::copy("tests/data/minimal.yml", &netsukefile)
        .with_context(|| format!("copy manifest to {}", netsukefile.display()))?;

    let mut cmd = Command::cargo_bin("netsuke").context("locate netsuke binary")?;
    let output = cmd
        // Intentionally use a different working directory than the one we pass via -C
        .current_dir(workdir.path())
        .env("PATH", "")
        .arg("-C")
        .arg(chdir.path())
        .arg("manifest")
        .arg("-")
        .output()
        .context("run netsuke manifest - with -C")?;

    assert!(
        output.status.success(),
        "netsuke manifest - with -C did not succeed: status={:?}, stderr={}",
        output.status,
        String::from_utf8_lossy(&output.stderr),
    );
    assert!(
        !output.stdout.is_empty(),
        "netsuke manifest - with -C did not write anything to stdout"
    );

    // Ensure no file literally named "-" was created in either directory
    assert!(
        !workdir.path().join("-").exists(),
        "a file named '-' was unexpectedly created in the working directory"
    );
    assert!(
        !chdir.path().join("-").exists(),
        "a file named '-' was unexpectedly created in the -C directory"
    );

    Ok(())
}

#[test]

```

This implementation assumes:
1. `tempdir`, `Command`, `fs`, and `Result` are already imported in `tests/assert_cmd_tests.rs` (they are used by the existing `manifest_subcommand_streams_to_stdout_when_dash` test).
2. The CLI syntax for using `-C` is `netsuke -C <dir> manifest -`. If the actual syntax places `-C` elsewhere, adjust the `.arg(...)` sequence accordingly.
3. If there is a preferred assertion style (e.g., using `predicates` or custom helpers) elsewhere in the file, you may want to align the new assertions with that style.
</issue_to_address>

### Comment 3
<location> `src/runner/process/file_io.rs:93` </location>
<code_context>

+pub fn write_ninja_stdout(content: &NinjaContent) -> AnyResult<()> {
+    let mut stdout = io::stdout().lock();
+    match stdout.write_all(content.as_str().as_bytes()) {
+        Ok(()) => {}
+        Err(err) if err.kind() == io::ErrorKind::BrokenPipe => return Ok(()),
</code_context>

<issue_to_address>
**suggestion (review_instructions):** This `match` has three branches and should be refactored so the multi-branch condition is handled via a predicate function, per the review guideline.

The guideline asks that conditionals with more than two branches be moved into a predicate function. Here the `match` over `stdout.write_all` has three branches (success, broken pipe, other errors). Consider extracting the branch logic into a small helper, e.g. a predicate or dedicated function that encapsulates the broken-pipe vs other-error distinction, so the main function doesn’t directly embed the 3-way conditional.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.rs`

**Instructions:**
Move conditionals with >2 branches into a predicate function.

</details>
</issue_to_address>

@coderabbitai

This comment was marked as resolved.

- Added new test_support/netsuke.rs module for invoking netsuke binary with assert_cmd
- Replaced direct assert_cmd usage in manifest_command_steps.rs with test_support netsuke helpers
- Simplified step definitions by using plain String instead of wrapper newtypes
- Updated Cargo.toml files to add assert_cmd dependency to test_support workspace
- Cleaned up Cargo.lock to remove derive_more dependency

This refactor improves test code reuse and clarity by centralizing netsuke command invocation logic.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 24, 2025

@coderabbitai Have the following now been resolved?

❌ Outstanding Issues (2/5)
Overall Comment 1: Custom wrapper types

NOT RESOLVED - The wrapper types (DirectoryName, FileName, ManifestOutput, OutputFragment) remain in tests/steps/manifest_command_steps.rs, though they've been simplified using a cucumber_string_wrapper! macro.

Recommendation: Since cucumber can work directly with String parameters (which already implement FromStr), you can remove these wrappers entirely and accept String directly in the step functions.
Overall Comment 2: Reusable command execution helper

PARTIALLY RESOLVED - A helper function run_netsuke_and_record now exists and captures stdout/stderr, but it's private to the manifest_command_steps module.

Recommendation: To make it reusable by other command steps, either:

Add it as a method on CliWorld struct in tests/cucumber.rs, or
Create a shared test utilities module and make it public

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 24, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/steps/manifest_command_steps.rs

Comment on file

//! Step definitions for `netsuke manifest` behavioural tests.

❌ New issue: String Heavy Function Arguments
In this module, 45.8% of all arguments to its 11 functions are strings. The threshold for string arguments is 39.0%

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

…larity

- Introduced CommandOutput struct to encapsulate command output data.
- Simplified function signatures by using &str instead of Option<&String> and avoiding unnecessary boxing.
- Added clippy expect annotations to allow owned String args required by cucumber steps.
- Cleaned up passing of string references and ownership for better readability and efficiency.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 24, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

tests/steps/manifest_command_steps.rs

Comment on file

//! Step definitions for `netsuke manifest` behavioural tests.

❌ New issue: String Heavy Function Arguments
In this module, 39.1% of all arguments to its 11 functions are strings. The threshold for string arguments is 39.0%

@coderabbitai

This comment was marked as resolved.

- Introduced OutputType enum to represent stdout and stderr output streams
- Updated assert_output_contains to accept OutputType instead of string
- Improved error messages to use OutputType formatting
- Updated test step definitions to use new enum for clarity and maintainability

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 24, 2025

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

Code Duplication

tests/assert_cmd_tests.rs:

What lead to degradation?

The module contains 4 functions with similar structure: build_with_emit_resolves_output_relative_to_directory,build_with_emit_writes_file,manifest_subcommand_resolves_output_relative_to_directory,manifest_subcommand_writes_file

Why does this problem occur?

Duplicated code often leads to code that's harder to change since the same logical change has to be done in multiple functions. More duplication gives lower code health.

How to fix it?

A certain degree of duplicated code might be acceptable. The problems start when it is the same behavior that is duplicated across the functions in the module, ie. a violation of the Don't Repeat Yourself (DRY) principle. DRY violations lead to code that is changed together in predictable patterns, which is both expensive and risky. DRY violations can be identified using CodeScene's X-Ray analysis to detect clusters of change coupled functions with high code similarity. Read More
Once you have identified the similarities across functions, look to extract and encapsulate the concept that varies into its own function(s). These shared abstractions can then be re-used, which minimizes the amount of duplication and simplifies change.

@coderabbitai

This comment was marked as resolved.

Refactored the assert_cmd tests by introducing helper functions to
set up test workspaces and create the command instances. This removes
repeated code for temp directory setup, manifest copying, and command
creation, making tests clearer and more maintainable.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/assert_cmd_tests.rs (1)

47-192: Consider rstest parameterization to reduce test duplication.

Several test pairs follow similar patterns:

  • Lines 48-58 and 150-166: write file tests for manifest vs build --emit
  • Lines 89-110 and 168-192: relative path resolution tests for manifest vs build --emit

Evaluate whether parameterizing these with #[rstest] would improve maintainability without sacrificing clarity. Based on coding guidelines preference for parameterized cases.

Example parameterization approach
#[rstest]
#[case("manifest", &["manifest", "output.ninja"], "output.ninja", false)]
#[case("build_emit", &["build", "--emit", "output.ninja"], "output.ninja", true)]
fn subcommand_writes_file(
    #[case] name: &str,
    #[case] args: &[&str],
    #[case] output_file: &str,
    #[case] needs_ninja: bool,
) -> Result<()> {
    let temp = setup_simple_workspace(&format!("{} test", name))?;
    let output = temp.path().join(output_file);
    
    let path = if needs_ninja {
        let (ninja_dir, _) = fake_ninja(0u8)?;
        path_with_fake_ninja(&ninja_dir)?
    } else {
        OsString::from("")
    };
    
    let mut cmd = create_netsuke_command(temp.path(), path)?;
    cmd.args(args).assert().success();
    ensure!(output.exists(), "{} should create output file", name);
    Ok(())
}

Only proceed if this improves rather than obscures test intent.

📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a8b7c61 and 2fa1753.

📒 Files selected for processing (1)
  • tests/assert_cmd_tests.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Clippy warnings MUST be disallowed in Rust code.
Fix any warnings emitted during Rust tests in the code itself rather than silencing them.
In Rust, extract meaningfully named helper functions when a function is too long, adhering to separation of concerns and CQRS.
In Rust, group related parameters in meaningfully named structs when a function has too many parameters.
In Rust, consider using Arc to reduce data returned when a function is returning a large error.
Every Rust module must begin with a module level (//!) comment explaining the module's purpose and utility.
Document public APIs in Rust using Rustdoc comments (///) so documentation can be generated with cargo doc.
In Rust, prefer immutable data and avoid unnecessary mut bindings.
In Rust, handle errors with the Result type instead of panicking where feasible.
Avoid unsafe code in Rust unless absolutely necessary, and document any usage clearly.
In Rust, place function attributes after doc comments.
In Rust, do not use return in single-line functions.
In Rust, use predicate functions for conditional criteria with more than two branches.
Lints in Rust must not be silenced except as a last resort.
Lint rule suppressions in Rust must be tightly scoped and include a clear reason.
In Rust, prefer expect over allow.
In Rust tests, use rstest fixtures for shared setup.
In Rust tests, replace duplicated tests with #[rstest(...)] parameterized cases.
In Rust, prefer mockall for mocks and stubs.
In Rust, use concat!() to combine long string literals rather than escaping newlines with a backslash.
In Rust, prefer single line versions of functions where appropriate (e.g., pub fn new(id: u64) -> Self { Self(id) } instead of multi-line).
Use NewTypes in Rust to model domain values and eliminate 'integer soup'. Reach for newt-hype when introducing many homogeneous wrappers; add small shims for string-backed wrappers. For path-centric wrappers implement AsRef<Path> along...

Files:

  • tests/assert_cmd_tests.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cognitive complexity of functions no more than 9.

  • Adhere to single responsibility and CQRS
  • Place function attributes after doc comments.
  • Do not use return in single-line functions.
  • Move conditionals with >2 branches into a predicate function.
  • Avoid unsafe unless absolutely necessary.
  • Every module must begin with a //! doc comment that explains the module's purpose and utility.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lints must not be silenced except as a last resort.
    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.
  • Replace duplicated tests with #[rstest(...)] parameterised cases.
  • Prefer mockall for mocks/stubs.
  • Prefer .expect() over .unwrap() in tests.
  • .expect() and .unwrap() are forbidden outside of tests. Errors must be propagated.
  • Ensure that any API or behavioural changes are reflected in the documentation in docs/
  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/
  • Files must not exceed 400 lines in length
    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such
    • For testing of functionality depending upon environment variables, dependency injection and...

Files:

  • tests/assert_cmd_tests.rs
🧬 Code graph analysis (1)
tests/assert_cmd_tests.rs (1)
test_support/src/lib.rs (1)
  • fake_ninja (96-128)
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: release / build-windows (x86_64-pc-windows-msvc, amd64, x64, windows-x86_64) / Build windows artefacts
  • GitHub Check: release / build-macos (x86_64-apple-darwin, macos-15-intel, macos-x86_64, macos-x86_64) / Build macos artefacts
  • GitHub Check: release / build-windows (aarch64-pc-windows-msvc, arm64, arm64, windows-aarch64) / Build windows artefacts
  • GitHub Check: Sourcery review
  • GitHub Check: build-test (ubuntu-latest, stable)

Comment thread tests/assert_cmd_tests.rs
Comment thread tests/assert_cmd_tests.rs
Comment thread tests/assert_cmd_tests.rs
…ed setup

- Upgrade assert_cmd from 2.0.17 to 2.1.1 and remove doc-comment dependency.
- Use explicit netsuke executable path resolution instead of cargo_bin helper.
- Refactor tests to use parameterized rstest for subcommands writing files and relative output paths.
- Add helper functions to setup workspaces and manage PATH for fake ninja binaries.
- Simplify test assertions and improve test coverage on manifest and build --emit subcommands.
- Consolidate redundant tests and improve test clarity and maintainability.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Dec 25, 2025

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 25, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
test_support/Cargo.toml (1)

17-17: LGTM: Dependency placement and version are correct.

The version "2.0" follows implicit semver caret conventions. Placing assert_cmd in [dependencies] (not [dev-dependencies]) is appropriate because this test support crate exposes command execution utilities in its public API.

Note: This resolves the past review feedback requesting the change from "2.0.17" to "2.0".

tests/assert_cmd_tests.rs (2)

16-53: LGTM: Helper functions are well-documented and correctly implemented.

All helper functions now have clear doc comments addressing the past review feedback. Error handling uses .context() throughout, and the cargo_bin_cmd! macro is used appropriately.

Note: This resolves past review comments requesting doc comments on helpers.


224-251: LGTM: Test covers the -C with stdout streaming edge case.

This test addresses the past review feedback by explicitly verifying that manifest - combined with -C streams to stdout without creating a file named "-" in either the working directory or the -C directory.

Note: This resolves past review comments requesting coverage of the -C and - interaction.

📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2fa1753 and 9573232.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • Cargo.toml
  • test_support/Cargo.toml
  • test_support/src/netsuke.rs
  • tests/assert_cmd_tests.rs
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Clippy warnings MUST be disallowed in Rust code.
Fix any warnings emitted during Rust tests in the code itself rather than silencing them.
In Rust, extract meaningfully named helper functions when a function is too long, adhering to separation of concerns and CQRS.
In Rust, group related parameters in meaningfully named structs when a function has too many parameters.
In Rust, consider using Arc to reduce data returned when a function is returning a large error.
Every Rust module must begin with a module level (//!) comment explaining the module's purpose and utility.
Document public APIs in Rust using Rustdoc comments (///) so documentation can be generated with cargo doc.
In Rust, prefer immutable data and avoid unnecessary mut bindings.
In Rust, handle errors with the Result type instead of panicking where feasible.
Avoid unsafe code in Rust unless absolutely necessary, and document any usage clearly.
In Rust, place function attributes after doc comments.
In Rust, do not use return in single-line functions.
In Rust, use predicate functions for conditional criteria with more than two branches.
Lints in Rust must not be silenced except as a last resort.
Lint rule suppressions in Rust must be tightly scoped and include a clear reason.
In Rust, prefer expect over allow.
In Rust tests, use rstest fixtures for shared setup.
In Rust tests, replace duplicated tests with #[rstest(...)] parameterized cases.
In Rust, prefer mockall for mocks and stubs.
In Rust, use concat!() to combine long string literals rather than escaping newlines with a backslash.
In Rust, prefer single line versions of functions where appropriate (e.g., pub fn new(id: u64) -> Self { Self(id) } instead of multi-line).
Use NewTypes in Rust to model domain values and eliminate 'integer soup'. Reach for newt-hype when introducing many homogeneous wrappers; add small shims for string-backed wrappers. For path-centric wrappers implement AsRef<Path> along...

Files:

  • test_support/src/netsuke.rs
  • tests/assert_cmd_tests.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cognitive complexity of functions no more than 9.

  • Adhere to single responsibility and CQRS
  • Place function attributes after doc comments.
  • Do not use return in single-line functions.
  • Move conditionals with >2 branches into a predicate function.
  • Avoid unsafe unless absolutely necessary.
  • Every module must begin with a //! doc comment that explains the module's purpose and utility.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lints must not be silenced except as a last resort.
    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.
  • Replace duplicated tests with #[rstest(...)] parameterised cases.
  • Prefer mockall for mocks/stubs.
  • Prefer .expect() over .unwrap() in tests.
  • .expect() and .unwrap() are forbidden outside of tests. Errors must be propagated.
  • Ensure that any API or behavioural changes are reflected in the documentation in docs/
  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/
  • Files must not exceed 400 lines in length
    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such
    • For testing of functionality depending upon environment variables, dependency injection and...

Files:

  • test_support/src/netsuke.rs
  • tests/assert_cmd_tests.rs
Cargo.toml

📄 CodeRabbit inference engine (AGENTS.md)

Cargo.toml: In Rust Cargo.toml, use explicit version ranges and keep dependencies up-to-date.
All crate versions specified in Cargo.toml must use SemVer-compatible caret requirements (e.g., some-crate = "1.2.3") to allow safe non-breaking updates.
The use of wildcard (*) or open-ended inequality (>=) version requirements in Cargo.toml is strictly forbidden.
Tilde requirements (~) in Cargo.toml should only be used where a dependency must be locked to patch-level updates for a specific, documented reason.

Files:

  • Cargo.toml
🧬 Code graph analysis (1)
tests/assert_cmd_tests.rs (1)
test_support/src/lib.rs (1)
  • fake_ninja (96-128)
🪛 GitHub Actions: Release Dry Run
Cargo.toml

[error] 1-1: Cargo.toml parse failure. Could not read manifest. Command used: python3 .github/workflows/scripts/read_manifest.py --manifest-path "$manifest_path" name 2>/tmp/name.err

test_support/Cargo.toml

[error] 1-1: Cargo.toml parse failure. Could not read manifest. Command used: python3 .github/workflows/scripts/read_manifest.py --manifest-path "$manifest_path" name 2>/tmp/name.err

🔍 Remote MCP

Let me search the web for information about netsuke and this PR:The web search results did not return information about the netsuke project itself. Let me search more specifically:Based on the comprehensive context provided in the PR summary and AI analysis, I can now synthesize the relevant facts for the code review without needing additional web sources. The provided documentation is sufficiently detailed.

Summary of Relevant Context for PR #250 Review

Project Purpose & Architecture

The PR implements a netsuke manifest <FILE> subcommand for a Rust-based build tool that generates Ninja build manifests. Key architectural elements relevant to this review:

  1. Core Functionality: The netsuke CLI generates Ninja build files (manifest format) and can now either persist them to disk or stream them to stdout
  2. Module Structure:
    • src/cli.rs - CLI parsing and command definitions
    • src/runner/mod.rs - Command execution logic with path resolution
    • src/runner/process/file_io.rs - File I/O utilities (newly extended)
    • Test modules with integration (assert_cmd) and behavior-driven (cucumber) testing

Key Technical Changes to Review

1. Public API Additions [::Deepwiki::]

  • is_stdout_path(path: &Path) -> bool - detects "-" sentinel value
  • write_ninja_stdout(content: &NinjaContent) -> AnyResult<()> - streams to stdout with BrokenPipe handling
  • resolve_output_path(cli, path) - resolves relative paths against -C/--directory parameter

2. Behavior Changes

  • Manifest command now routes output based on file argument: "-" triggers stdout streaming, otherwise disk write
  • --emit flag uses same resolve_output_path() logic for directory-aware resolution
  • Success messaging preserved regardless of output destination
  • No file creation when streaming to stdout (important for test verification)

3. Error Handling Improvements

  • BrokenPipe errors now consistently ignored on both write() and flush() operations to stdout
  • Refactored multi-branch match into helpers (is_broken_pipe, write_all_ignoring_broken_pipe, flush_ignoring_broken_pipe)
  • Contextual error messages for path resolution failures

Test Coverage & Verification Points

Integration Tests [::Deepwiki::]

  • subcommand_writes_file - verifies disk output with fake Ninja binary support
  • manifest_subcommand_streams_to_stdout_when_dash - confirms stdout stream and no file "-" creation
  • subcommand_resolves_output_relative_to_directory - validates -C directory resolution for both manifest and --emit
  • manifest_subcommand_streams_to_stdout_with_directory - combined -C and stdout streaming

Unit Tests

  • resolve_output_path behavior with relative/absolute paths
  • is_stdout_path detection
  • write_ninja_stdout stdout handling including broken-pipe scenarios

Test Helpers Refactored

  • setup_simple_workspace() and setup_workspace_with_subdir() for test setup
  • path_with_fake_ninja() for PATH override in tests
  • Parameterized test cases via WritesFileCase and RelativeOutputCase enums

Documentation Updates

Design and user-facing documentation expanded to clarify:

  • - as stdout sentinel (e.g., netsuke manifest - | ...)
  • -C/--directory path resolution semantics for relative output paths
  • No underlying behavior changes—documentation clarifies existing intent

Code Review Risk Areas

  1. Broken-Pipe Handling - Critical for robustness. Verify both write() and flush() consistently ignore BrokenPipe
  2. Path Resolution - Ensure relative path resolution respects directory context in all code paths (manifest and emit)
  3. Test Coverage - Integration tests should confirm no file named "-" created in any scenario (including with -C)
  4. String Parameters in Tests - Codescene metrics noted high string-argument count; refactoring with CommandOutput struct and OutputType enum addresses this
🔇 Additional comments (6)
test_support/src/netsuke.rs (2)

31-40: LGTM: Struct documentation is complete and clear.

The NetsukeRun struct and its fields are properly documented with rustdoc comments. The minimal Debug derive is appropriate for a test utility type.


42-64: LGTM: Function is well-documented and correctly implemented.

The run_netsuke_in function properly documents its behaviour and errors. Clearing PATH ensures test isolation, and using from_utf8_lossy is appropriate for capturing potentially non-UTF-8 output in tests.

tests/assert_cmd_tests.rs (3)

95-116: LGTM: Parametrised test follows best practices.

The test correctly uses rstest parametrisation, helper functions, and propagates errors with ensure! rather than panicking. This addresses past review feedback.


118-139: LGTM: Test correctly verifies stdout streaming behaviour.

The test properly uses helper functions and checks both that stdout contains expected content and that no file named "-" is created. Error handling is consistent with project patterns.


188-222: LGTM: Test correctly validates -C directory resolution.

The parametrised test properly verifies that output files are resolved relative to the -C directory, checking both positive (file exists in workdir) and negative (file doesn't exist outside workdir) conditions.

test_support/Cargo.toml (1)

17-17: No action required. The manifest at test_support/Cargo.toml parses successfully. The assert_cmd = "2.0" dependency is syntactically correct TOML with proper formatting and placement in [dependencies].

Likely an incorrect or invalid review comment.

Comment thread Cargo.toml Outdated
Comment thread test_support/src/netsuke.rs
Comment thread tests/assert_cmd_tests.rs
Comment thread tests/assert_cmd_tests.rs
…nifest files

- Introduced the WritesFileCase and RelativeOutputCase enums to describe
  variants of tests related to subcommands that write manifest files.
- Added descriptive helper methods to these enums for context, CLI args,
  output filename, ninja dependency, and expected assertions.
- Created new detailed tests checking output file creation and path
  resolution with -C option.
- Added netsuke_executable helper to test_support for locating the test
  binary.
- Updated assert_cmd dependency to exact 2.0.0 version in Cargo.toml files.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title Implement manifest subcommand with stdout stream and -C path handling Implement manifest subcommand with stdout streaming and -C dir Dec 25, 2025
@leynos leynos merged commit 4f233ee into main Dec 25, 2025
14 checks passed
@leynos leynos deleted the terragon/implement-manifest-subcommand-jbwg7f branch December 25, 2025 01:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant