Skip to content

feat: extract domain logic into focused microcrates#21

Merged
EffortlessSteven merged 9 commits into
mainfrom
chore/repo-url-normalize
Feb 24, 2026
Merged

feat: extract domain logic into focused microcrates#21
EffortlessSteven merged 9 commits into
mainfrom
chore/repo-url-normalize

Conversation

@EffortlessSteven

Copy link
Copy Markdown
Member

Summary

Decomposes tightly-coupled domain logic from the core shipper crate into 7 new focused microcrates, improving testability, reusability, and compile-time parallelism. Each microcrate owns a single concern with comprehensive tests (unit, property-based, and fuzz targets).

New crates

Crate Purpose Lines
shipper-schema Schema version parsing & backward-compatibility validation for persisted state files ~100
shipper-duration Human-readable duration parsing (2s, 500ms, 1m) with bidirectional serde codecs ~100
shipper-plan Workspace plan generation — metadata loading, publishability filtering, topological sort, dependency maps ~650
shipper-policy Publish policy evaluation (Safe/Balanced/Fast) → concrete PolicyEffects flags ~180
shipper-cargo-failure Cargo publish failure classification into Retryable/Permanent/Ambiguous error classes ~180
shipper-levels Dependency-level grouping (Kahn's algorithm) for parallel publish ordering ~330
shipper-sparse-index Sparse-index path computation and JSONL version-presence lookups ~150

Core crate refactoring

  • engine.rs — Policy evaluation delegated to shipper-policy; failure classification delegated to shipper-cargo-failure
  • plan.rs — Level grouping delegated to shipper-levels
  • state.rs / store.rs — Schema version validation delegated to shipper-schema
  • types.rs — Duration serde codecs delegated to shipper-duration
  • registry.rs — Sparse-index helpers delegated to shipper-sparse-index
  • lib.rs — Re-exports shipper-cargo-failure as cargo_failure for public API continuity

Testing & fuzzing

  • Each new crate includes integration/contract tests
  • 6 new fuzz targets added: cargo_failure_classifier, duration_codec, policy_effects, release_levels, schema_version, sparse_index
  • New BDD-style publish test (bdd_publish.rs) and parallel levels integration test
  • Existing tests pass unchanged

Other changes

  • Rust toolchain updated to 1.92.0
  • CI action dtolnay/rust-toolchain bumped to 1.100.0
  • Repository URLs normalized to effortlessmetrics org
  • .gitignore updated for fuzz artifacts

Test plan

  • cargo test --workspace passes
  • cargo clippy --workspace --all-targets --all-features -- -D warnings clean
  • cargo fmt --all -- --check clean
  • Fuzz targets compile: cargo +nightly fuzz list
  • No behavioral changes to CLI — cargo run -p shipper-cli -- --help works as before

Copilot AI review requested due to automatic review settings February 24, 2026 08:21
@coderabbitai

coderabbitai Bot commented Feb 24, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features

    • Added parallel publish level grouping to enable concurrent crate releases while respecting dependencies.
    • Added index readiness mode to verify published versions via sparse index metadata.
    • Added balanced policy option for refined safety and verification trade-offs.
    • Enhanced cargo publish failure classification supporting retryable, permanent, and ambiguous scenarios.
  • Tests

    • Expanded test coverage with micro-plan feature and new fuzz targets for schema validation, policy evaluation, duration codec, failure classification, and dependency grouping.

Walkthrough

This pull request introduces seven new workspace crates to modularize shipper functionality: shipper-duration, shipper-schema, shipper-plan, shipper-policy, shipper-levels, shipper-sparse-index, and shipper-cargo-failure. Existing crates are refactored to delegate to these new modules rather than maintaining inline implementations. CI configuration is expanded to include micro-plan feature testing and new fuzz targets.

Changes

Cohort / File(s) Summary
Duration Handling
crates/shipper-duration/Cargo.toml, crates/shipper-duration/src/lib.rs, crates/shipper-duration/tests/duration_config_integration.rs
New crate providing parse_duration, deserialize_duration, and serialize_duration functions with TOML/JSON integration tests.
Schema Version Handling
crates/shipper-schema/Cargo.toml, crates/shipper-schema/src/lib.rs, crates/shipper-schema/tests/schema_contract_integration.rs
New crate for parse_schema_version and validate_schema_version with validation tests and error handling.
Policy Evaluation
crates/shipper-policy/Cargo.toml, crates/shipper-policy/src/lib.rs, crates/shipper-policy/tests/runtime_options_contract.rs
New crate introducing PolicyKind enum and PolicyEffects struct with evaluate and apply_policy functions for Safe/Balanced/Fast policies.
Cargo Failure Classification
crates/shipper-cargo-failure/Cargo.toml, crates/shipper-cargo-failure/src/lib.rs, crates/shipper-cargo-failure/tests/classifier_contract_integration.rs
New crate exposing classify_publish_failure to categorize publish errors as Retryable, Permanent, or Ambiguous with pattern matching.
Package Level Grouping
crates/shipper-levels/Cargo.toml, crates/shipper-levels/src/lib.rs
New crate implementing group_packages_by_levels function to partition packages into sequential dependency levels for parallel publishing.
Sparse Index Utilities
crates/shipper-sparse-index/Cargo.toml, crates/shipper-sparse-index/src/lib.rs, crates/shipper-sparse-index/tests/sparse_index_contract_integration.rs
New crate providing sparse_index_path and contains_version for crates.io sparse-index path computation and version detection.
Release Planning
crates/shipper-plan/Cargo.toml, crates/shipper-plan/src/lib.rs
New crate implementing build_plan to construct a publish plan from ReleaseSpec with dependency ordering and cycle detection (650 LOC).
Workspace Root Configuration
Cargo.toml
Adds seven new workspace members and updates dependency graph.
Core shipper Engine Refactoring
crates/shipper/src/engine.rs, crates/shipper/src/engine_parallel.rs, crates/shipper/src/lib.rs, crates/shipper/Cargo.toml
Replaces inline policy evaluation and cargo failure classification with calls to policy_effects and shipper_cargo_failure modules; introduces public re-export of cargo\_failure and conditional plan module loading for micro-plan feature.
Plan Module Refactoring
crates/shipper/src/plan.rs, crates/shipper/src/plan_micro.rs
Delegates package level grouping to shipper_levels::group_packages_by_levels; plan\_micro.rs re-exports shipper_plan when micro-plan feature is enabled.
Registry/State/Store Refactoring
crates/shipper/src/registry.rs, crates/shipper/src/state.rs, crates/shipper/src/store.rs
Removes inline sparse-index and schema-version logic, delegating to shipper_sparse_index and shipper_schema modules; updates test paths accordingly.
Types Module Updates
crates/shipper/src/types.rs, crates/shipper-types/Cargo.toml, crates/shipper-types/src/lib.rs
Removes humantime dependency, re-exports deserialize_duration and serialize_duration from shipper-duration, delegates level grouping to shipper-levels.
CLI Updates
crates/shipper-cli/Cargo.toml, crates/shipper-cli/src/main.rs
Replaces humantime with shipper-duration; adds micro-plan feature; updates API/index base URL defaulting and duration parsing logic.
BDD Test Suite
crates/shipper-cli/tests/bdd_publish.rs
Expands test utilities and scenarios: adds readiness-mode tests, parallel workspace grouping tests, policy validation, schema version validation, and classification tests (321 new LOC).
Integration Tests
crates/shipper/tests/parallel_levels_integration.rs, crates/shipper/tests/schema_version_integration.rs, crates/shipper-types/tests/release_levels_integration.rs
New end-to-end tests validating level-based parallelization, schema version handling, and release plan grouping.
Fuzz Testing Infrastructure
fuzz/Cargo.toml, fuzz/fuzz_targets/duration_codec.rs, fuzz/fuzz_targets/schema_version.rs, fuzz/fuzz_targets/policy_effects.rs, fuzz/fuzz_targets/cargo_failure_classifier.rs, fuzz/fuzz_targets/release_levels.rs, fuzz/fuzz_targets/sparse_index.rs
Adds six new fuzz targets for duration codec, schema parsing, policy evaluation, failure classification, level grouping, and sparse index; expands fuzz dependencies.
State/Store Dependencies
crates/shipper-state/Cargo.toml, crates/shipper-state/src/lib.rs, crates/shipper-store/Cargo.toml, crates/shipper-store/src/lib.rs
Adds shipper-schema dependency and delegates schema validation to external crate; refactors test usage of schema parsing.
Registry Dependencies
crates/shipper-registry/Cargo.toml, crates/shipper-registry/src/lib.rs
Adds shipper-sparse-index dependency; delegates sparse-index path and version checking to external module.
CI & Documentation
.github/workflows/ci.yml, docs/testing.md, features/parallel_levels.feature, features/preflight_checks.feature, features/publish_resume.feature
Expands CI matrix with micro-plan feature in BDD/fuzz jobs; updates MSRV toolchain handling; adds new fuzz targets and test scenarios for levels, readiness mode, and policy validation; documents new testing coverage.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Seven crates sprout from the garden so bright,
Duration and schema take structured flight,
Plans bloom with levels of parallel care,
Policy whispers through shipper's fresh air,
The rabbit refactors with joy and delight—
From tangled monolith to modules so right! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: extract domain logic into focused microcrates' clearly and concisely summarizes the main objective of the changeset—decomposing domain logic into new focused crates. It is specific, descriptive, and directly reflects the primary change.
Description check ✅ Passed The description comprehensively documents the PR purpose, lists all seven new crates with their purposes and line counts, details core refactoring changes, covers testing/fuzzing additions, and provides a test plan checklist. It is well-structured and directly related to the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/repo-url-normalize

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @EffortlessSteven, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors the shipper project by decomposing tightly-coupled domain logic from the main shipper crate into several focused microcrates. This architectural change aims to enhance the system's testability, reusability of components, and improve compile-time parallelism. The core shipper crate now acts as an orchestrator, delegating specialized tasks to these new, independent modules.

Highlights

  • Microcrate Extraction: Extracted tightly-coupled domain logic from the core shipper crate into 7 new focused microcrates: shipper-schema, shipper-duration, shipper-plan, shipper-policy, shipper-cargo-failure, shipper-levels, and shipper-sparse-index.
  • Core Crate Refactoring: Refactored the core shipper crate to delegate responsibilities to these new microcrates, enhancing modularity and maintainability.
  • Enhanced Testing: Introduced comprehensive testing for each new microcrate, including unit, property-based, and fuzz targets, alongside new BDD-style integration tests for publish behavior and parallel levels.
  • Toolchain and CI Updates: Updated the Rust toolchain to 1.92.0 and bumped the CI action dtolnay/rust-toolchain to 1.100.0, ensuring compatibility and leveraging newer features.
Changelog
  • Cargo.lock
    • Added new microcrate dependencies and updated existing package hashes.
  • Cargo.toml
    • Added new microcrates to the workspace members list.
  • crates/shipper-cargo-failure/Cargo.toml
    • Added new crate manifest for shipper-cargo-failure.
  • crates/shipper-cargo-failure/src/lib.rs
    • Implemented logic for classifying Cargo publish failures into retryable, permanent, or ambiguous categories.
  • crates/shipper-cargo-failure/tests/classifier_contract_integration.rs
    • Added integration tests for Cargo failure classification.
  • crates/shipper-cli/Cargo.toml
    • Updated dependencies to use shipper-duration and enabled the micro-plan feature.
  • crates/shipper-cli/src/main.rs
    • Refactored registry API/index base handling and updated duration parsing to use shipper-duration.
  • crates/shipper-cli/tests/bdd_publish.rs
    • Added new test helpers and scenarios for parallel workspaces, index readiness, policy modes, and failure classification.
  • crates/shipper-duration/Cargo.toml
    • Added new crate manifest for shipper-duration.
  • crates/shipper-duration/src/lib.rs
    • Implemented human-readable duration parsing and Serde codecs.
  • crates/shipper-duration/tests/duration_config_integration.rs
    • Added integration tests for duration parsing and serialization.
  • crates/shipper-levels/Cargo.toml
    • Added new crate manifest for shipper-levels.
  • crates/shipper-levels/src/lib.rs
    • Implemented logic for grouping packages into dependency levels for parallel processing.
  • crates/shipper-plan/Cargo.toml
    • Added new crate manifest for shipper-plan.
  • crates/shipper-plan/src/lib.rs
    • Implemented workspace planning, publishability filtering, and topological sorting.
  • crates/shipper-policy/Cargo.toml
    • Added new crate manifest for shipper-policy.
  • crates/shipper-policy/src/lib.rs
    • Implemented logic for evaluating publish policy effects.
  • crates/shipper-policy/tests/runtime_options_contract.rs
    • Added integration tests for policy evaluation with runtime options.
  • crates/shipper-registry/Cargo.toml
    • Added shipper-sparse-index as a dependency.
  • crates/shipper-registry/src/lib.rs
    • Refactored sparse index path calculation and version parsing to use shipper-sparse-index.
  • crates/shipper-schema/Cargo.toml
    • Added new crate manifest for shipper-schema.
  • crates/shipper-schema/src/lib.rs
    • Implemented schema version parsing and compatibility validation.
  • crates/shipper-schema/tests/schema_contract_integration.rs
    • Added integration tests for schema version validation.
  • crates/shipper-sparse-index/Cargo.toml
    • Added new crate manifest for shipper-sparse-index.
  • crates/shipper-sparse-index/src/lib.rs
    • Implemented Cargo sparse-index path computation and version lookup.
  • crates/shipper-sparse-index/tests/sparse_index_contract_integration.rs
    • Added integration tests for sparse index helpers.
  • crates/shipper-state/Cargo.toml
    • Added shipper-schema as a dependency.
  • crates/shipper-state/src/lib.rs
    • Refactored receipt schema version validation to use shipper-schema.
  • crates/shipper-store/Cargo.toml
    • Added shipper-schema as a dependency.
  • crates/shipper-store/src/lib.rs
    • Refactored schema version validation to use shipper-schema.
  • crates/shipper-types/Cargo.toml
    • Updated dependencies to use shipper-duration and shipper-levels.
  • crates/shipper-types/src/lib.rs
    • Replaced internal duration serialization/deserialization with shipper-duration and moved level grouping logic to shipper-levels.
  • crates/shipper-types/tests/release_levels_integration.rs
    • Added new integration test for release levels.
  • crates/shipper/Cargo.toml
    • Added new microcrates as direct dependencies and updated feature flags.
  • crates/shipper/src/engine.rs
    • Updated policy application and cargo failure classification to use shipper-policy and shipper-cargo-failure.
  • crates/shipper/src/engine_parallel.rs
    • Updated policy application to use shipper-policy.
  • crates/shipper/src/lib.rs
    • Added cargo_failure re-export and conditional compilation for the plan module.
  • crates/shipper/src/plan.rs
    • Moved group_by_levels implementation to shipper-levels.
  • crates/shipper/src/plan_micro.rs
    • Added new file to re-export shipper-plan.
  • crates/shipper/src/registry.rs
    • Updated sparse index path calculation and version parsing to use shipper-sparse-index.
  • crates/shipper/src/state.rs
    • Updated schema version validation to use shipper-schema.
  • crates/shipper/src/store.rs
    • Updated schema version validation to use shipper-schema.
  • crates/shipper/tests/parallel_levels_integration.rs
    • Added new integration test for parallel levels.
  • crates/shipper/tests/schema_version_integration.rs
    • Added new integration test for schema version validation.
  • docs/testing.md
    • Updated testing documentation to include new microcrates and fuzz targets.
  • features/parallel_levels.feature
    • Added new BDD feature for parallel publish level grouping.
  • features/preflight_checks.feature
    • Added new BDD scenario for balanced policy.
  • features/publish_resume.feature
    • Added new BDD scenarios for retryable failures and index readiness.
  • fuzz/Cargo.toml
    • Added new fuzz targets for shipper-duration, shipper-schema, shipper-policy, shipper-cargo-failure, shipper-levels, and shipper-sparse-index.
  • fuzz/fuzz_targets/cargo_failure_classifier.rs
    • Added new fuzz target for Cargo failure classification.
  • fuzz/fuzz_targets/duration_codec.rs
    • Added new fuzz target for duration parsing and Serde codecs.
  • fuzz/fuzz_targets/policy_effects.rs
    • Added new fuzz target for policy effect evaluation.
  • fuzz/fuzz_targets/release_levels.rs
    • Added new fuzz target for dependency-level grouping.
  • fuzz/fuzz_targets/schema_version.rs
    • Added new fuzz target for schema version parsing.
  • fuzz/fuzz_targets/sparse_index.rs
    • Added new fuzz target for sparse-index path and version lookup.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/ci.yml
Activity
  • The core shipper crate's domain logic has been extracted into 7 new microcrates.
  • Each new microcrate includes unit, property-based, and fuzz tests to ensure robustness.
  • New BDD-style tests were added to shipper-cli for publish behavior, parallel levels, and readiness modes.
  • The Rust toolchain was updated to 1.92.0 and the CI action dtolnay/rust-toolchain was bumped to 1.100.0.
  • Repository URLs were normalized to the effortlessmetrics organization.
  • .gitignore was updated to include fuzz artifacts.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This is an impressive and well-executed refactoring. Decomposing the core shipper logic into focused microcrates significantly improves modularity, testability, and maintainability. The new crates are well-defined, and the addition of comprehensive unit, property, and fuzz tests for each is excellent. The overall structure is much cleaner and easier to navigate. I have one minor suggestion for simplification, but otherwise, this is a fantastic change.

Comment on lines +31 to 45
pub(crate) fn policy_effects(opts: &RuntimeOptions) -> shipper_policy::PolicyEffects {
let policy = match opts.policy {
crate::types::PublishPolicy::Safe => shipper_policy::PolicyKind::Safe,
crate::types::PublishPolicy::Balanced => shipper_policy::PolicyKind::Balanced,
crate::types::PublishPolicy::Fast => shipper_policy::PolicyKind::Fast,
};

pub(crate) fn apply_policy(opts: &RuntimeOptions) -> PolicyEffects {
match opts.policy {
PublishPolicy::Safe => PolicyEffects {
run_dry_run: !opts.no_verify,
check_ownership: !opts.skip_ownership_check,
strict_ownership: opts.strict_ownership,
readiness_enabled: opts.readiness.enabled,
},
PublishPolicy::Balanced => PolicyEffects {
run_dry_run: !opts.no_verify,
check_ownership: false,
strict_ownership: false,
readiness_enabled: opts.readiness.enabled,
},
PublishPolicy::Fast => PolicyEffects {
run_dry_run: false,
check_ownership: false,
strict_ownership: false,
readiness_enabled: false,
},
}
shipper_policy::evaluate(
policy,
opts.no_verify,
opts.skip_ownership_check,
opts.strict_ownership,
opts.readiness.enabled,
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This function appears to be a reimplementation of shipper_policy::apply_policy. Since shipper-policy is already a dependency and its apply_policy function has the same signature and logic, you can simplify this by calling it directly. This will reduce code duplication and keep the logic centralized in the shipper-policy crate.

pub(crate) fn policy_effects(opts: &RuntimeOptions) -> shipper_policy::PolicyEffects {
    shipper_policy::apply_policy(opts)
}

@codecov

codecov Bot commented Feb 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.37041% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/shipper-plan/src/lib.rs 99.34% 3 Missing ⚠️
crates/shipper-levels/src/lib.rs 98.90% 2 Missing ⚠️
crates/shipper-cli/src/main.rs 88.88% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dc73ee0391

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

.api_base
.clone()
.unwrap_or_else(|| "https://crates.io".to_string());
let index_base = cli.api_base.as_ref().map(|_| api_base.clone());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve derived sparse index host when api base is overridden

Setting index_base to the same value as --api-base makes index readiness checks hit the API host instead of the sparse-index host. This regresses users who pass --api-base (including https://crates.io) and run with --readiness-method index (or both with index preferred): readiness polling can never observe the new version because requests go to the wrong endpoint, causing false publish failures/timeouts after upload.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR extracts tightly-coupled domain logic from the core shipper crate into 7 new focused microcrates, significantly improving modularity, testability, and compile-time parallelism. The refactoring maintains backward compatibility through careful delegation and re-exports while adding comprehensive testing infrastructure including 6 new fuzz targets and extensive property-based tests.

Changes:

  • Extracted 7 domain-specific microcrates (schema, duration, plan, policy, cargo-failure, levels, sparse-index) with ~1,700 lines of focused, reusable code
  • Refactored core crates to delegate to microcrates while maintaining public API compatibility
  • Added 6 new fuzz targets and extensive property-based tests for hardening critical parsing and classification logic

Reviewed changes

Copilot reviewed 56 out of 57 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
crates/shipper-schema/* New microcrate for schema version parsing and backward-compatibility validation
crates/shipper-duration/* New microcrate for human-readable duration parsing with bidirectional serde codecs
crates/shipper-plan/* New microcrate extracting workspace plan generation and topological sorting logic
crates/shipper-policy/* New microcrate isolating publish policy evaluation into PolicyEffects
crates/shipper-cargo-failure/* New microcrate for cargo publish failure classification heuristics
crates/shipper-levels/* New microcrate implementing Kahn's algorithm for dependency-level grouping
crates/shipper-sparse-index/* New microcrate for sparse-index path computation and version lookup
crates/shipper/src/engine.rs Delegates policy evaluation and failure classification to microcrates
crates/shipper/src/plan.rs Delegates level grouping to shipper-levels microcrate
crates/shipper/src/state.rs Delegates schema validation to shipper-schema microcrate
crates/shipper/src/store.rs Delegates schema validation to shipper-schema microcrate
crates/shipper/src/registry.rs Delegates sparse-index logic to shipper-sparse-index microcrate
crates/shipper/src/types.rs Re-exports duration codecs from shipper-duration
crates/shipper/src/lib.rs Adds cargo_failure public module re-export for API continuity
crates/shipper-types/src/lib.rs Delegates duration serde and level grouping to microcrates
crates/shipper-cli/src/main.rs Uses shipper-duration for CLI duration parsing
fuzz/fuzz_targets/* 6 new fuzz targets for microcrate hardening
crates/shipper/tests/* New integration tests validating microcrate contracts
crates/shipper-cli/tests/bdd_publish.rs Extended BDD tests covering new functionality
features/*.feature Updated Gherkin scenarios for policy and readiness modes
docs/testing.md Updated documentation for new test targets and microcrates
Cargo.toml Added 7 new workspace members
.github/workflows/ci.yml Updated feature matrix and MSRV check configuration

Comment thread .github/workflows/ci.yml
Comment on lines +101 to +104
- name: Install Rust (MSRV)
uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.92.0

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

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

The toolchain action configuration has changed from using a pinned version dtolnay/rust-toolchain@1.100.0 to dtolnay/rust-toolchain@stable with an explicit toolchain: 1.92.0 parameter. This is actually more correct - the action version (like @stable or @1.100.0) refers to the GitHub Action itself, not the Rust version. The Rust version should be specified via the toolchain parameter, which this change properly does. However, @stable is an alias that could point to different action versions over time. Consider using a specific action version hash or tag for reproducibility.

Copilot uses AI. Check for mistakes.
Comment on lines +232 to +237
let index_suffix = match lower.len() {
1 => format!("1/{lower}"),
2 => format!("2/{lower}"),
3 => format!("3/{}/{lower}", &lower[..1]),
_ => format!("{}/{}/{lower}", &lower[..2], &lower[2..4]),
};

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

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

The sparse index path computation logic is duplicated here (lines 232-237) instead of reusing the shipper_sparse_index::sparse_index_path function. This creates a maintenance burden - if the sparse index path logic changes, this test will need to be updated separately. Consider importing and using shipper_sparse_index::sparse_index_path directly.

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/ci.yml
Comment on lines 264 to +266
cargo fuzz run resolve_token --target x86_64-unknown-linux-gnu -- -max_total_time=60
cargo fuzz run schema_version --target x86_64-unknown-linux-gnu -- -max_total_time=60
cargo fuzz run release_levels --target x86_64-unknown-linux-gnu -- -max_total_time=60

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

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

The PR adds 6 new fuzz targets (duration_codec, schema_version, policy_effects, cargo_failure_classifier, release_levels, sparse_index) but the CI workflow only runs 2 of them (schema_version and release_levels). The other 4 targets should be added to ensure they're regularly exercised and don't regress.

Suggested change
cargo fuzz run resolve_token --target x86_64-unknown-linux-gnu -- -max_total_time=60
cargo fuzz run schema_version --target x86_64-unknown-linux-gnu -- -max_total_time=60
cargo fuzz run release_levels --target x86_64-unknown-linux-gnu -- -max_total_time=60
cargo fuzz run resolve_token --target x86_64-unknown-linux-gnu -- -max_total_time=60
cargo fuzz run duration_codec --target x86_64-unknown-linux-gnu -- -max_total_time=60
cargo fuzz run schema_version --target x86_64-unknown-linux-gnu -- -max_total_time=60
cargo fuzz run policy_effects --target x86_64-unknown-linux-gnu -- -max_total_time=60
cargo fuzz run cargo_failure_classifier --target x86_64-unknown-linux-gnu -- -max_total_time=60
cargo fuzz run release_levels --target x86_64-unknown-linux-gnu -- -max_total_time=60
cargo fuzz run sparse_index --target x86_64-unknown-linux-gnu -- -max_total_time=60

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 25

Caution

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

⚠️ Outside diff range comments (1)
.github/workflows/ci.yml (1)

101-117: ⚠️ Potential issue | 🟡 Minor

MSRV job should use cargo build per build-command guideline.
Switching from cargo check aligns the debug build command with the workspace standard.

💡 Suggested change
-      - name: Check with MSRV
-        run: cargo check --workspace
+      - name: Build with MSRV
+        run: cargo build --workspace
As per coding guidelines, "Use `cargo build` for debug builds and `cargo build --release` for release builds with LTO and stripping".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 101 - 117, Replace the debug check
command in the MSRV job: the step currently named "Check with MSRV" runs "cargo
check --workspace" — change it to run "cargo build --workspace" (and optionally
rename the step to "Build with MSRV") so the MSRV job uses a debug build per the
build-command guideline.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/shipper-cargo-failure/tests/classifier_contract_integration.rs`:
- Around line 1-19: The tests only exercise a single pattern per class; add unit
tests that call classify_publish_failure with additional representative strings
from RETRYABLE_PATTERNS and PERMANENT_PATTERNS (e.g., rate-limit messages, auth
failures, "already uploaded" messages) and assert CargoFailureClass::Retryable
or ::Permanent accordingly, and add a proptest using proptest::prelude::*, e.g.,
a test classify_never_panics that generates arbitrary stderr and stdout strings
and asserts the returned outcome.class is one of CargoFailureClass::Retryable |
CargoFailureClass::Permanent | CargoFailureClass::Ambiguous to ensure no panics
or unexpected behavior from classify_publish_failure.

In `@crates/shipper-cli/src/main.rs`:
- Around line 279-294: The current code sets index_base =
cli.api_base.as_ref().map(|_| api_base.clone()), causing index readiness checks
to call the API host; change index_base handling so it is left None when the
user supplies --api-base (allow Registry::get_index_base to derive the index
host), or explicitly derive an index URL from the provided api_base (e.g.,
convert https://crates.io -> https://index.crates.io) before constructing
ReleaseSpec; update the variable index_base and the ReleaseSpec/Registry
construction (see index_base, api_base, ReleaseSpec and Registry) to use the
corrected value.

In `@crates/shipper-cli/tests/bdd_publish.rs`:
- Around line 77-162: Extract the duplicated workspace builders
(create_single_crate_workspace and create_parallel_workspace) into a shared test
helper module (e.g., tests/support or a common test_utils module) and update
this test and the other consumer (parallel_levels_integration.rs) to call the
shared helpers; specifically, move the implementation of both functions into the
shared module, export them (pub) so tests can use them, replace the local
definitions in crates/shipper-cli/tests/bdd_publish.rs with imports from the
shared helper, and update any use sites to call the new shared function names to
avoid drift and duplication.
- Around line 227-268: The test server in spawn_index_readiness_registry
currently stops after exactly four requests (for _ in 0..4), which can make
tests flaky; change the fixed-count loop to a loop that continues until
server.recv_timeout returns None or Err (i.e., loop { match
server.recv_timeout(Duration::from_secs(5)) { Ok(Some(req)) => { ... handle req
(same logic using api_path/index_path/index_body) }, Ok(None) | Err(_) => break
} } ) so the thread serves requests until a timeout occurs (or make the Duration
configurable) rather than a hard limit of 4.
- Around line 313-352: Replace the hard-coded asserts in the test function
given_parallelizable_workspace_when_grouping_levels_then_independent_crates_share_level
with an insta snapshot assertion: import insta::assert_yaml_snapshot at the top
of the test module, serialize the computed levels (the levels variable produced
by ws.plan.group_by_levels()) into a snapshot-friendly form (e.g., map levels to
their package names using the existing iterator logic) and call
assert_yaml_snapshot! with that value so the expected level grouping is stored
and managed by insta instead of inline assert_eq! calls.
- Around line 565-625: Add an insta JSON snapshot assertion for the receipt
instead of (or in addition to) the manual package assertion: import
insta::assert_json_snapshot at the top of the test module and replace the
current packages/assert! check in
given_index_readiness_mode_when_publish_then_reads_sparse_index_metadata with a
call to assert_json_snapshot!(receipt) (or assert_json_snapshot!(packages) if
you prefer to snapshot only the packages array) so the test records the receipt
JSON output for future diffs.

In `@crates/shipper-duration/src/lib.rs`:
- Around line 34-39: The current serialize_duration function casts
duration.as_millis() (u128) to u64, which truncates values > u64::MAX; change
serialize_duration to call duration.as_millis().try_into() and handle the
Result—on Ok pass the u64 into serializer.serialize_u64, on Err return a serde
serialization error (e.g., via S::Error::custom or serializer error conversion)
so overflow is detected and reported instead of silently truncating. Ensure you
update the function signature uses S: Serializer and keep the
serializer.serialize_u64 call for the success path.
- Around line 10-13: Change parse_duration to return anyhow::Result<Duration>
instead of Result<Duration, humantime::DurationError>: add anyhow to
dependencies and update the signature of parse_duration and its body to
map/convert the humantime error into an anyhow::Error (for example via
.map_err(anyhow::Error::from) or using anyhow::Context). Specifically, update
the function named parse_duration to return anyhow::Result<Duration> and convert
the result of humantime::parse_duration(input) into an anyhow result; also add
anyhow = "..." to the crate's Cargo.toml dependencies.

In `@crates/shipper-duration/tests/duration_config_integration.rs`:
- Around line 19-51: Add insta snapshot assertions to the three tests instead of
literal assert_eq checks: add insta = "1" to dev-dependencies, import insta
macros in the test module, and replace the existing equality checks in
toml_accepts_human_readable_durations, json_accepts_millisecond_integers, and
json_serializes_as_milliseconds with insta::assert_snapshot! calls of the
serialized output (e.g., for toml test serialize cfg with toml::to_string(&cfg)
and assert_snapshot!(toml_out), for json tests use serde_json::to_string(&cfg)
or reuse the existing out variable and assert_snapshot!(out)); keep the rest of
the test logic intact so the parsed RetryLikeConfig values are what get
serialized for snapshots.

In `@crates/shipper-levels/src/lib.rs`:
- Around line 148-223: Add insta as a dev-dependency in the crate by adding
`insta = "1"` to the `[dev-dependencies]` in Cargo.toml, then convert the
equality assertions in the tests in lib.rs (tests like
returns_empty_for_empty_input, assigns_chain_to_strict_levels,
assigns_independent_branches_to_same_level,
ignores_dependencies_outside_the_plan,
falls_back_deterministically_when_cycle_is_present) from
assert_eq!(names(&levels), ...) to insta snapshots, e.g. replace each assert_eq!
with insta::assert_debug_snapshot!(names(&levels)); so the tests use insta
snapshots for the output of names(&levels).

In `@crates/shipper-plan/src/lib.rs`:
- Around line 1-9: Add the crate-level attribute to forbid unsafe code by
inserting #![forbid(unsafe_code)] at the top of the crate root (lib.rs) before
the use/imports so the compiler enforces the workspace unsafe-code prohibition
for this crate; ensure the attribute appears as the very first item in the file
above the existing use statements.
- Around line 24-226: The dependency name lists inserted into the plan are
nondeterministic because they iterate over deps_of (keyed by PackageId); in
build_plan, update the logic that builds the dependencies BTreeMap so that for
each id you collect dependency package names, sort them (e.g., lexicographically
by package name) before inserting into dependencies; locate the construction
around the deps_of.get(id) mapping in build_plan and ensure the Vec<String>
dep_names is sorted deterministically prior to dependencies.insert(pkg_name,
dep_names).
- Around line 451-626: Add serial_test = "3" to dev-dependencies in
crates/shipper-plan/Cargo.toml, then import the crate (use serial_test::serial
or #[serial] attribute) in lib.rs and mark the filesystem-mutating tests with
#[serial]: build_plan_filters_publishability_and_orders_dependencies,
build_plan_rejects_publishable_depending_on_non_publishable,
build_plan_package_selection_ignores_unrelated_invalid_deps,
build_plan_allows_dev_dep_on_non_publishable,
build_plan_selected_packages_include_internal_dependencies,
build_plan_selected_single_package_does_not_include_dependents,
build_plan_errors_for_unknown_selected_package, topo_sort_reports_cycles, and
build_plan_is_deterministic_for_independent_nodes_by_name; leave
build_plan_errors_for_missing_manifest unchanged. Ensure the import and
attribute usage reference the serial_test crate so tests run serially when they
touch the filesystem.
- Around line 312-329: The tests module uses verbose inline assertions; add
insta to dev-dependencies and convert those assertions to snapshot tests: update
Cargo.toml [dev-dependencies] to include insta, then in the tests mod (functions
using proptest and the test helpers like write_file, MetadataCommand usages and
the tests that currently call assert_eq! on package name vectors and
assert!(format!("{err:#}").contains(...))) replace those equality and
error-contains checks with insta snapshot assertions (e.g., capture the vector
or formatted error as a string and call insta::assert_snapshot! or
assert_display_snapshot!) so the package name lists and error messages are
stored as snapshots for easier maintenance. Ensure to import insta macros in the
tests module and keep existing test setup (tempdir, write_file, etc.) intact.

In `@crates/shipper-policy/src/lib.rs`:
- Around line 83-123: Add insta as a dev-dependency in the crate's Cargo.toml
(insta = "1.46.3"), import the macro in the test module with use
insta::assert_debug_snapshot;, and replace the three assert_eq! checks in the
tests safe_policy_respects_verify_ownership_and_readiness_flags,
balanced_policy_disables_ownership_enforcement, and
fast_policy_disables_safety_checks with assert_debug_snapshot!(effects); so each
test snapshot-asserts the result of evaluate(PolicyKind::..., ...), referencing
the evaluate function, PolicyKind enum, and PolicyEffects struct.

In `@crates/shipper-policy/tests/runtime_options_contract.rs`:
- Around line 36-81: Add the insta snapshot testing dependency and replace the
manual boolean assertions with snapshot assertions: add insta = "1.46" to
[dev-dependencies] in the crate's Cargo.toml (match workspace version), add use
insta::assert_debug_snapshot at the top of
crates/shipper-policy/tests/runtime_options_contract.rs, and in each test
(apply_policy_reads_runtime_options_for_safe_mode,
apply_policy_balanced_ignores_strict_ownership,
apply_policy_fast_disables_all_safety_checks) replace the multiple assert! calls
with a single assert_debug_snapshot!(effects) to snapshot the apply_policy
output created from sample_runtime_options and apply_policy.

In `@crates/shipper-schema/src/lib.rs`:
- Around line 6-15: The validation in parse_schema_version is too permissive
because parts[0].starts_with("shipper") accepts values like "shipperx"; change
the check to require an exact match (parts[0] == "shipper") and keep the
existing v-prefixed version check on parts[2]; then continue parsing
version_part as before (ensure you still slice off the leading 'v' from parts[2]
and parse into u32). This update should be made inside the parse_schema_version
function where parts, parts[0], and parts[2] are inspected.

In `@crates/shipper-sparse-index/src/lib.rs`:
- Around line 18-26: The function sparse_index_path panics on non-ASCII crate
names because it uses byte-range slicing like &lower[..2] and &lower[2..4];
change it to operate on chars (e.g., iterate lower.chars()) and build prefix
segments from characters rather than byte indices so multibyte UTF-8 chars don't
split; compute segments by collecting the first 0/1/2 chars and the next 2 chars
as needed (falling back to available chars for short names), then format the
same "0/", "1/{...}", "2/{...}", "3/{}/{...}", and "{}/{}/{}" patterns using
those char-derived strings to preserve behavior without panics in
sparse_index_path and any callers using lower.

In `@crates/shipper-types/tests/release_levels_integration.rs`:
- Around line 15-59: Add insta snapshot testing for the test
release_plan_groups_parallel_levels_from_shared_microcrate_logic: add insta to
dev-dependencies in crates/shipper-types/Cargo.toml, then replace the three
explicit assert_eq checks against levels with a single snapshot assertion that
serializes `levels` (the value returned from ReleasePlan::group_by_levels())
into a stable string (e.g., using Debug/serde JSON or Display) and call
insta::assert_snapshot! with that serialized output; keep the test name and use
the `levels` variable to generate the snapshot so future changes to
ReleasePlan::group_by_levels() are validated via insta snapshots.

In `@crates/shipper/src/lib.rs`:
- Around line 160-164: The feature-gated module swap causes two distinct
PlannedWorkspace types; fix by moving the PlannedWorkspace definition into a
shared types crate and making both implementations import and re-export that
single type so crate::plan::PlannedWorkspace is identical whether feature
"micro-plan" is enabled or not: create a new crate (e.g., shipper-types) that
defines PlannedWorkspace, update crates/shipper/src/plan.rs to use and re-export
shipper-types::PlannedWorkspace, update crates/shipper-plan to depend on and use
the same shipper-types::PlannedWorkspace (and have plan_micro.rs re-export it),
then keep engine.rs and engine_parallel.rs importing
crate::plan::PlannedWorkspace unchanged.

In `@crates/shipper/tests/parallel_levels_integration.rs`:
- Around line 93-117: The test currently uses multiple assert_eq! checks on the
levels variable to validate ordering; replace these with an insta snapshot
assertion by converting levels into a stable, human-readable representation (for
example map each level's packages to their names via levels.iter().map(|l|
l.packages.iter().map(|p|
p.name.as_str()).collect::<Vec<_>>()).collect::<Vec<_>>() or otherwise stringify
the levels) and call insta::assert_snapshot! on that serialized value so the
expected ordering is stored in a snapshot; update the test to import insta and
snapshot the derived Vec of package-name lists from levels (referencing the
levels variable and packages/name fields).
- Around line 1-7: The filesystem-mutating test that uses tempdir() and
write_file() must be marked with the serial test attribute to avoid concurrent
runs; add use serial_test::serial to the imports and annotate the test function
that performs the tempdir()/write_file() operations with #[serial] (place the
attribute immediately above the test fn declaration, e.g., the test that calls
tempdir()/write_file()), ensuring the test remains annotated alongside any
existing #[test] attribute.

In `@features/preflight_checks.feature`:
- Around line 45-49: The test Scenario "Balanced policy ignores strict ownership
requirement" currently uses the "--no-verify" flag which can short-circuit
ownership checks; either remove "--no-verify" from the When step that runs
"shipper preflight" so the preflight performs real ownership verification, or
keep the flag but add an explicit assertion that ownership verification was
executed and that it was bypassed due to the balanced policy (e.g. assert the
verification log/message or a specific exit-path indicator); update the Scenario
step referencing the command and the policy flags ("--policy balanced" and
"--strict-ownership") and add the extra assertion step that checks for the
ownership-verification outcome to ensure the test actually validates policy
behavior.

In `@fuzz/fuzz_targets/schema_version.rs`:
- Around line 10-11: Tests only fuzz the first parameter of
validate_schema_version because validate_schema_version calls
parse_schema_version on both the fuzz-controlled input and the hardcoded
minimum_supported value; the hardcoded side is never exercised. Modify the fuzz
target in schema_version.rs to split the incoming fuzz bytes into two slices
(e.g., via simple length prefix or half-splitting) and pass one slice to
shipper_schema::parse_schema_version and the other slice as the
minimum_supported argument to shipper_schema::validate_schema_version so both
parse_schema_version and the minimum_supported parsing are fuzz-exercised.

In `@fuzz/fuzz_targets/sparse_index.rs`:
- Around line 3-29: The fuzz harness builds JSON by embedding raw fuzzer strings
with format! which fails when inputs contain quotes or backslashes; update the
code that creates JSON lines (the mapping in content_lines and the guaranteed
string) to build JSON objects via serde_json (e.g., use the json! macro or
serde_json::to_string on a map) so values are properly escaped, add the
appropriate use/import for serde_json::json at the top, and replace the two
format! calls that produce "{\"vers\":\"...\"}" (used when building
content_lines and guaranteed_content) with serde_json-produced strings; keep all
existing variable names and calls to contains_version and sparse_index_path
unchanged.

---

Outside diff comments:
In @.github/workflows/ci.yml:
- Around line 101-117: Replace the debug check command in the MSRV job: the step
currently named "Check with MSRV" runs "cargo check --workspace" — change it to
run "cargo build --workspace" (and optionally rename the step to "Build with
MSRV") so the MSRV job uses a debug build per the build-command guideline.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6255166 and dc73ee0.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (56)
  • .github/workflows/ci.yml
  • Cargo.toml
  • crates/shipper-cargo-failure/Cargo.toml
  • crates/shipper-cargo-failure/src/lib.rs
  • crates/shipper-cargo-failure/tests/classifier_contract_integration.rs
  • crates/shipper-cli/Cargo.toml
  • crates/shipper-cli/src/main.rs
  • crates/shipper-cli/tests/bdd_publish.rs
  • crates/shipper-duration/Cargo.toml
  • crates/shipper-duration/src/lib.rs
  • crates/shipper-duration/tests/duration_config_integration.rs
  • crates/shipper-levels/Cargo.toml
  • crates/shipper-levels/src/lib.rs
  • crates/shipper-plan/Cargo.toml
  • crates/shipper-plan/src/lib.rs
  • crates/shipper-policy/Cargo.toml
  • crates/shipper-policy/src/lib.rs
  • crates/shipper-policy/tests/runtime_options_contract.rs
  • crates/shipper-registry/Cargo.toml
  • crates/shipper-registry/src/lib.rs
  • crates/shipper-schema/Cargo.toml
  • crates/shipper-schema/src/lib.rs
  • crates/shipper-schema/tests/schema_contract_integration.rs
  • crates/shipper-sparse-index/Cargo.toml
  • crates/shipper-sparse-index/src/lib.rs
  • crates/shipper-sparse-index/tests/sparse_index_contract_integration.rs
  • crates/shipper-state/Cargo.toml
  • crates/shipper-state/src/lib.rs
  • crates/shipper-store/Cargo.toml
  • crates/shipper-store/src/lib.rs
  • crates/shipper-types/Cargo.toml
  • crates/shipper-types/src/lib.rs
  • crates/shipper-types/tests/release_levels_integration.rs
  • crates/shipper/Cargo.toml
  • crates/shipper/src/engine.rs
  • crates/shipper/src/engine_parallel.rs
  • crates/shipper/src/lib.rs
  • crates/shipper/src/plan.rs
  • crates/shipper/src/plan_micro.rs
  • crates/shipper/src/registry.rs
  • crates/shipper/src/state.rs
  • crates/shipper/src/store.rs
  • crates/shipper/src/types.rs
  • crates/shipper/tests/parallel_levels_integration.rs
  • crates/shipper/tests/schema_version_integration.rs
  • docs/testing.md
  • features/parallel_levels.feature
  • features/preflight_checks.feature
  • features/publish_resume.feature
  • fuzz/Cargo.toml
  • fuzz/fuzz_targets/cargo_failure_classifier.rs
  • fuzz/fuzz_targets/duration_codec.rs
  • fuzz/fuzz_targets/policy_effects.rs
  • fuzz/fuzz_targets/release_levels.rs
  • fuzz/fuzz_targets/schema_version.rs
  • fuzz/fuzz_targets/sparse_index.rs

Comment on lines +1 to +19
use shipper_cargo_failure::{CargoFailureClass, classify_publish_failure};

#[test]
fn classifies_common_registry_throttling_errors_as_retryable() {
let outcome = classify_publish_failure("received HTTP 503 from index", "");
assert_eq!(outcome.class, CargoFailureClass::Retryable);
}

#[test]
fn classifies_manifest_validation_errors_as_permanent() {
let outcome = classify_publish_failure("", "error: failed to parse manifest at Cargo.toml");
assert_eq!(outcome.class, CargoFailureClass::Permanent);
}

#[test]
fn unknown_output_stays_ambiguous() {
let outcome = classify_publish_failure("tool exited with status 101", "see logs");
assert_eq!(outcome.class, CargoFailureClass::Ambiguous);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Expand coverage: only one pattern per failure class is tested.

Each test exercises a single representative string per class, but the classifier matches against arrays of patterns (RETRYABLE_PATTERNS, PERMANENT_PATTERNS). Regressions in other patterns (e.g., rate-limit messages, authentication errors, "already uploaded" permanents) would go undetected.

Consider:

  1. Adding tests for additional patterns in each class (at least the most critical ones).
  2. Adding a proptest test that generates arbitrary (stderr, stdout) strings and asserts the result is always one of {Retryable, Permanent, Ambiguous} — this would catch panics/crashes on unexpected input.

As per coding guidelines, "Use property-based testing with proptest for testing with generated inputs".

💡 Example proptest for exhaustive class coverage
// At bottom of file or in a separate proptest module:
use proptest::prelude::*;

proptest! {
    #[test]
    fn classify_never_panics(stderr in ".*", stdout in ".*") {
        let outcome = classify_publish_failure(&stderr, &stdout);
        // Result is always a valid class
        match outcome.class {
            CargoFailureClass::Retryable
            | CargoFailureClass::Permanent
            | CargoFailureClass::Ambiguous => {}
        }
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/shipper-cargo-failure/tests/classifier_contract_integration.rs` around
lines 1 - 19, The tests only exercise a single pattern per class; add unit tests
that call classify_publish_failure with additional representative strings from
RETRYABLE_PATTERNS and PERMANENT_PATTERNS (e.g., rate-limit messages, auth
failures, "already uploaded" messages) and assert CargoFailureClass::Retryable
or ::Permanent accordingly, and add a proptest using proptest::prelude::*, e.g.,
a test classify_never_panics that generates arbitrary stderr and stdout strings
and asserts the returned outcome.class is one of CargoFailureClass::Retryable |
CargoFailureClass::Permanent | CargoFailureClass::Ambiguous to ensure no panics
or unexpected behavior from classify_publish_failure.

Comment on lines +279 to 294
let api_base = cli
.api_base
.clone()
.unwrap_or_else(|| "https://crates.io".to_string());
let index_base = cli.api_base.as_ref().map(|_| api_base.clone());

let spec = ReleaseSpec {
manifest_path: cli.manifest_path.clone(),
registry: Registry {
name: cli
.registry
.clone()
.unwrap_or_else(|| "crates-io".to_string()),
api_base: cli
.api_base
.clone()
.unwrap_or_else(|| "https://crates.io".to_string()),
index_base: None,
api_base,
index_base,
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix index_base default when --api-base is set.

Line 283 sets index_base to the API base, so index readiness checks will hit the API host instead of the derived index host (e.g., https://crates.io vs https://index.crates.io). Leave it unset (and let Registry::get_index_base derive) or derive explicitly.

🐛 Suggested fix
-    let index_base = cli.api_base.as_ref().map(|_| api_base.clone());
+    let index_base = None;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let api_base = cli
.api_base
.clone()
.unwrap_or_else(|| "https://crates.io".to_string());
let index_base = cli.api_base.as_ref().map(|_| api_base.clone());
let spec = ReleaseSpec {
manifest_path: cli.manifest_path.clone(),
registry: Registry {
name: cli
.registry
.clone()
.unwrap_or_else(|| "crates-io".to_string()),
api_base: cli
.api_base
.clone()
.unwrap_or_else(|| "https://crates.io".to_string()),
index_base: None,
api_base,
index_base,
},
let api_base = cli
.api_base
.clone()
.unwrap_or_else(|| "https://crates.io".to_string());
let index_base = None;
let spec = ReleaseSpec {
manifest_path: cli.manifest_path.clone(),
registry: Registry {
name: cli
.registry
.clone()
.unwrap_or_else(|| "crates-io".to_string()),
api_base,
index_base,
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/shipper-cli/src/main.rs` around lines 279 - 294, The current code sets
index_base = cli.api_base.as_ref().map(|_| api_base.clone()), causing index
readiness checks to call the API host; change index_base handling so it is left
None when the user supplies --api-base (allow Registry::get_index_base to derive
the index host), or explicitly derive an index URL from the provided api_base
(e.g., convert https://crates.io -> https://index.crates.io) before constructing
ReleaseSpec; update the variable index_base and the ReleaseSpec/Registry
construction (see index_base, api_base, ReleaseSpec and Registry) to use the
corrected value.

Comment on lines +77 to +162
fn create_single_crate_workspace(root: &Path) {
write_file(
&root.join("Cargo.toml"),
r#"
[workspace]
members = ["demo"]
resolver = "2"
"#,
);

write_file(
&root.join("demo/Cargo.toml"),
r#"
[package]
name = "demo"
version = "0.1.0"
edition = "2021"
"#,
);
write_file(&root.join("demo/src/lib.rs"), "pub fn demo() {}\n");
}

fn create_parallel_workspace(root: &Path) {
write_file(
&root.join("Cargo.toml"),
r#"
[workspace]
members = ["core", "api", "cli", "app"]
resolver = "2"
"#,
);

write_file(
&root.join("core/Cargo.toml"),
r#"
[package]
name = "core"
version = "0.1.0"
edition = "2021"
"#,
);
write_file(&root.join("core/src/lib.rs"), "pub fn core() {}\n");

write_file(
&root.join("api/Cargo.toml"),
r#"
[package]
name = "api"
version = "0.1.0"
edition = "2021"

[dependencies]
core = { path = "../core" }
"#,
);
write_file(&root.join("api/src/lib.rs"), "pub fn api() {}\n");

write_file(
&root.join("cli/Cargo.toml"),
r#"
[package]
name = "cli"
version = "0.1.0"
edition = "2021"

[dependencies]
core = { path = "../core" }
"#,
);
write_file(&root.join("cli/src/lib.rs"), "pub fn cli() {}\n");

write_file(
&root.join("app/Cargo.toml"),
r#"
[package]
name = "app"
version = "0.1.0"
edition = "2021"

[dependencies]
api = { path = "../api" }
cli = { path = "../cli" }
"#,
);
write_file(&root.join("app/src/lib.rs"), "pub fn app() {}\n");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider extracting workspace builders to shared test support.
create_parallel_workspace overlaps with the helper in crates/shipper/tests/parallel_levels_integration.rs; sharing avoids drift between test suites.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/shipper-cli/tests/bdd_publish.rs` around lines 77 - 162, Extract the
duplicated workspace builders (create_single_crate_workspace and
create_parallel_workspace) into a shared test helper module (e.g., tests/support
or a common test_utils module) and update this test and the other consumer
(parallel_levels_integration.rs) to call the shared helpers; specifically, move
the implementation of both functions into the shared module, export them (pub)
so tests can use them, replace the local definitions in
crates/shipper-cli/tests/bdd_publish.rs with imports from the shared helper, and
update any use sites to call the new shared function names to avoid drift and
duplication.

Comment on lines +227 to +268
fn spawn_index_readiness_registry(crate_name: &str, version: &str) -> TestRegistry {
let server = Server::http("127.0.0.1:0").expect("server");
let base_url = format!("http://{}", server.server_addr());
let api_path = format!("/api/v1/crates/{crate_name}/{version}");
let lower = crate_name.to_ascii_lowercase();
let index_suffix = match lower.len() {
1 => format!("1/{lower}"),
2 => format!("2/{lower}"),
3 => format!("3/{}/{lower}", &lower[..1]),
_ => format!("{}/{}/{lower}", &lower[..2], &lower[2..4]),
};
let index_path = format!("/{index_suffix}");
let index_body = format!(
"{{\"name\":\"{crate_name}\",\"vers\":\"{version}\",\"deps\":[],\"cksum\":\"deadbeef\"}}"
);

let handle = thread::spawn(move || {
for _ in 0..4 {
let req = match server.recv_timeout(Duration::from_secs(5)) {
Ok(Some(req)) => req,
_ => break,
};

let (status, body) = if req.url() == api_path {
(404, "{}".to_string())
} else if req.url() == index_path {
(200, index_body.clone())
} else {
(404, "{}".to_string())
};

let resp = Response::from_string(body)
.with_status_code(StatusCode(status))
.with_header(
Header::from_bytes("Content-Type", "application/json").expect("header"),
);
req.respond(resp).expect("respond");
}
});

TestRegistry { base_url, handle }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid hard‑coding the registry to only four requests.
If publish flow adds more index/API checks, the server thread may stop early and cause flaky tests. Prefer looping until timeout (or make the limit configurable).

🔧 Suggested change
-    let handle = thread::spawn(move || {
-        for _ in 0..4 {
-            let req = match server.recv_timeout(Duration::from_secs(5)) {
-                Ok(Some(req)) => req,
-                _ => break,
-            };
+    let handle = thread::spawn(move || {
+        loop {
+            let req = match server.recv_timeout(Duration::from_secs(5)) {
+                Ok(Some(req)) => req,
+                _ => break,
+            };
             let (status, body) = if req.url() == api_path {
                 (404, "{}".to_string())
             } else if req.url() == index_path {
                 (200, index_body.clone())
             } else {
                 (404, "{}".to_string())
             };
@@
             req.respond(resp).expect("respond");
         }
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn spawn_index_readiness_registry(crate_name: &str, version: &str) -> TestRegistry {
let server = Server::http("127.0.0.1:0").expect("server");
let base_url = format!("http://{}", server.server_addr());
let api_path = format!("/api/v1/crates/{crate_name}/{version}");
let lower = crate_name.to_ascii_lowercase();
let index_suffix = match lower.len() {
1 => format!("1/{lower}"),
2 => format!("2/{lower}"),
3 => format!("3/{}/{lower}", &lower[..1]),
_ => format!("{}/{}/{lower}", &lower[..2], &lower[2..4]),
};
let index_path = format!("/{index_suffix}");
let index_body = format!(
"{{\"name\":\"{crate_name}\",\"vers\":\"{version}\",\"deps\":[],\"cksum\":\"deadbeef\"}}"
);
let handle = thread::spawn(move || {
for _ in 0..4 {
let req = match server.recv_timeout(Duration::from_secs(5)) {
Ok(Some(req)) => req,
_ => break,
};
let (status, body) = if req.url() == api_path {
(404, "{}".to_string())
} else if req.url() == index_path {
(200, index_body.clone())
} else {
(404, "{}".to_string())
};
let resp = Response::from_string(body)
.with_status_code(StatusCode(status))
.with_header(
Header::from_bytes("Content-Type", "application/json").expect("header"),
);
req.respond(resp).expect("respond");
}
});
TestRegistry { base_url, handle }
}
fn spawn_index_readiness_registry(crate_name: &str, version: &str) -> TestRegistry {
let server = Server::http("127.0.0.1:0").expect("server");
let base_url = format!("http://{}", server.server_addr());
let api_path = format!("/api/v1/crates/{crate_name}/{version}");
let lower = crate_name.to_ascii_lowercase();
let index_suffix = match lower.len() {
1 => format!("1/{lower}"),
2 => format!("2/{lower}"),
3 => format!("3/{}/{lower}", &lower[..1]),
_ => format!("{}/{}/{lower}", &lower[..2], &lower[2..4]),
};
let index_path = format!("/{index_suffix}");
let index_body = format!(
"{{\"name\":\"{crate_name}\",\"vers\":\"{version}\",\"deps\":[],\"cksum\":\"deadbeef\"}}"
);
let handle = thread::spawn(move || {
loop {
let req = match server.recv_timeout(Duration::from_secs(5)) {
Ok(Some(req)) => req,
_ => break,
};
let (status, body) = if req.url() == api_path {
(404, "{}".to_string())
} else if req.url() == index_path {
(200, index_body.clone())
} else {
(404, "{}".to_string())
};
let resp = Response::from_string(body)
.with_status_code(StatusCode(status))
.with_header(
Header::from_bytes("Content-Type", "application/json").expect("header"),
);
req.respond(resp).expect("respond");
}
});
TestRegistry { base_url, handle }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/shipper-cli/tests/bdd_publish.rs` around lines 227 - 268, The test
server in spawn_index_readiness_registry currently stops after exactly four
requests (for _ in 0..4), which can make tests flaky; change the fixed-count
loop to a loop that continues until server.recv_timeout returns None or Err
(i.e., loop { match server.recv_timeout(Duration::from_secs(5)) { Ok(Some(req))
=> { ... handle req (same logic using api_path/index_path/index_body) },
Ok(None) | Err(_) => break } } ) so the thread serves requests until a timeout
occurs (or make the Duration configurable) rather than a hard limit of 4.

Comment on lines +313 to +352
// Scenario: Workspace fan-out/fan-in groups independent crates into a shared parallel level
#[test]
fn given_parallelizable_workspace_when_grouping_levels_then_independent_crates_share_level() {
let td = tempdir().expect("tempdir");
create_parallel_workspace(td.path());

let spec = shipper::types::ReleaseSpec {
manifest_path: td.path().join("Cargo.toml"),
registry: shipper::types::Registry::crates_io(),
selected_packages: None,
};
let ws = shipper::plan::build_plan(&spec).expect("plan");
let levels = ws.plan.group_by_levels();

assert_eq!(levels.len(), 3);
assert_eq!(
levels[0]
.packages
.iter()
.map(|p| p.name.as_str())
.collect::<Vec<_>>(),
vec!["core"]
);
assert_eq!(
levels[1]
.packages
.iter()
.map(|p| p.name.as_str())
.collect::<Vec<_>>(),
vec!["api", "cli"]
);
assert_eq!(
levels[2]
.packages
.iter()
.map(|p| p.name.as_str())
.collect::<Vec<_>>(),
vec!["app"]
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check if insta is available in dev-dependencies
rg -n "insta" Cargo.toml

Repository: EffortlessMetrics/shipper

Length of output: 51


🏁 Script executed:

# Check if insta snapshot assertions are used anywhere in the test suite
rg -n "insta::assert.*snapshot" crates/shipper-cli/tests

Repository: EffortlessMetrics/shipper

Length of output: 134


🏁 Script executed:

# Also check the entire workspace for insta usage
rg -n "insta::assert.*snapshot" --type rs

Repository: EffortlessMetrics/shipper

Length of output: 95


🏁 Script executed:

# Find all Cargo.toml files and search for insta in each
find . -name "Cargo.toml" -exec grep -l "insta" {} \;

Repository: EffortlessMetrics/shipper

Length of output: 128


🏁 Script executed:

# Check if the file bdd_publish.rs exists and verify the code snippet
cat -n crates/shipper-cli/tests/bdd_publish.rs | sed -n '313,352p'

Repository: EffortlessMetrics/shipper

Length of output: 1651


🏁 Script executed:

# Check the structure of insta usage in the existing test file
head -20 crates/shipper-cli/tests/cli_e2e.rs

Repository: EffortlessMetrics/shipper

Length of output: 541


🏁 Script executed:

# Check if insta is imported in bdd_publish.rs
head -30 crates/shipper-cli/tests/bdd_publish.rs | grep -n "insta"

Repository: EffortlessMetrics/shipper

Length of output: 51


🏁 Script executed:

# Check if there are any snapshot tests in bdd_publish.rs currently
rg -n "insta::" crates/shipper-cli/tests/bdd_publish.rs

Repository: EffortlessMetrics/shipper

Length of output: 51


🏁 Script executed:

# Check the dev-dependencies in shipper-cli Cargo.toml to confirm insta
grep -A 30 "\[dev-dependencies\]" crates/shipper-cli/Cargo.toml | head -40

Repository: EffortlessMetrics/shipper

Length of output: 221


Use insta snapshot for level-grouping expectations.
This aligns with the project's snapshot testing guidelines and makes the expected level structure more maintainable.

📸 Suggested snapshot-based assertion
-        assert_eq!(levels.len(), 3);
-        assert_eq!(
-            levels[0]
-                .packages
-                .iter()
-                .map(|p| p.name.as_str())
-                .collect::<Vec<_>>(),
-            vec!["core"]
-        );
-        assert_eq!(
-            levels[1]
-                .packages
-                .iter()
-                .map(|p| p.name.as_str())
-                .collect::<Vec<_>>(),
-            vec!["api", "cli"]
-        );
-        assert_eq!(
-            levels[2]
-                .packages
-                .iter()
-                .map(|p| p.name.as_str())
-                .collect::<Vec<_>>(),
-            vec!["app"]
-        );
+        let snapshot = levels
+            .iter()
+            .map(|level| {
+                level
+                    .packages
+                    .iter()
+                    .map(|p| p.name.as_str())
+                    .collect::<Vec<_>>()
+            })
+            .collect::<Vec<_>>();
+        insta::assert_yaml_snapshot!(snapshot);

Add use insta::assert_yaml_snapshot; at the top of the test module. The insta crate is already available in dev-dependencies.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Scenario: Workspace fan-out/fan-in groups independent crates into a shared parallel level
#[test]
fn given_parallelizable_workspace_when_grouping_levels_then_independent_crates_share_level() {
let td = tempdir().expect("tempdir");
create_parallel_workspace(td.path());
let spec = shipper::types::ReleaseSpec {
manifest_path: td.path().join("Cargo.toml"),
registry: shipper::types::Registry::crates_io(),
selected_packages: None,
};
let ws = shipper::plan::build_plan(&spec).expect("plan");
let levels = ws.plan.group_by_levels();
assert_eq!(levels.len(), 3);
assert_eq!(
levels[0]
.packages
.iter()
.map(|p| p.name.as_str())
.collect::<Vec<_>>(),
vec!["core"]
);
assert_eq!(
levels[1]
.packages
.iter()
.map(|p| p.name.as_str())
.collect::<Vec<_>>(),
vec!["api", "cli"]
);
assert_eq!(
levels[2]
.packages
.iter()
.map(|p| p.name.as_str())
.collect::<Vec<_>>(),
vec!["app"]
);
}
// Scenario: Workspace fan-out/fan-in groups independent crates into a shared parallel level
#[test]
fn given_parallelizable_workspace_when_grouping_levels_then_independent_crates_share_level() {
let td = tempdir().expect("tempdir");
create_parallel_workspace(td.path());
let spec = shipper::types::ReleaseSpec {
manifest_path: td.path().join("Cargo.toml"),
registry: shipper::types::Registry::crates_io(),
selected_packages: None,
};
let ws = shipper::plan::build_plan(&spec).expect("plan");
let levels = ws.plan.group_by_levels();
let snapshot = levels
.iter()
.map(|level| {
level
.packages
.iter()
.map(|p| p.name.as_str())
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
insta::assert_yaml_snapshot!(snapshot);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/shipper-cli/tests/bdd_publish.rs` around lines 313 - 352, Replace the
hard-coded asserts in the test function
given_parallelizable_workspace_when_grouping_levels_then_independent_crates_share_level
with an insta snapshot assertion: import insta::assert_yaml_snapshot at the top
of the test module, serialize the computed levels (the levels variable produced
by ws.plan.group_by_levels()) into a snapshot-friendly form (e.g., map levels to
their package names using the existing iterator logic) and call
assert_yaml_snapshot! with that value so the expected level grouping is stored
and managed by insta instead of inline assert_eq! calls.

Comment on lines +1 to +7
use std::fs;
use std::path::Path;

use shipper::plan;
use shipper::types::{Registry, ReleaseSpec};
use tempfile::tempdir;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd crates/shipper && find . -name "parallel_levels_integration.rs" -type f

Repository: EffortlessMetrics/shipper

Length of output: 107


🏁 Script executed:

cat -n crates/shipper/tests/parallel_levels_integration.rs | head -120

Repository: EffortlessMetrics/shipper

Length of output: 3444


🏁 Script executed:

grep -n "#\[serial\]" crates/shipper/tests/parallel_levels_integration.rs

Repository: EffortlessMetrics/shipper

Length of output: 51


🏁 Script executed:

grep -n "#\[test\]" crates/shipper/tests/parallel_levels_integration.rs

Repository: EffortlessMetrics/shipper

Length of output: 79


🏁 Script executed:

# Check if serial_test is in dependencies
rg "serial_test" Cargo.toml

Repository: EffortlessMetrics/shipper

Length of output: 51


🏁 Script executed:

# Check if insta is used in the test file
rg "insta" crates/shipper/tests/parallel_levels_integration.rs

Repository: EffortlessMetrics/shipper

Length of output: 51


🏁 Script executed:

cat crates/shipper/Cargo.toml | head -50

Repository: EffortlessMetrics/shipper

Length of output: 2406


🏁 Script executed:

# Check test dependencies in shipper crate
rg "\[dev-dependencies\]" -A 10 crates/shipper/Cargo.toml

Repository: EffortlessMetrics/shipper

Length of output: 204


Add #[serial] attribute to filesystem-mutating test.

This test writes to the filesystem via tempdir() and write_file(); it must be marked with #[serial] to prevent concurrent interference with other tests.

🔧 Suggested patch
 use std::fs;
 use std::path::Path;

 use shipper::plan;
 use shipper::types::{Registry, ReleaseSpec};
+use serial_test::serial;
 use tempfile::tempdir;

-#[test]
+#[test]
+#[serial]
 fn build_plan_groups_independent_packages_into_same_parallel_level() {

Per coding guidelines, **/*.rs: Use #[serial] attribute from serial_test for tests that mutate filesystem state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/shipper/tests/parallel_levels_integration.rs` around lines 1 - 7, The
filesystem-mutating test that uses tempdir() and write_file() must be marked
with the serial test attribute to avoid concurrent runs; add use
serial_test::serial to the imports and annotate the test function that performs
the tempdir()/write_file() operations with #[serial] (place the attribute
immediately above the test fn declaration, e.g., the test that calls
tempdir()/write_file()), ensuring the test remains annotated alongside any
existing #[test] attribute.

Comment on lines +93 to +117
assert_eq!(levels.len(), 3);
assert_eq!(
levels[0]
.packages
.iter()
.map(|p| p.name.as_str())
.collect::<Vec<_>>(),
vec!["core"]
);
assert_eq!(
levels[1]
.packages
.iter()
.map(|p| p.name.as_str())
.collect::<Vec<_>>(),
vec!["api", "cli"]
);
assert_eq!(
levels[2]
.packages
.iter()
.map(|p| p.name.as_str())
.collect::<Vec<_>>(),
vec!["app"]
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Check if the file exists and show the content around lines 93-117
echo "=== Checking file existence and content ==="
if [ -f "crates/shipper/tests/parallel_levels_integration.rs" ]; then
  echo "File exists."
  sed -n '93,117p' crates/shipper/tests/parallel_levels_integration.rs
else
  echo "File not found"
fi

Repository: EffortlessMetrics/shipper

Length of output: 691


🏁 Script executed:

#!/bin/bash

# Check if insta is in the workspace dependencies
echo "=== Checking for insta in Cargo.toml files ==="
rg "insta" --type toml Cargo.toml

# Check specifically in test dependencies
echo ""
echo "=== Checking shipper crate Cargo.toml ==="
if [ -f "crates/shipper/Cargo.toml" ]; then
  rg "insta|snapshot" crates/shipper/Cargo.toml || echo "No insta found in shipper Cargo.toml"
fi

Repository: EffortlessMetrics/shipper

Length of output: 175


🏁 Script executed:

#!/bin/bash

# Check if insta is used in existing test files
echo "=== Checking for insta usage in test files ==="
rg "use insta" --type rust crates/shipper/tests/

echo ""
echo "=== Checking for insta::assert_snapshot ==="
rg "insta::assert" --type rust crates/shipper/tests/

Repository: EffortlessMetrics/shipper

Length of output: 160


🏁 Script executed:

#!/bin/bash

# Get a broader view of what snapshot tests exist
echo "=== Looking for all test files in shipper crate ==="
find crates/shipper/tests -name "*.rs" -type f

echo ""
echo "=== Checking test structure and usage ==="
ls -la crates/shipper/tests/

Repository: EffortlessMetrics/shipper

Length of output: 536


🏁 Script executed:

#!/bin/bash

# Show the full test function to understand context
echo "=== Full test file content ==="
cat -n crates/shipper/tests/parallel_levels_integration.rs

Repository: EffortlessMetrics/shipper

Length of output: 3475


🏁 Script executed:

#!/bin/bash

# Search for insta usage across the entire codebase
echo "=== Searching for insta across entire codebase ==="
rg "use insta|assert_snapshot|insta::" --type rust

echo ""
echo "=== Checking if insta is listed as test dependency ==="
rg "\[dev-dependencies\]" -A 20 crates/shipper/Cargo.toml

Repository: EffortlessMetrics/shipper

Length of output: 665


Use insta snapshot testing for level assertions.

Replace the multiple assert_eq! calls with insta::assert_snapshot! to consolidate the level ordering expectations. Snapshot testing is the required pattern for test assertions per project guidelines, and insta is already used in CLI tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/shipper/tests/parallel_levels_integration.rs` around lines 93 - 117,
The test currently uses multiple assert_eq! checks on the levels variable to
validate ordering; replace these with an insta snapshot assertion by converting
levels into a stable, human-readable representation (for example map each
level's packages to their names via levels.iter().map(|l|
l.packages.iter().map(|p|
p.name.as_str()).collect::<Vec<_>>()).collect::<Vec<_>>() or otherwise stringify
the levels) and call insta::assert_snapshot! on that serialized value so the
expected ordering is stored in a snapshot; update the test to import insta and
snapshot the derived Vec of package-name lists from levels (referencing the
levels variable and packages/name fields).

Comment on lines +45 to +49
Scenario: Balanced policy ignores strict ownership requirement
Given no registry token is configured
And the registry returns "not found" for all crates
When I run "shipper preflight" with "--policy balanced --strict-ownership --no-verify"
Then the exit code is 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid --no-verify masking the policy behavior.

This scenario is intended to prove that the balanced policy ignores strict ownership, but --no-verify could short-circuit ownership checks and make the test pass regardless of policy. Consider removing --no-verify (if stubbing already avoids network) or adding an assertion that ownership verification actually ran and was bypassed specifically due to the policy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/preflight_checks.feature` around lines 45 - 49, The test Scenario
"Balanced policy ignores strict ownership requirement" currently uses the
"--no-verify" flag which can short-circuit ownership checks; either remove
"--no-verify" from the When step that runs "shipper preflight" so the preflight
performs real ownership verification, or keep the flag but add an explicit
assertion that ownership verification was executed and that it was bypassed due
to the balanced policy (e.g. assert the verification log/message or a specific
exit-path indicator); update the Scenario step referencing the command and the
policy flags ("--policy balanced" and "--strict-ownership") and add the extra
assertion step that checks for the ownership-verification outcome to ensure the
test actually validates policy behavior.

Comment on lines +10 to +11
let _ = shipper_schema::parse_schema_version(input);
let _ = shipper_schema::validate_schema_version(input, "shipper.receipt.v1", "schema");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

minimum_supported is never fuzz-exercised — minor coverage gap.

validate_schema_version internally calls parse_schema_version on both input (fuzz-controlled) and the hardcoded "shipper.receipt.v1" (always valid). The code path where parse_schema_version(minimum_supported) receives an arbitrary string is never reached. To close this gap, consider splitting the fuzz input and exercising both parameters:

♻️ Suggested enhancement
 fuzz_target!(|data: &[u8]| {
     let Ok(input) = std::str::from_utf8(data) else {
         return;
     };

+    // Split on first ASCII space to vary both parameters
+    let (version, minimum) = match input.find(' ') {
+        Some(i) => (&input[..i], &input[i + 1..]),
+        None => (input, "shipper.receipt.v1"),
+    };
+
-    let _ = shipper_schema::parse_schema_version(input);
-    let _ = shipper_schema::validate_schema_version(input, "shipper.receipt.v1", "schema");
+    let _ = shipper_schema::parse_schema_version(version);
+    let _ = shipper_schema::parse_schema_version(minimum);
+    let _ = shipper_schema::validate_schema_version(version, minimum, "schema");
+    let _ = shipper_schema::validate_schema_version(minimum, version, "schema");
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fuzz/fuzz_targets/schema_version.rs` around lines 10 - 11, Tests only fuzz
the first parameter of validate_schema_version because validate_schema_version
calls parse_schema_version on both the fuzz-controlled input and the hardcoded
minimum_supported value; the hardcoded side is never exercised. Modify the fuzz
target in schema_version.rs to split the incoming fuzz bytes into two slices
(e.g., via simple length prefix or half-splitting) and pass one slice to
shipper_schema::parse_schema_version and the other slice as the
minimum_supported argument to shipper_schema::validate_schema_version so both
parse_schema_version and the minimum_supported parsing are fuzz-exercised.

Comment on lines +3 to +29
use libfuzzer_sys::fuzz_target;
use shipper_sparse_index::{contains_version, sparse_index_path};

fuzz_target!(|data: (String, String, Vec<String>)| {
let (crate_name, target, versions) = data;

let path = sparse_index_path(&crate_name);
assert_eq!(path, sparse_index_path(&crate_name));

if !crate_name.is_empty() {
assert!(path.ends_with(&crate_name.to_ascii_lowercase()));
}

let mut content_lines = versions
.iter()
.take(256)
.map(|version| format!("{{\"vers\":\"{}\"}}", version))
.collect::<Vec<_>>();
content_lines.push("not-json".to_string());
let content = content_lines.join("\n");

let result = contains_version(&content, &target);
assert_eq!(result, contains_version(&content, &target));

let guaranteed = format!("{{\"vers\":\"{}\"}}", target);
let guaranteed_content = format!("{content}\n{guaranteed}");
assert!(contains_version(&guaranteed_content, &target));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if serde_json is in fuzz/Cargo.toml dependencies
cat fuzz/Cargo.toml

Repository: EffortlessMetrics/shipper

Length of output: 1628


🏁 Script executed:

#!/bin/bash
# Verify the actual code in the fuzz target file
cat -n fuzz/fuzz_targets/sparse_index.rs

Repository: EffortlessMetrics/shipper

Length of output: 1264


🏁 Script executed:

#!/bin/bash
# Check what dependencies are available in the workspace
git ls-files | grep -i cargo.toml

Repository: EffortlessMetrics/shipper

Length of output: 922


Escape JSON strings in fuzz content to prevent invalid JSON generation.

Lines 19 and 27 use format!() to construct JSON objects with unescaped string values. When fuzzer-generated input contains " or \, the resulting JSON becomes invalid—for example, version="1.0\"beta" produces {"vers":"1.0"beta"}. This causes unpredictable behavior in contains_version() and false test failures unrelated to the tested logic.

serde_json is already a dependency in fuzz/Cargo.toml, so replace the format calls with json!() macro:

Proposed fix
 use libfuzzer_sys::fuzz_target;
+use serde_json::json;
 use shipper_sparse_index::{contains_version, sparse_index_path};

         .map(|version| format!("{{\"vers\":\"{}\"}}", version))
+        .map(|version| json!({ "vers": version }).to_string())

-    let guaranteed = format!("{{\"vers\":\"{}\"}}", target);
+    let guaranteed = json!({ "vers": target }).to_string();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fuzz/fuzz_targets/sparse_index.rs` around lines 3 - 29, The fuzz harness
builds JSON by embedding raw fuzzer strings with format! which fails when inputs
contain quotes or backslashes; update the code that creates JSON lines (the
mapping in content_lines and the guaranteed string) to build JSON objects via
serde_json (e.g., use the json! macro or serde_json::to_string on a map) so
values are properly escaped, add the appropriate use/import for serde_json::json
at the top, and replace the two format! calls that produce "{\"vers\":\"...\"}"
(used when building content_lines and guaranteed_content) with
serde_json-produced strings; keep all existing variable names and calls to
contains_version and sparse_index_path unchanged.

@EffortlessSteven EffortlessSteven merged commit de2d7a7 into main Feb 24, 2026
40 checks passed
@EffortlessSteven EffortlessSteven deleted the chore/repo-url-normalize branch February 24, 2026 14:18
@coderabbitai coderabbitai Bot mentioned this pull request Mar 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants