Skip to content

Refresh stale Cargo lockfiles before publish preflight (#61) - #75

Merged
leynos merged 24 commits into
mainfrom
issue-61-refresh-stale-nested-cargo-lock-files-before-publish-preflight
Jun 8, 2026
Merged

Refresh stale Cargo lockfiles before publish preflight (#61)#75
leynos merged 24 commits into
mainfrom
issue-61-refresh-stale-nested-cargo-lock-files-before-publish-preflight

Conversation

@lodyai

@lodyai lodyai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Summary

This branch refreshes tracked Cargo.lock files after lading bump rewrites manifest versions, then validates tracked lockfiles before lading publish enters the full cargo preflight. It addresses stale nested lockfiles such as the rstest-bdd tests/ui_lints/Cargo.lock case described in issue #61.

Closes #61.

Review walkthrough

Validation

  • make check-fmt: passed
  • make lint: passed
  • make test: passed, 490 tests
  • make typecheck: passed
  • coderabbit review --agent: attempted twice; both runs reached tools_completed and then hung without returning findings, so each was terminated after an extended wait

Notes

The branch implements source-checkout lockfile refresh and source preflight validation. It does not restructure publish staging so that all publish preflight runs against the staged tree; that remains the larger follow-up investigation called out in issue #61.

@sourcery-ai sourcery-ai 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.

Sorry @LodyAI[bot], you have reached your weekly rate limit of 2500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f31ad427-c5ac-4f43-aeca-9f9bc893036a

📥 Commits

Reviewing files that changed from the base of the PR and between ad7fc83 and 527c7a8.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (35)
  • docs/developers-guide.md
  • docs/roadmap.md
  • docs/users-guide.md
  • lading/__init__.py
  • lading/cli.py
  • lading/commands/bump.py
  • lading/commands/lockfile.py
  • lading/commands/publish.py
  • lading/commands/publish_errors.py
  • lading/commands/publish_manifest.py
  • lading/commands/publish_plan.py
  • lading/commands/publish_preflight.py
  • lading/config.py
  • lading/exceptions.py
  • lading/workspace/metadata.py
  • lading/workspace/models.py
  • pyproject.toml
  • tests/bdd/features/cli.feature
  • tests/bdd/steps/metadata_fixtures.py
  • tests/bdd/steps/test_bump_steps.py
  • tests/bdd/steps/test_publish_given_steps.py
  • tests/bdd/steps/test_publish_infrastructure.py
  • tests/e2e/steps/test_e2e_steps.py
  • tests/unit/__snapshots__/test_bump_command_internals.ambr
  • tests/unit/publish/__snapshots__/test_preflight_lockfile_validation.ambr
  • tests/unit/publish/test_command_helpers.py
  • tests/unit/publish/test_preflight_cargo_runner.py
  • tests/unit/publish/test_preflight_checks.py
  • tests/unit/publish/test_preflight_lockfile_validation.py
  • tests/unit/publish/test_run_integration.py
  • tests/unit/test_bump_command_integration.py
  • tests/unit/test_bump_command_internals.py
  • tests/unit/test_cargo_metadata_loading.py
  • tests/unit/test_cli.py
  • tests/unit/test_lockfile.py

Summary

This pull request implements automatic discovery, refresh, and validation of git-tracked Cargo lockfiles in the lading tool, addressing issue #61. The implementation ensures that after version bumps update manifest files, any adjacent stale lockfiles are refreshed, and that the publish pre-flight validation detects and reports stale lockfiles before attempting cargo build steps.

Key Changes

Core Functionality

New lading/commands/lockfile.py module provides three main functions:

  • discover_tracked_lockfiles(): Discovers git-tracked Cargo.lock files (root and nested) whilst excluding those under target/ directories, and validates each has an adjacent Cargo.toml
  • refresh_lockfile(): Regenerates a lockfile by invoking cargo generate-lockfile --manifest-path <manifest>, with the invocation directory set to the manifest's parent
  • validate_lockfile_freshness(): Tests lockfile freshness under --locked using cargo metadata, returning True only when the manifest is valid and up-to-date

lading bump integration (lading/commands/bump.py):

  • Adds optional runner parameter to BumpOptions for dependency injection of the command executor
  • Extends BumpChanges with a lockfiles field to report which lockfiles were refreshed
  • After manifest rewrites, calls _refresh_lockfiles() to discover and regenerate tracked lockfiles (only when manifests changed)
  • Updated CLI output to list refreshed lockfiles with a (lockfile) suffix and to correctly report the "no changes" case only when manifests, documentation, and lockfiles are all absent

lading publish pre-flight validation (lading/commands/publish_preflight.py):

  • Introduces _validate_lockfile_freshness() helper which discovers tracked lockfiles and validates each one under --locked
  • When stale lockfiles are detected, raises PublishPreflightError with a detailed message listing each stale lockfile and providing an actionable shell-quoted cargo generate-lockfile --manifest-path <path>/Cargo.toml repair command for each

Exception Hierarchy Consolidation

Establishes lading.exceptions.LadingError as the common base exception for all expected domain-specific failures:

  • LockfileRefreshError, PublishPreflightError, PublishError, ConfigurationError, CargoMetadataError, WorkspaceModelError, and PublishPreparationError now inherit from LadingError rather than RuntimeError
  • LadingError is exported from the top-level lading package

Testing and Validation

New test suite (tests/unit/test_lockfile.py):

  • Unit tests verify lockfile discovery filters correctly, respects manifest adjacency, and handles non-git directories gracefully
  • Parametrised tests confirm refresh and freshness validation behaviour with stubbed runners
  • Hypothesis property-based tests validate internal filtering logic invariants (no target/ paths, manifest adjacency, git output subset)

Integration tests (tests/unit/test_bump_command_integration.py):

  • Verify live bump refreshes all discovered lockfiles and includes them in output
  • Confirm dry-run mode reports lockfiles without modifying them
  • Test partial-failure recovery when lockfile refresh fails mid-operation

Publish pre-flight tests (new tests/unit/publish/test_preflight_lockfile_validation.py):

  • Stub discover_tracked_lockfiles and validate the helper correctly passes environment variables to both discovery and validation steps
  • Verify stale lockfiles are aggregated and reported with actionable repair commands
  • Snapshot test confirms exact error message format

BDD scenarios (tests/bdd/features/cli.feature, tests/bdd/steps/test_bump_steps.py, tests/bdd/steps/test_publish_given_steps.py):

  • Bump scenarios verify lockfiles are listed in output and refreshed (or not, in dry-run)
  • Publish scenarios verify stale lockfiles cause exit code 1 and include repair commands in stderr

Documentation

User-facing guidance (docs/users-guide.md):

  • Documents that lading bump discovers and refreshes adjacent git-tracked lockfiles (excluding target/)
  • Explains that refreshed lockfiles are reported with a (lockfile) suffix
  • Notes that dry-run mode lists lockfiles without modifying them
  • Describes publish pre-flight validation and the actionable repair command

Developer documentation (docs/developers-guide.md):

  • New "Workspace discovery helpers" subsection documents lockfile discovery, refresh, and validation semantics
  • "Bump command internals" section explains runner dependency injection and lockfile refresh integration
  • Updated exception hierarchy guidance to reflect LadingError as the common base

Roadmap updates (docs/roadmap.md):

  • Explicitly notes how lading bump --dry-run treats tracked lockfiles
  • Describes publish pre-flight lockfile freshness validation and repair guidance

Design Decisions

  • Manifest path shell-quoting: The repair command shell-quotes manifest paths using shlex.quote to prevent unsafe copy-paste failures
  • Lockfile refresh gating: Refresh only occurs when manifests changed, avoiding unnecessary cargo invocations
  • Source checkout validation: Validation runs against the source tree (not a staged copy), leaving restructure to run pre-flight against staged artifacts as a longer-term follow-up
  • Environment propagation: The pre-flight validation step explicitly forwards the base environment (e.g. CARGO_TERM_COLOR) to lockfile freshness checks

Known Follow-ups

The PR defers several architectural improvements to tracked issues:

  • Filesystem I/O injection to remove direct .exists() calls in domain logic (#79)
  • Hypothesis property tests for filtering invariants (#80)
  • Syrupy snapshot tests for CLI output (#81)
  • Remove infrastructure runner from public domain types (#82)
  • Evaluate eliminating redundant rglob pass (#83)
  • Partial-failure recovery for lockfile refresh (#84)
  • Observability metrics (counters/timers) (#91)

Closes issue #61.

Walkthrough

This PR implements Cargo lockfile discovery, refresh, and freshness validation across lading bump and lading publish commands. It introduces a shared exception hierarchy rooted in LadingError, provides lockfile helpers using git and cargo invocations, wires lockfile refresh into bump after manifest rewrites (reporting results with a (lockfile) suffix, or listing without refresh in dry-run mode), and enforces lockfile freshness during publish preflight with actionable repair commands for stale locks.

Changes

Unified exception hierarchy

Layer / File(s) Summary
LadingError base and exception migration
lading/exceptions.py, lading/__init__.py, lading/commands/publish_errors.py, lading/commands/publish_manifest.py, lading/commands/publish_plan.py, lading/config.py, lading/workspace/metadata.py, lading/workspace/models.py
All domain-specific exceptions now inherit from LadingError instead of RuntimeError. Package exports LadingError from the root module. Exception hierarchy documentation updated.

Lockfile support

Layer / File(s) Summary
Lockfile discovery, refresh, and validation
lading/commands/lockfile.py, tests/unit/test_lockfile.py, docs/developers-guide.md
New module provides discover_tracked_lockfiles (git ls-files with target/ filtering and manifest adjacency), refresh_lockfile (cargo generate-lockfile with error handling), and validate_lockfile_freshness (cargo metadata --locked exit code check). Comprehensive unit tests include deterministic discovery/refresh/freshness tests plus Hypothesis property tests for filtering logic. Developer documentation describes the helpers and their semantics.
Bump command lockfile refresh workflow
lading/commands/bump.py, lading/cli.py, tests/bdd/features/cli.feature, tests/bdd/steps/test_bump_steps.py, tests/unit/test_bump_command_integration.py, tests/unit/test_bump_command_internals.py, tests/unit/__snapshots__/test_bump_command_internals.ambr, docs/developers-guide.md, docs/users-guide.md, docs/roadmap.md
BumpOptions gains optional runner field for dependency injection. BumpChanges includes lockfiles sequence. After manifest rewrites, _refresh_lockfiles discovers tracked lockfiles, logs intention in dry-run mode, refreshes each via the injected runner, and returns sorted paths. Lockfiles are included in no-change and output-format logic with (lockfile) suffix. CLI wires runner into options. BDD scenarios verify bump lists and refreshes tracked lockfiles, whilst dry-run lists without refreshing. Integration tests verify refresh execution, dry-run reporting, and partial failure propagation. Snapshot test documents formatted output including lockfile paths.
Publish preflight lockfile freshness enforcement
lading/commands/publish_preflight.py, lading/commands/publish.py, tests/bdd/features/cli.feature, tests/bdd/steps/test_publish_given_steps.py, tests/bdd/steps/test_publish_infrastructure.py, tests/unit/publish/test_preflight_lockfile_validation.py, tests/unit/publish/__snapshots__/test_preflight_lockfile_validation.ambr, docs/developers-guide.md, docs/users-guide.md, docs/roadmap.md
New _validate_lockfile_freshness helper discovers tracked lockfiles, validates each via validate_lockfile_freshness, aggregates stale files, and raises PublishPreflightError with shell-quoted cargo generate-lockfile --manifest-path ... repair commands. Wired into publish preflight after auxiliary build commands. BDD scenarios verify stale-lockfile error messaging and repair commands. Test infrastructure refactored to support prefix-dispatch and git command routing. Unit tests verify freshness validation, environment forwarding, and error message construction. Snapshot documents expected stale-lockfile output.

Test infrastructure and documentation updates

Layer / File(s) Summary
Test infrastructure
tests/bdd/steps/metadata_fixtures.py, tests/bdd/steps/test_publish_infrastructure.py, tests/unit/publish/test_command_helpers.py, tests/unit/publish/test_preflight_cargo_runner.py, tests/unit/publish/test_preflight_checks.py, tests/unit/publish/test_run_integration.py, tests/e2e/steps/test_e2e_steps.py, tests/unit/test_cargo_metadata_loading.py, pyproject.toml
BDD metadata fixtures mock git ls-files for Cargo.lock discovery. Preflight test infrastructure refactored with dispatch helpers and git handler for flexible cmd-mox stubbing. Cmd-mox runner selection via environment variable. Preflight cargo runner tests expanded. E2E git setup uses passthrough spy. Obsolete diagnostic test removed. Hypothesis added to dev dependencies. UTF-8 byte decoding test added.
Documentation
docs/developers-guide.md, docs/users-guide.md, docs/roadmap.md
Developer guide documents lockfile helpers, bump runner injection, and command-runner architecture. User guide describes tracked lockfile refresh behaviour (live vs dry-run) and publish freshness validation with repair guidance. Roadmap expanded with lockfile handling details.

Sequence Diagram(s)

sequenceDiagram
  participant Bump as lading bump
  participant Discover as discover_tracked_lockfiles
  participant Git as git ls-files
  participant Refresh as refresh_lockfile
  participant Cargo as cargo generate-lockfile
  Bump->>Discover: workspace_root, runner
  Discover->>Git: ls-files Cargo.lock **/Cargo.lock
  Git-->>Discover: stdout
  Discover-->>Bump: sorted lockfile paths (filtered by target/ and manifest adjacency)
  Bump->>Refresh: each manifest_path, runner
  Refresh->>Cargo: --manifest-path manifest_dir/Cargo.toml
  Cargo-->>Refresh: exit code, stderr
  Refresh-->>Bump: Cargo.lock path or LockfileRefreshError
  Bump-->>CLI: BumpChanges with lockfiles for output rendering
Loading
sequenceDiagram
  participant Preflight as _run_preflight_checks
  participant Validate as _validate_lockfile_freshness
  participant Discover as discover_tracked_lockfiles
  participant Freshness as validate_lockfile_freshness
  participant CargoMeta as cargo metadata --locked
  Preflight->>Validate: workspace_root, runner, env
  Validate->>Discover: workspace_root, runner
  Discover-->>Validate: lockfile paths
  loop each lockfile
    Validate->>Freshness: manifest_path, runner with env
    Freshness->>CargoMeta: --locked --manifest-path manifest_dir/Cargo.toml
    CargoMeta-->>Freshness: exit code
    Freshness-->>Validate: boolean (fresh or stale)
  end
  alt any stale
    Validate-->>Preflight: raise PublishPreflightError with cargo generate-lockfile commands
  else all fresh
    Validate-->>Preflight: return (continue to cargo check/test)
  end
Loading

Possibly related issues

  • leynos/lading#92: Main changes implement lockfile discovery and refresh in bump flow with corresponding BDD/E2E and unit test scenarios, directly addressing the requested test coverage.
  • leynos/lading#81: Main changes add Syrupy snapshot tests for bump lockfile output messages and publish stale-lockfile error messages, directly matching the request.
  • leynos/lading#80: Main changes add Hypothesis property-based tests covering _lockfiles_with_manifests filtering logic, directly implementing the request.

Possibly related PRs

  • leynos/lading#70: Both PRs modify lading/commands/publish.py and lading/commands/publish_preflight.py to refactor how pre-flight checks are invoked and wired into the publish execution pipeline.

Suggested labels

Roadmap


📦 Fresh lockfiles blooming bright,
Dry runs list them, live runs write,
Stale locks caught before they stray,
Cargo commands light the way! ✨

📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #61

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-61-refresh-stale-nested-cargo-lock-files-before-publish-preflight

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Jun 5, 2026

Copy link
Copy Markdown
Owner

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response.

lading/commands/lockfile.py

Comment on lines +19 to +58

def discover_tracked_lockfiles(
    workspace_root: Path,
    runner: _CommandRunner,
) -> tuple[Path, ...]:
    """Return tracked Cargo.lock files with adjacent manifests."""
    candidate_lockfiles = tuple(
        path
        for path in workspace_root.rglob("Cargo.lock")
        if "target" not in path.relative_to(workspace_root).parts
    )
    if not candidate_lockfiles:
        return ()

    exit_code, stdout, stderr = runner(
        ("git", "ls-files", "*/Cargo.lock", "Cargo.lock"),
        cwd=workspace_root,
    )
    if exit_code != 0:
        detail = (stderr or stdout).strip()
        if "not a git repository" in detail.lower():
            LOGGER.warning(
                "Skipping Cargo.lock discovery because %s is not a git repository",
                workspace_root,
            )
            return ()
        LOGGER.warning(
            "Skipping Cargo.lock discovery after git ls-files failed: %s", detail
        )
        return ()

    lockfiles: list[Path] = []
    for line in stdout.splitlines():
        relative_path = line.strip()
        if not relative_path:
            continue
        lockfile_path = workspace_root / relative_path
        manifest_path = lockfile_path.parent / "Cargo.toml"
        if manifest_path.exists():
            lockfiles.append(lockfile_path)
    return tuple(lockfiles)

❌ New issue: Complex Method
discover_tracked_lockfiles has a cyclomatic complexity of 9, threshold = 9

@leynos

leynos commented Jun 5, 2026

Copy link
Copy Markdown
Owner

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response.

lading/commands/lockfile.py

Comment on lines +19 to +58

def discover_tracked_lockfiles(
    workspace_root: Path,
    runner: _CommandRunner,
) -> tuple[Path, ...]:
    """Return tracked Cargo.lock files with adjacent manifests."""
    candidate_lockfiles = tuple(
        path
        for path in workspace_root.rglob("Cargo.lock")
        if "target" not in path.relative_to(workspace_root).parts
    )
    if not candidate_lockfiles:
        return ()

    exit_code, stdout, stderr = runner(
        ("git", "ls-files", "*/Cargo.lock", "Cargo.lock"),
        cwd=workspace_root,
    )
    if exit_code != 0:
        detail = (stderr or stdout).strip()
        if "not a git repository" in detail.lower():
            LOGGER.warning(
                "Skipping Cargo.lock discovery because %s is not a git repository",
                workspace_root,
            )
            return ()
        LOGGER.warning(
            "Skipping Cargo.lock discovery after git ls-files failed: %s", detail
        )
        return ()

    lockfiles: list[Path] = []
    for line in stdout.splitlines():
        relative_path = line.strip()
        if not relative_path:
            continue
        lockfile_path = workspace_root / relative_path
        manifest_path = lockfile_path.parent / "Cargo.toml"
        if manifest_path.exists():
            lockfiles.append(lockfile_path)
    return tuple(lockfiles)

❌ New issue: Bumpy Road Ahead
discover_tracked_lockfiles has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is 2 blocks per function

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
leynos marked this pull request as ready for review June 6, 2026 23:34

@sourcery-ai sourcery-ai 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.

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

Please try again later or upgrade to continue using Sourcery

chatgpt-codex-connector[bot]

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot added the Issue label Jun 7, 2026
coderabbitai[bot]

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Jun 7, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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.

leynos added 14 commits June 8, 2026 19:33
Update partial-refresh recovery guidance, log successful lockfile freshness validation, and exercise the real validation helper in preflight tests.
Keep the runner environment propagation assertion intact.
Document that rerunning lading bump after a partial lockfile refresh failure will not refresh lockfiles unless manifests are rewritten, and point users to cargo generate-lockfile for repair.
Require dry_run to be passed by keyword in bump output formatting helpers and remove the positional-boolean lint suppressions.
Assert the bump CLI leaves the injectable command runner unset so the command layer uses its production default while lower-level tests can still inject stubs.
Use a recursive git pathspec for tracked Cargo.lock discovery and align test stubs with that command.
Update stale-lockfile recovery text to point at direct cargo generate-lockfile repair.
Inject the manifest-existence probe used during lockfile discovery so filesystem checks are explicit and replaceable.
Add observability for lockfile discovery, refresh, and validation, plus regression coverage for partial refresh failures.
Lock bump and publish remediation formatting with syrupy snapshots.
Expand the developer guide exception hierarchy section with package-level LadingError guidance, a reuse plan for new domain exceptions, and caller/test usage rules.
Move command utility, cargo runner, metadata decoding, and lockfile
validation tests out of `test_preflight_checks.py` so the remaining
file covers only pre-flight orchestration.

Relocate the syrupy snapshot with the moved lockfile-validation test
module while preserving the snapshot key and content.
Replace four structurally identical cargo preflight command tests with a
single parametrized scenario table. Keep the failure and diagnostic tests
unchanged while preserving the same option combinations and expected command
arguments.
Expand the package exception module documentation so `LadingError` is clearly
identified as the common root for lading domain failures.

Add bump command internals guidance covering the `BumpOptions.runner` injection
point and the `BumpChanges.lockfiles` summary field.
Add the missing docstring to the lockfile discovery runner closure so the
project docstring coverage check stays above the required threshold.
Move the shared `_CommandRunner` protocol behind a neutral `_runners` module
and point bump, publish, preflight, and lockfile typing at that boundary.
Leave the concrete subprocess and cmd-mox implementation in
`publish_execution`, with `_runners._invoke` delegating there at call time.

Document the boundary in the developer guide so bump no longer appears to
depend on publish-specific execution internals for lockfile refreshes.
Add Hypothesis as a development dependency and cover the lockfile filtering
helper with generated git stdout cases.

Check that returned lockfiles never include `target` components, only pass
through approved adjacent manifests, and always originate from non-empty git
stdout lines.
Keep the issue 61 lockfile behaviour aligned with the runtime runner
boundary now present on `origin/main`. Pass the selected CLI runner into
`lading bump`, keep publish preflight runner wrappers compatible with the
shared protocol, and update tests to use the current helper modules.

Adjust BDD cmd-mox preflight registration so metadata, lockfile, package,
and publish command stubs can coexist after the rebase. Update developer
documentation to describe `lading.runtime.CommandRunner` instead of the
removed command-local runner module.
@lodyai
lodyai Bot force-pushed the issue-61-refresh-stale-nested-cargo-lock-files-before-publish-preflight branch from cfaa044 to 527c7a8 Compare June 8, 2026 17:56

@codescene-delta-analysis codescene-delta-analysis 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 Health Improved (1 files improve in Code Health)

Our agent can fix these. Install it.

Gates Passed
6 Quality Gates Passed

View Improvements
File Code Health Impact Categories Improved
test_preflight_checks.py 9.39 → 10.00 Code Duplication

Absence of Expected Change Pattern

  • lading/lading/commands/publish.py is usually changed with: lading/tests/bdd/steps/test_publish_steps.py

Quality Gate Profile: Pay Down Tech Debt
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.

@leynos

This comment was marked as resolved.

1 similar comment
@leynos

leynos commented Jun 8, 2026

Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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 coderabbitai Bot added Roadmap and removed Issue labels Jun 8, 2026

@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: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lading/commands/lockfile.py`:
- Around line 55-68: The current handler returns an empty tuple for any non-zero
git exit_code, which silently skips lockfile discovery; change it so only the
explicit "not a git repository" case returns an empty tuple, and for all other
non-zero exit_code paths raise an exception (e.g., RuntimeError) containing the
git output details so failures are surfaced. Locate the block that examines
exit_code and builds detail from (stderr or stdout). Keep the LOGGER.warning for
the "not a git repository" branch (return ()), but replace the other
LOGGER.warning branch so it raises a RuntimeError (or a domain-specific
exception if one exists in the module) with a message including detail and
workspace context instead of returning ().

In `@lading/commands/publish_preflight.py`:
- Around line 220-246: The current loop treats any non-success from
validate_lockfile_freshness as a stale lockfile; change
validate_lockfile_freshness (or its caller) to distinguish a genuine "locked
needs update" Cargo response from other cargo failures: have
validate_lockfile_freshness return a tri-state or (bool, optional_error) so the
caller can detect the specific Cargo stderr/message that indicates the lockfile
must be regenerated (e.g. the stderr text stating the lockfile needs updating)
and only append to stale_lockfiles in that case; if validate_lockfile_freshness
returns an actual cargo error (other stderr/exit), surface that error
immediately via LOGGER.error with cargo details and raise PublishPreflightError
rather than suggesting repair commands; update usages of
validate_lockfile_freshness, stale_lockfiles, runner_with_env, LOGGER and
PublishPreflightError accordingly.

In `@tests/unit/publish/test_command_helpers.py`:
- Around line 85-109: The test duplicates implementation logic; update
test_normalise_cmd_mox_command_forwards_non_cargo_commands to accept explicit
expected_program and expected_args instead of deriving them: change the
`@pytest.mark.parametrize` signature to ("command", "expected_program",
"expected_args") and supply pytest.param entries for each case (e.g.
("cargo","check") -> expected_program "cargo::check", expected_args [],
("cargo","test","--workspace") -> "cargo::test", ["--workspace"], and
("git","status","--porcelain") -> "git", ["status","--porcelain"]); then remove
the conditional block that reconstructs expectations and assert
rewritten_program == expected_program and rewritten_args == expected_args,
leaving normalise_cmd_mox_command as the function under test.

In `@tests/unit/publish/test_preflight_cargo_runner.py`:
- Around line 53-85: Update the docstring of _run_and_record_cargo_preflight to
explain the recording-runner pattern: describe that the helper injects a closure
named recording_runner into publish._run_cargo_preflight to capture subprocess
arguments instead of executing them, so tests can assert the constructed cargo
command (returned as a tuple) without spawning real processes; include a short
note that recorded is a list of commands and recording_runner appends the
intercepted command and returns a dummy success tuple.
- Around line 87-143: Add a parametrized case to
test_run_cargo_preflight_command_arguments that verifies composition of
test_excludes and unit_tests_only: create a _RunCargoPreflightCase using
publish._CargoPreflightOptions with extra_args=("--workspace","--all-targets"),
test_excludes=["slow-integration"], and unit_tests_only=True, and set
expected_tail to ("--exclude","slow-integration","--lib","--bins"); give the
case id "test_excludes_with_unit_tests_only" and append it to the existing param
list so the test asserts both options combine correctly.

In `@tests/unit/publish/test_preflight_lockfile_validation.py`:
- Around line 75-77: Update the two pytest.raises blocks that assert
PublishPreflightError in test_preflight_lockfile_validation to include explicit
message constraints via the match= parameter so the test only accepts the
intended failure text; locate the asserts that call
publish_preflight._validate_lockfile_freshness (the block using `with
pytest.raises(publish.PublishPreflightError) as excinfo:` around the
tmp_path/runner call) and the second similar block around lines 119-123, and
replace them to use `with pytest.raises(publish.PublishPreflightError,
match="...")` with a regex or exact substring matching the expected preflight
error message for lockfile freshness.

In `@tests/unit/test_cargo_metadata_loading.py`:
- Around line 68-86: Rename the boolean-capturing list recorded_echo_stdout in
test_load_cargo_metadata_suppresses_stdout_echo to a clearer name (e.g.,
echo_flags) and update all references inside the test and its nested runner
(where recorded_echo_stdout.append(echo_stdout) and the final assertion
recorded_echo_stdout == [False]) to the new name so the variable clearly
represents a list of echo_stdout booleans; the test function name, nested runner
signature, and call to load_cargo_metadata should remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f31ad427-c5ac-4f43-aeca-9f9bc893036a

📥 Commits

Reviewing files that changed from the base of the PR and between ad7fc83 and 527c7a8.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (35)
  • docs/developers-guide.md
  • docs/roadmap.md
  • docs/users-guide.md
  • lading/__init__.py
  • lading/cli.py
  • lading/commands/bump.py
  • lading/commands/lockfile.py
  • lading/commands/publish.py
  • lading/commands/publish_errors.py
  • lading/commands/publish_manifest.py
  • lading/commands/publish_plan.py
  • lading/commands/publish_preflight.py
  • lading/config.py
  • lading/exceptions.py
  • lading/workspace/metadata.py
  • lading/workspace/models.py
  • pyproject.toml
  • tests/bdd/features/cli.feature
  • tests/bdd/steps/metadata_fixtures.py
  • tests/bdd/steps/test_bump_steps.py
  • tests/bdd/steps/test_publish_given_steps.py
  • tests/bdd/steps/test_publish_infrastructure.py
  • tests/e2e/steps/test_e2e_steps.py
  • tests/unit/__snapshots__/test_bump_command_internals.ambr
  • tests/unit/publish/__snapshots__/test_preflight_lockfile_validation.ambr
  • tests/unit/publish/test_command_helpers.py
  • tests/unit/publish/test_preflight_cargo_runner.py
  • tests/unit/publish/test_preflight_checks.py
  • tests/unit/publish/test_preflight_lockfile_validation.py
  • tests/unit/publish/test_run_integration.py
  • tests/unit/test_bump_command_integration.py
  • tests/unit/test_bump_command_internals.py
  • tests/unit/test_cargo_metadata_loading.py
  • tests/unit/test_cli.py
  • tests/unit/test_lockfile.py

@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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lading/commands/lockfile.py`:
- Around line 55-68: The current handler returns an empty tuple for any non-zero
git exit_code, which silently skips lockfile discovery; change it so only the
explicit "not a git repository" case returns an empty tuple, and for all other
non-zero exit_code paths raise an exception (e.g., RuntimeError) containing the
git output details so failures are surfaced. Locate the block that examines
exit_code and builds detail from (stderr or stdout). Keep the LOGGER.warning for
the "not a git repository" branch (return ()), but replace the other
LOGGER.warning branch so it raises a RuntimeError (or a domain-specific
exception if one exists in the module) with a message including detail and
workspace context instead of returning ().

In `@lading/commands/publish_preflight.py`:
- Around line 220-246: The current loop treats any non-success from
validate_lockfile_freshness as a stale lockfile; change
validate_lockfile_freshness (or its caller) to distinguish a genuine "locked
needs update" Cargo response from other cargo failures: have
validate_lockfile_freshness return a tri-state or (bool, optional_error) so the
caller can detect the specific Cargo stderr/message that indicates the lockfile
must be regenerated (e.g. the stderr text stating the lockfile needs updating)
and only append to stale_lockfiles in that case; if validate_lockfile_freshness
returns an actual cargo error (other stderr/exit), surface that error
immediately via LOGGER.error with cargo details and raise PublishPreflightError
rather than suggesting repair commands; update usages of
validate_lockfile_freshness, stale_lockfiles, runner_with_env, LOGGER and
PublishPreflightError accordingly.

In `@tests/unit/publish/test_command_helpers.py`:
- Around line 85-109: The test duplicates implementation logic; update
test_normalise_cmd_mox_command_forwards_non_cargo_commands to accept explicit
expected_program and expected_args instead of deriving them: change the
`@pytest.mark.parametrize` signature to ("command", "expected_program",
"expected_args") and supply pytest.param entries for each case (e.g.
("cargo","check") -> expected_program "cargo::check", expected_args [],
("cargo","test","--workspace") -> "cargo::test", ["--workspace"], and
("git","status","--porcelain") -> "git", ["status","--porcelain"]); then remove
the conditional block that reconstructs expectations and assert
rewritten_program == expected_program and rewritten_args == expected_args,
leaving normalise_cmd_mox_command as the function under test.

In `@tests/unit/publish/test_preflight_cargo_runner.py`:
- Around line 53-85: Update the docstring of _run_and_record_cargo_preflight to
explain the recording-runner pattern: describe that the helper injects a closure
named recording_runner into publish._run_cargo_preflight to capture subprocess
arguments instead of executing them, so tests can assert the constructed cargo
command (returned as a tuple) without spawning real processes; include a short
note that recorded is a list of commands and recording_runner appends the
intercepted command and returns a dummy success tuple.
- Around line 87-143: Add a parametrized case to
test_run_cargo_preflight_command_arguments that verifies composition of
test_excludes and unit_tests_only: create a _RunCargoPreflightCase using
publish._CargoPreflightOptions with extra_args=("--workspace","--all-targets"),
test_excludes=["slow-integration"], and unit_tests_only=True, and set
expected_tail to ("--exclude","slow-integration","--lib","--bins"); give the
case id "test_excludes_with_unit_tests_only" and append it to the existing param
list so the test asserts both options combine correctly.

In `@tests/unit/publish/test_preflight_lockfile_validation.py`:
- Around line 75-77: Update the two pytest.raises blocks that assert
PublishPreflightError in test_preflight_lockfile_validation to include explicit
message constraints via the match= parameter so the test only accepts the
intended failure text; locate the asserts that call
publish_preflight._validate_lockfile_freshness (the block using `with
pytest.raises(publish.PublishPreflightError) as excinfo:` around the
tmp_path/runner call) and the second similar block around lines 119-123, and
replace them to use `with pytest.raises(publish.PublishPreflightError,
match="...")` with a regex or exact substring matching the expected preflight
error message for lockfile freshness.

In `@tests/unit/test_cargo_metadata_loading.py`:
- Around line 68-86: Rename the boolean-capturing list recorded_echo_stdout in
test_load_cargo_metadata_suppresses_stdout_echo to a clearer name (e.g.,
echo_flags) and update all references inside the test and its nested runner
(where recorded_echo_stdout.append(echo_stdout) and the final assertion
recorded_echo_stdout == [False]) to the new name so the variable clearly
represents a list of echo_stdout booleans; the test function name, nested runner
signature, and call to load_cargo_metadata should remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f31ad427-c5ac-4f43-aeca-9f9bc893036a

📥 Commits

Reviewing files that changed from the base of the PR and between ad7fc83 and 527c7a8.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (35)
  • docs/developers-guide.md
  • docs/roadmap.md
  • docs/users-guide.md
  • lading/__init__.py
  • lading/cli.py
  • lading/commands/bump.py
  • lading/commands/lockfile.py
  • lading/commands/publish.py
  • lading/commands/publish_errors.py
  • lading/commands/publish_manifest.py
  • lading/commands/publish_plan.py
  • lading/commands/publish_preflight.py
  • lading/config.py
  • lading/exceptions.py
  • lading/workspace/metadata.py
  • lading/workspace/models.py
  • pyproject.toml
  • tests/bdd/features/cli.feature
  • tests/bdd/steps/metadata_fixtures.py
  • tests/bdd/steps/test_bump_steps.py
  • tests/bdd/steps/test_publish_given_steps.py
  • tests/bdd/steps/test_publish_infrastructure.py
  • tests/e2e/steps/test_e2e_steps.py
  • tests/unit/__snapshots__/test_bump_command_internals.ambr
  • tests/unit/publish/__snapshots__/test_preflight_lockfile_validation.ambr
  • tests/unit/publish/test_command_helpers.py
  • tests/unit/publish/test_preflight_cargo_runner.py
  • tests/unit/publish/test_preflight_checks.py
  • tests/unit/publish/test_preflight_lockfile_validation.py
  • tests/unit/publish/test_run_integration.py
  • tests/unit/test_bump_command_integration.py
  • tests/unit/test_bump_command_internals.py
  • tests/unit/test_cargo_metadata_loading.py
  • tests/unit/test_cli.py
  • tests/unit/test_lockfile.py
🛑 Comments failed to post (7)
lading/commands/lockfile.py (1)

55-68: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast on unexpected git ls-files failures.

At Line 55, stop returning an empty tuple for all non-zero exits. Preserve the empty-tuple path only for the explicit “not a git repository” case, and raise a domain error for all other failures so lockfile refresh/validation cannot be silently skipped.

Patch sketch
+class LockfileDiscoveryError(LadingError):
+    """Raised when tracked Cargo.lock files cannot be discovered."""
+
 def _handle_git_ls_files_failure(
@@
 ) -> tuple[Path, ...] | None:
@@
     if "not a git repository" in detail.lower():
         LOGGER.warning(
             "Skipping Cargo.lock discovery because %s is not a git repository",
             workspace_root,
         )
-    else:
-        LOGGER.warning(
-            "Skipping Cargo.lock discovery after git ls-files failed: %s", detail
-        )
-    return ()
+        return ()
+
+    message = "git ls-files failed during Cargo.lock discovery"
+    if detail:
+        message = f"{message}: {detail}"
+    LOGGER.error(message)
+    raise LockfileDiscoveryError(message)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lading/commands/lockfile.py` around lines 55 - 68, The current handler
returns an empty tuple for any non-zero git exit_code, which silently skips
lockfile discovery; change it so only the explicit "not a git repository" case
returns an empty tuple, and for all other non-zero exit_code paths raise an
exception (e.g., RuntimeError) containing the git output details so failures are
surfaced. Locate the block that examines exit_code and builds detail from
(stderr or stdout). Keep the LOGGER.warning for the "not a git repository"
branch (return ()), but replace the other LOGGER.warning branch so it raises a
RuntimeError (or a domain-specific exception if one exists in the module) with a
message including detail and workspace context instead of returning ().
lading/commands/publish_preflight.py (1)

220-246: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Differentiate stale lockfiles from generic cargo metadata failures.

Line 222 treats every non-zero cargo metadata --locked result as a stale lockfile, then Lines 229-242 emit lockfile-repair commands. Preserve non-lockfile failures as direct preflight errors with Cargo detail, and only emit stale-lock remediation when Cargo reports the --locked lockfile-update condition.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lading/commands/publish_preflight.py` around lines 220 - 246, The current
loop treats any non-success from validate_lockfile_freshness as a stale
lockfile; change validate_lockfile_freshness (or its caller) to distinguish a
genuine "locked needs update" Cargo response from other cargo failures: have
validate_lockfile_freshness return a tri-state or (bool, optional_error) so the
caller can detect the specific Cargo stderr/message that indicates the lockfile
must be regenerated (e.g. the stderr text stating the lockfile needs updating)
and only append to stale_lockfiles in that case; if validate_lockfile_freshness
returns an actual cargo error (other stderr/exit), surface that error
immediately via LOGGER.error with cargo details and raise PublishPreflightError
rather than suggesting repair commands; update usages of
validate_lockfile_freshness, stale_lockfiles, runner_with_env, LOGGER and
PublishPreflightError accordingly.
tests/unit/publish/test_command_helpers.py (1)

85-109: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Simplify the parametrized test by removing redundant conditional logic.

Lines 101-108 reconstruct the expected output by re-implementing the same transformation logic tested in normalise_cmd_mox_command. This duplicates the implementation rather than verifying it against known outputs.

Replace the conditional block with explicit expected values in each parametrize case:

 `@pytest.mark.parametrize`(
-    "command",
+    ("command", "expected_program", "expected_args"),
     [
-        ("cargo", "check"),
-        ("cargo", "test", "--workspace"),
-        ("git", "status", "--porcelain"),
+        pytest.param(
+            ("cargo", "check"),
+            "cargo::check",
+            [],
+            id="cargo_check",
+        ),
+        pytest.param(
+            ("cargo", "test", "--workspace"),
+            "cargo::test",
+            ["--workspace"],
+            id="cargo_test_with_args",
+        ),
+        pytest.param(
+            ("git", "status", "--porcelain"),
+            "git",
+            ["status", "--porcelain"],
+            id="git_command",
+        ),
     ],
 )
 def test_normalise_cmd_mox_command_forwards_non_cargo_commands(
-    command: tuple[str, ...],
+    command: tuple[str, ...],
+    expected_program: str,
+    expected_args: list[str],
 ) -> None:
     """cmd-mox normalisation preserves non-cargo commands and arguments."""
     program, args = command[0], tuple(command[1:])
 
     rewritten_program, rewritten_args = normalise_cmd_mox_command(program, args)
 
-    if program == "cargo" and args:
-        expected_program = f"cargo::{args[0]}"
-        expected_args = list(args[1:])
-    else:
-        expected_program = program
-        expected_args = list(args)
-
     assert rewritten_program == expected_program
     assert rewritten_args == expected_args

This separates test data from test logic, making failures easier to diagnose and extending the test matrix simpler.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/publish/test_command_helpers.py` around lines 85 - 109, The test
duplicates implementation logic; update
test_normalise_cmd_mox_command_forwards_non_cargo_commands to accept explicit
expected_program and expected_args instead of deriving them: change the
`@pytest.mark.parametrize` signature to ("command", "expected_program",
"expected_args") and supply pytest.param entries for each case (e.g.
("cargo","check") -> expected_program "cargo::check", expected_args [],
("cargo","test","--workspace") -> "cargo::test", ["--workspace"], and
("git","status","--porcelain") -> "git", ["status","--porcelain"]); then remove
the conditional block that reconstructs expectations and assert
rewritten_program == expected_program and rewritten_args == expected_args,
leaving normalise_cmd_mox_command as the function under test.
tests/unit/publish/test_preflight_cargo_runner.py (2)

53-85: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Document the recording runner pattern in the helper docstring.

The _run_and_record_cargo_preflight helper uses a closure to record commands, but the docstring only describes the return value. Expand the docstring to explain the recording pattern so future test authors understand why this abstraction exists.

📝 Proposed docstring enhancement
 def _run_and_record_cargo_preflight(
     workspace_root: Path,
     subcommand: typ.Literal["check", "test"],
     options: publish._CargoPreflightOptions,
 ) -> tuple[str, ...]:
-    """Run cargo preflight with a recording runner and return the command.
+    """Run cargo preflight with a recording runner and return the command.
+
+    This helper isolates command construction from execution by injecting a
+    recording runner that captures the exact arguments passed to the subprocess
+    layer. It allows tests to verify option-to-argument transformations without
+    spawning real processes.
 
     Returns
     -------
         The recorded cargo command as a tuple of strings.
 
     """
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/publish/test_preflight_cargo_runner.py` around lines 53 - 85,
Update the docstring of _run_and_record_cargo_preflight to explain the
recording-runner pattern: describe that the helper injects a closure named
recording_runner into publish._run_cargo_preflight to capture subprocess
arguments instead of executing them, so tests can assert the constructed cargo
command (returned as a tuple) without spawning real processes; include a short
note that recorded is a list of commands and recording_runner appends the
intercepted command and returns a dummy success tuple.

87-143: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add parametrize case for combined test_excludes and unit_tests_only.

The test matrix covers test_excludes and unit_tests_only independently, but not their combination. Add a case verifying that both options compose correctly:

➕ Proposed additional test case
         pytest.param(
             _RunCargoPreflightCase(
                 options=publish._CargoPreflightOptions(
                     extra_args=("--workspace", "--all-targets"),
                     unit_tests_only=False,
                 ),
                 expected_tail=(),
             ),
             id="unit_tests_only_false",
         ),
+        pytest.param(
+            _RunCargoPreflightCase(
+                options=publish._CargoPreflightOptions(
+                    extra_args=("--workspace", "--all-targets"),
+                    test_excludes=["slow-integration"],
+                    unit_tests_only=True,
+                ),
+                expected_tail=("--exclude", "slow-integration", "--lib", "--bins"),
+            ),
+            id="test_excludes_with_unit_tests_only",
+        ),
     ],
 )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/publish/test_preflight_cargo_runner.py` around lines 87 - 143, Add
a parametrized case to test_run_cargo_preflight_command_arguments that verifies
composition of test_excludes and unit_tests_only: create a
_RunCargoPreflightCase using publish._CargoPreflightOptions with
extra_args=("--workspace","--all-targets"), test_excludes=["slow-integration"],
and unit_tests_only=True, and set expected_tail to
("--exclude","slow-integration","--lib","--bins"); give the case id
"test_excludes_with_unit_tests_only" and append it to the existing param list so
the test asserts both options combine correctly.
tests/unit/publish/test_preflight_lockfile_validation.py (1)

75-77: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add message constraints to exception assertions.

Assert the failure contract directly with match=... in both pytest.raises blocks, so tests fail only for the intended preflight error text instead of any PublishPreflightError.

Patch
-    with pytest.raises(publish.PublishPreflightError) as excinfo:
+    with pytest.raises(
+        publish.PublishPreflightError,
+        match=r"Tracked Cargo\.lock files are stale",
+    ) as excinfo:
@@
-    with pytest.raises(publish.PublishPreflightError) as excinfo:
+    with pytest.raises(
+        publish.PublishPreflightError,
+        match=r"cargo generate-lockfile --manifest-path",
+    ) as excinfo:

As per coding guidelines, “In tests, use specific exception types and message constraints with pytest.raises(SpecificException, match=pattern) instead of generic exceptions (B017)”.

Also applies to: 119-123

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/publish/test_preflight_lockfile_validation.py` around lines 75 -
77, Update the two pytest.raises blocks that assert PublishPreflightError in
test_preflight_lockfile_validation to include explicit message constraints via
the match= parameter so the test only accepts the intended failure text; locate
the asserts that call publish_preflight._validate_lockfile_freshness (the block
using `with pytest.raises(publish.PublishPreflightError) as excinfo:` around the
tmp_path/runner call) and the second similar block around lines 119-123, and
replace them to use `with pytest.raises(publish.PublishPreflightError,
match="...")` with a regex or exact substring matching the expected preflight
error message for lockfile freshness.

Source: Coding guidelines

tests/unit/test_cargo_metadata_loading.py (1)

68-86: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Use a more descriptive variable name for the echo_stdout capture.

The variable recorded_echo_stdout collects boolean values, but the name suggests it might contain stdout content. Rename it to clarify intent:

♻️ Proposed refactor
 def test_load_cargo_metadata_suppresses_stdout_echo(tmp_path: Path) -> None:
     """Cargo metadata captures JSON without echoing it to the terminal."""
-    recorded_echo_stdout: list[bool] = []
+    echo_flags: list[bool] = []
 
     def runner(
         command: tuple[str, ...],
         *,
         cwd: Path | None = None,
         env: cabc.Mapping[str, str] | None = None,
         echo_stdout: bool = True,
     ) -> tuple[int, str, str]:
         del command, cwd, env
-        recorded_echo_stdout.append(echo_stdout)
+        echo_flags.append(echo_stdout)
         return 0, json.dumps(_METADATA_PAYLOAD), ""
 
     result = load_cargo_metadata(tmp_path, runner=runner)
 
     assert result == _METADATA_PAYLOAD
-    assert recorded_echo_stdout == [False]
+    assert echo_flags == [False]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_cargo_metadata_loading.py` around lines 68 - 86, Rename the
boolean-capturing list recorded_echo_stdout in
test_load_cargo_metadata_suppresses_stdout_echo to a clearer name (e.g.,
echo_flags) and update all references inside the test and its nested runner
(where recorded_echo_stdout.append(echo_stdout) and the final assertion
recorded_echo_stdout == [False]) to the new name so the variable clearly
represents a list of echo_stdout booleans; the test function name, nested runner
signature, and call to load_cargo_metadata should remain unchanged.

@leynos
leynos merged commit d917151 into main Jun 8, 2026
5 checks passed
@leynos
leynos deleted the issue-61-refresh-stale-nested-cargo-lock-files-before-publish-preflight branch June 8, 2026 19:47
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.

Refresh or diagnose stale nested Cargo.lock files before publish preflight

1 participant