Skip to content

Add rope actuator plugin with runtime wiring, e2e tests, and docs#59

Merged
leynos merged 8 commits intomainfrom
plugin-rope-python-refactor-wuecqo
Feb 17, 2026
Merged

Add rope actuator plugin with runtime wiring, e2e tests, and docs#59
leynos merged 8 commits intomainfrom
plugin-rope-python-refactor-wuecqo

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Feb 12, 2026

Summary

  • Adds a dedicated rope actuator plugin crate (crates/weaver-plugin-rope) with a one-shot rope-based refactor operation and a Python rope adapter boundary, wired into the weaver runtime.
  • Wires plugin execution through the daemon runtime (weaverd) so rope-based refactoring results flow through the existing apply-patch Double-Lock path.
  • Introduces end-to-end tests and CLI ergonomics snapshots, including a fake daemon for deterministic tests.
  • Updates the ExecPlan and alignment with governance/docs to reflect shipped rope plugin and runtime wiring.
  • This PR contains substantial code changes beyond documentation, not merely planning.

Changes

Documentation

  • Added ExecPlan: docs/execplans/3-1-1a-plugin-for-rope.md (living document outlining purpose, constraints, risks, progress, decisions, and a Stage A–G plan for rope-based actuator integration).
  • Updated roadmap and design/docs references to reflect shipped rope plugin and runtime wiring (docs/weaver-design.md, docs/users-guide.md, docs/roadmap.md).

Code

  • New crate: crates/weaver-plugin-rope with Cargo.toml and source files (lib.rs, main.rs).
    • Provides a RopeAdapter trait and a PythonRopeAdapter implementing rope-based rename refactor via a generated Python script executed in a temporary workspace.
    • Exposes run and run_with_adapter functions to dispatch a single PluginRequest to a RopeAdapter and emit a PluginResponse.
    • Handles validation of file paths, sandboxed workspace creation, and error mapping for robust CI-friendly testing.
  • End-to-end wiring in weaverd:
    • crates/weaverd/src/dispatch/router.rs updated to instantiate and use a RefactorRuntime via DomainRouter for the refactor domain, wiring dependencies for rope-based execution.
    • Added SandboxRefactorRuntime and associated runtime wiring (default_runtime, resolve_rope_plugin_path) to enable plugin discovery and execution.
    • DomainRouter::new now wires a runtime so act refactor can execute a plugin through the runtime.
  • Binary entry for rope plugin:
    • crates/weaver-plugin-rope/src/main.rs and crates/weaver-plugin-rope/src/lib.rs implement the plugin protocol and adapter usage.
  • Tests
    • New behaviour tests: crates/weaver-plugin-rope/src/tests/behaviour.rs and crates/weaver-plugin-rope/src/tests/mod.rs.
    • Feature tests for rope plugin behavior: crates/weaver-plugin-rope/tests/features/rope_plugin.feature.
    • End-to-end CLI ergonomics snapshots added: crates/weaver-e2e/tests/refactor_rope_cli_snapshots.rs and corresponding snapshots.
    • E2E test dependencies updated accordingly in crates/weaver-e2e/Cargo.toml.
  • Additional tests and scaffolding to support deterministic rope adapter behaviour and plugin request handling in unit/BDD tests.

Rationale

  • Establishes a dedicated rope plugin executable crate to keep rope/refactoring logic separate from the daemon, enabling swap-in/DI-based testing and reliable CI without requiring a system-wide rope installation.
  • Reuses existing Double-Lock safe apply-patch flow for patch application, avoiding duplication of critical safety logic.
  • Introduces a Bulldog-friendly design: one-shot rope operation (rename) with explicit arguments, and deterministic adapter boundaries for tests.

Plan alignment

  • ExecPlan document (docs/execplans/3-1-1a-plugin-for-rope.md) reflects Stage A–G plan and the rope actuator rollout.
  • docs/weaver-design.md, docs/users-guide.md, and docs/roadmap.md updated to reflect shipped rope plugin and runtime wiring.

Revision note

  • Initial draft created to map Phase 3 -> first actuator rope plugin implementation and runtime integration.

📎 Task: https://www.devboxer.com/task/abbb65ed-f29f-42da-b73f-6d1aa2410d11

📎 Task: https://www.devboxer.com/task/33450ba1-c1f7-4278-8ebd-89bbced1a8c0

Add a detailed living ExecPlan document for implementing the first actuator plugin using rope for Python refactoring. This plan outlines purpose, constraints, tolerances, risks, progress, decisions, and detailed stages for development, testing, and documentation updates related to the rope plugin integration in the refactor action.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Feb 12, 2026

Reviewer's Guide

Implements the first rope-based refactor actuator as a standalone plugin crate and wires act refactor through a sandboxed plugin runtime into the existing Double-Lock apply-patch pipeline, with tests and docs covering the new flow.

Sequence diagram for act refactor rope flow through Double-Lock

sequenceDiagram
    actor User
    participant WeaverCLI as weaver_cli
    participant Weaverd as weaverd_DomainRouter
    participant Refactor as act_refactor_handler
    participant Runtime as RefactorPluginRuntime
    participant Runner as PluginRunner
    participant RopePlugin as weaver_plugin_rope
    participant RopeAdapter as PythonRopeAdapter
    participant ApplyPatch as act_apply_patch_handler
    participant DoubleLock as DoubleLock_harness

    User->>WeaverCLI: run act refactor --provider rope --refactoring rename --file notes.py offset new_name
    WeaverCLI->>Weaverd: JSONL CommandRequest (domain act, operation refactor)

    Weaverd->>Weaverd: DomainRouter::route
    Weaverd->>Refactor: handle(CommandRequest, RefactorDependencies)

    Refactor->>Refactor: parse_refactor_args, resolve_file, read file content
    Refactor->>Refactor: build PluginRequest (FilePayload, arguments)

    Refactor->>Runtime: execute(provider, PluginRequest)
    activate Runtime
    Runtime->>Runner: execute(provider, PluginRequest)
    activate Runner

    Runner->>RopePlugin: spawn process with SandboxExecutor
    activate RopePlugin
    RopePlugin->>RopePlugin: run(read PluginRequest from stdin)
    RopePlugin->>RopeAdapter: rename(FilePayload, offset, new_name)
    activate RopeAdapter
    RopeAdapter->>RopeAdapter: create TempDir, write workspace file
    RopeAdapter->>RopeAdapter: invoke python3 -c PYTHON_RENAME_SCRIPT
    RopeAdapter-->>RopePlugin: modified file content or RopeAdapterError
    deactivate RopeAdapter

    RopePlugin->>RopePlugin: build PluginResponse
    RopePlugin-->>Runner: PluginResponse (stdout JSONL)
    deactivate RopePlugin

    Runner-->>Runtime: PluginResponse
    deactivate Runner
    Runtime-->>Refactor: PluginResponse
    deactivate Runtime

    Refactor->>Refactor: handle_plugin_response
    alt PluginOutput Diff
        Refactor->>ApplyPatch: forward_diff_to_apply_patch(patch)
        ApplyPatch->>DoubleLock: validate syntax and semantics
        DoubleLock-->>ApplyPatch: ok
        ApplyPatch-->>Refactor: DispatchResult status 0
    else PluginOutput not Diff or failure
        Refactor-->>WeaverCLI: status 1 with diagnostics
    end

    Refactor-->>Weaverd: DispatchResult
    Weaverd-->>WeaverCLI: CLI exit status and output
    WeaverCLI-->>User: show result
Loading

Class diagram for rope plugin crate and refactor runtime wiring

classDiagram
    class RopeAdapter {
        <<trait>>
        +rename(file FilePayload, offset usize, new_name &str) Result String,RopeAdapterError
    }

    class PythonRopeAdapter {
        +PythonRopeAdapter()
        +rename(file FilePayload, offset usize, new_name &str) Result String,RopeAdapterError
    }

    RopeAdapter <|.. PythonRopeAdapter

    class RopeAdapterError {
        <<enum>>
        WorkspaceCreate
        WorkspaceWrite
        Spawn
        EngineFailed
        InvalidOutput
        InvalidPath
    }

    class PluginDispatchError {
        <<enum>>
        Write
        Serialize
    }

    class PluginRequest {
    }

    class PluginResponse {
        +success(output PluginOutput) PluginResponse
        +failure(diagnostics Vec~PluginDiagnostic~) PluginResponse
    }

    class PluginOutput {
        <<enum>>
        Diff
        Analysis
        Empty
    }

    class weaver_plugin_rope_lib {
        +run(stdin BufRead, stdout Write) Result void,PluginDispatchError
        +run_with_adapter(stdin BufRead, stdout Write, adapter RopeAdapter) Result void,PluginDispatchError
        -read_request(stdin BufRead) Result PluginRequest,String
        -execute_request(adapter RopeAdapter, request PluginRequest) PluginResponse
        -execute_rename(adapter RopeAdapter, request PluginRequest) PluginResponse
        -parse_rename_arguments(arguments HashMap~String,Value~) Result (usize,String),String
        -build_search_replace_patch(path Path, original &str, modified &str) String
        -validate_relative_path(path Path) Result void,RopeAdapterError
    }

    weaver_plugin_rope_lib ..> RopeAdapter : uses
    weaver_plugin_rope_lib ..> PluginRequest : uses
    weaver_plugin_rope_lib ..> PluginResponse : uses

    class RefactorPluginRuntime {
        <<trait>>
        +execute(provider &str, request &PluginRequest) Result PluginResponse,PluginError
    }

    class SandboxRefactorRuntime {
        -runner Option~PluginRunner~
        -startup_error Option~String~
        +from_environment() SandboxRefactorRuntime
        +execute(provider &str, request &PluginRequest) Result PluginResponse,PluginError
    }

    RefactorPluginRuntime <|.. SandboxRefactorRuntime

    class RefactorDependencies {
        +workspace_root &Path
        +runtime &RefactorPluginRuntime
        +new(workspace_root &Path, runtime &RefactorPluginRuntime) RefactorDependencies
    }

    class DomainRouter {
        -workspace_root PathBuf
        -refactor_runtime Arc~RefactorPluginRuntime~
        +new(workspace_root PathBuf) DomainRouter
        +route_act(request CommandRequest, writer ResponseWriter, backends FusionBackends) DispatchResult
    }

    class RefactorHandler {
        +handle(request CommandRequest, writer ResponseWriter, backends FusionBackends, dependencies RefactorDependencies) DispatchResult,DispatchError
        +handle_plugin_response(response PluginResponse, writer ResponseWriter, backends FusionBackends, workspace_root &Path) DispatchResult,DispatchError
        +forward_diff_to_apply_patch(patch &str, writer ResponseWriter, backends FusionBackends, workspace_root &Path) DispatchResult,DispatchError
    }

    DomainRouter ..> RefactorPluginRuntime : holds_arc
    DomainRouter ..> RefactorDependencies : constructs
    RefactorHandler ..> RefactorDependencies : uses
    RefactorHandler ..> RefactorPluginRuntime : execute
    SandboxRefactorRuntime ..> PluginRunner : wraps
    SandboxRefactorRuntime ..> PluginRegistry : registers_manifest
    SandboxRefactorRuntime ..> SandboxExecutor : uses
    SandboxRefactorRuntime ..> PluginManifest : builds
    SandboxRefactorRuntime ..> PluginMetadata : builds
    RefactorHandler ..> PluginRequest : builds
    RefactorHandler ..> PluginResponse : consumes
    RefactorHandler ..> PluginOutput : matches
Loading

File-Level Changes

Change Details Files
Wire act refactor to execute refactor plugins via a runtime and forward successful diffs into the existing act apply-patch Double-Lock path.
  • Introduce RefactorPluginRuntime trait, RefactorDependencies bundle, and SandboxRefactorRuntime implementing plugin execution using PluginRunner<SandboxExecutor>.
  • Resolve the rope plugin executable path from WEAVER_ROPE_PLUGIN_PATH or a default, construct a plugin manifest/registry, and expose default_runtime for daemon use.
  • Update handle in the refactor dispatcher to build a PluginRequest with logical file paths, invoke the injected runtime, and handle PluginResponse via handle_plugin_response and forward_diff_to_apply_patch.
  • Extend the ACT domain router to hold a shared refactor runtime and pass it through to the refactor handler instead of a bare workspace path, and add targeted docs/user-guide updates for the now-functional act refactor and rope registration.
crates/weaverd/src/dispatch/act/refactor/mod.rs
crates/weaverd/src/dispatch/router.rs
docs/users-guide.md
docs/weaver-design.md
Add the weaver-plugin-rope crate implementing a one-shot rope-backed rename refactor plugin with a Python adapter and JSONL protocol handling.
  • Define a RopeAdapter trait and PythonRopeAdapter that validates relative paths, materializes file content into a temporary workspace, and runs an embedded Python rope script to perform a rename and emit updated file contents.
  • Implement protocol orchestration (run_with_adapter/run): read a single PluginRequest JSON line from stdin, parse/validate it, dispatch rename via the adapter, and write a single PluginResponse JSON line to stdout mapping errors into diagnostics.
  • Add helper utilities to parse rename arguments, validate paths, write workspace files, construct a search/replace-style diff patch, and normalize paths for diff headers; define rich error types PluginDispatchError and RopeAdapterError.
  • Provide a binary entrypoint wiring OS stdin/stdout to run and exiting non-zero on dispatch failures, and register the crate in the workspace with appropriate dependencies and lints.
crates/weaver-plugin-rope/Cargo.toml
crates/weaver-plugin-rope/src/lib.rs
crates/weaver-plugin-rope/src/main.rs
Introduce unit, behavioural, and end-to-end tests to cover the rope plugin and the refactor dispatch/runtime wiring, plus CLI ergonomics snapshots.
  • Add rope plugin unit tests and BDD scenarios using mock adapters to cover successful rename, missing arguments, unsupported operations, unchanged output, and adapter failures.
  • Add refactor handler unit tests and BDD feature scenarios that exercise runtime error handling, non-diff outputs, diff application via act apply-patch, argument validation, and plugin diff validation (valid vs malformed).
  • Create e2e CLI snapshot tests using a fake TCP daemon, assert_cmd, and insta to capture weaver act refactor --provider rope usage in isolation and in an observe + jq + act refactor pipeline, along with the corresponding snapshot files.
  • Wire up dev-dependencies (assert_cmd, rstest, rstest-bdd) for the new tests and basic helper infrastructure (fake daemon, transcript capture).
crates/weaver-plugin-rope/src/tests/mod.rs
crates/weaver-plugin-rope/src/tests/behaviour.rs
crates/weaver-plugin-rope/tests/features/rope_plugin.feature
crates/weaverd/src/dispatch/act/refactor/tests.rs
crates/weaverd/src/dispatch/act/refactor/behaviour.rs
crates/weaverd/tests/features/refactor.feature
crates/weaver-e2e/Cargo.toml
crates/weaver-e2e/tests/refactor_rope_cli_snapshots.rs
crates/weaver-e2e/tests/snapshots/refactor_rope_cli_snapshots__refactor_actuator_isolation.snap
crates/weaver-e2e/tests/snapshots/refactor_rope_cli_snapshots__refactor_pipeline_observe_jq.snap
Document the rope actuator exec plan, design decisions, user-facing behaviour, and roadmap status.
  • Add an ExecPlan document describing constraints, risks, staged plan (A–G), validation criteria, and artifacts for implementing the rope actuator plugin and wiring it through the Double-Lock flow.
  • Update the design doc with Phase 3.1.1a implementation decisions for weaver-plugin-rope, default runtime registration, one-shot rename semantics, and CLI ergonomics testing.
  • Update the users guide to describe the now-working act refactor command, the rope provider’s supported operation and arguments, environment override for the plugin path, and plugin registry details.
  • Mark the roadmap item for the rope plugin as completed and add the new crate to the workspace member list and lockfile.
docs/execplans/3-1-1a-plugin-for-rope.md
docs/weaver-design.md
docs/users-guide.md
docs/roadmap.md
Cargo.toml
Cargo.lock

Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Add a rope-backed actuator plugin crate and CLI, wire a refactor plugin runtime into the daemon via dependency injection, and add unit, behavioural and e2e tests plus feature specs. Update workspace manifests and documentation to register and describe the new plugin.

Changes

Cohort / File(s) Summary
Plugin Rope Crate
crates/weaver-plugin-rope/Cargo.toml, crates/weaver-plugin-rope/src/lib.rs, crates/weaver-plugin-rope/src/main.rs
Add new actuator crate and binary. Implement RopeAdapter trait, PythonRopeAdapter, JSONL request/response handling, workspace materialisation, rename operation, diff generation, error enums, and run/run_with_adapter entrypoints.
Plugin Tests
crates/weaver-plugin-rope/src/tests/mod.rs, crates/weaver-plugin-rope/src/tests/behaviour.rs, crates/weaver-plugin-rope/tests/features/rope_plugin.feature
Add unit and BDD tests using Mock/Behaviour adapters and feature scenarios covering success, missing args, unsupported ops, adapter failures and unchanged-output handling.
E2E Test Infrastructure
crates/weaver-e2e/Cargo.toml, crates/weaver-e2e/tests/refactor_rope_cli_snapshots.rs
Add assert_cmd dev-dependency and e2e snapshot tests using a fake TCP daemon to capture requests and validate CLI streaming, pipeline ergonomics and snapshots.
Daemon Runtime Integration
crates/weaverd/src/dispatch/act/refactor/mod.rs, crates/weaverd/src/dispatch/act/refactor/behaviour.rs
Introduce RefactorPluginRuntime trait, SandboxRefactorRuntime and NoopRefactorRuntime, add RefactorContext, runtime-driven execution flow, plugin path resolution and helpers to handle PluginResponse and forward diffs to apply-patch.
Refactor Handler Tests
crates/weaverd/src/dispatch/act/refactor/tests.rs, crates/weaverd/src/dispatch/act/refactor/behaviour.rs, crates/weaverd/tests/features/refactor.feature
Add unit and behavioural tests for refactor handler with MockRuntime, covering success, runtime errors, malformed diffs, path resolution and file state/assertions.
Router Wiring
crates/weaverd/src/dispatch/router.rs
Add refactor_runtime: Arc<dyn RefactorPluginRuntime + Send + Sync> to DomainRouter, initialise default runtime, add DomainRouter::with_runtime for tests, and pass runtime into refactor handler via RefactorContext.
Workspace & Manifests
Cargo.toml, crates/weaver-plugin-rope/Cargo.toml, crates/weaverd/Cargo.toml
Register crates/weaver-plugin-rope in workspace members; add crate manifest with dependencies and dev-dependencies; add mockall dev-dependency to crates/weaverd.
Documentation & Exec Plan
docs/execplans/3-1-1a-plugin-for-rope.md, docs/weaver-design.md, docs/users-guide.md, docs/roadmap.md
Add execution plan and design notes for rope actuator, document CLI usage, plugin registry/startup behaviour and override path handling; mark roadmap items complete and update user guide with rope-specific constraints.

Sequence Diagrams

sequenceDiagram
    participant Client as CLI Client
    participant Daemon as Weaver Daemon
    participant Runtime as Refactor Runtime
    participant Plugin as Rope Plugin Process
    participant FS as File System

    Client->>Daemon: act refactor (provider=rope, file, args)
    activate Daemon

    Daemon->>Runtime: execute(provider, PluginRequest)
    activate Runtime

    Runtime->>Plugin: spawn executable with JSONL on stdin
    activate Plugin

    Plugin->>FS: create temp workspace and write file
    Plugin->>Plugin: run Python rope engine (rename)
    Plugin->>Plugin: compute diff / diagnostics
    Plugin-->>Runtime: stream JSONL PluginResponse
    deactivate Plugin

    Runtime-->>Daemon: return PluginResponse (Diff or Diagnostics)
    deactivate Runtime

    Daemon->>Daemon: handle_plugin_response
    alt Diff
        Daemon->>Daemon: forward_diff_to_apply_patch
        Daemon->>FS: apply patch to workspace files
    else Diagnostics/Failure
        Daemon->>Daemon: emit failure diagnostics, do not mutate FS
    end

    Daemon-->>Client: stream output and exit status
    deactivate Daemon
Loading
sequenceDiagram
    participant Test as E2E Test
    participant FakeDaemon as Fake Daemon Server
    participant CLI as Weaver CLI
    participant Observe as observe (jq pipe)

    Test->>FakeDaemon: start listening on TCP port
    activate FakeDaemon

    Test->>CLI: run refactor pipeline (observe | jq)
    activate CLI

    CLI->>FakeDaemon: send request
    FakeDaemon-->>CLI: stream JSONL responses
    CLI->>Observe: pipe output to jq
    Observe-->>CLI: transformed output
    CLI-->>Test: capture stdout, stderr, exit status
    deactivate CLI

    Test->>Test: assemble Transcript (command, outputs, requests)
    Test->>Test: snapshot with insta
    deactivate FakeDaemon
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Poem

🪢 A rope of Python threads now weaves through weaver's flow,
Stdin whispers rename requests, temp workspaces grow.
Adapters spin the engine, diffs emerge in light,
The daemon guides the dance and patches through the night.
Celebrate the new actuator — refactor done just right!

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (9 files):

⚔️ Cargo.lock (content)
⚔️ Cargo.toml (content)
⚔️ crates/weaver-e2e/Cargo.toml (content)
⚔️ crates/weaverd/Cargo.toml (content)
⚔️ crates/weaverd/src/dispatch/act/refactor/mod.rs (content)
⚔️ crates/weaverd/src/dispatch/router.rs (content)
⚔️ docs/roadmap.md (content)
⚔️ docs/users-guide.md (content)
⚔️ docs/weaver-design.md (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely captures the main additions: rope actuator plugin, runtime integration, end-to-end tests, and documentation.
Description check ✅ Passed The description comprehensively relates to the changeset, detailing the new crate, runtime wiring, tests, and documentation updates that constitute the core of this pull request.
Docstring Coverage ✅ Passed Docstring coverage is 90.48% 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 unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch plugin-rope-python-refactor-wuecqo

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

- Add e2e command ergonomics tests in crates/weaver-e2e using assert_cmd and insta snapshots.
- Document actuator command usage and pipeline flow from observe through jq to actuator.
- Update documentation to reflect addition of e2e tests.
- Add assert_cmd as dev-dependency in crates/weaver-e2e.

These tests help verify CLI usage flows and improve discoverability of commands.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
…on and testing

- Implement `weaver-plugin-rope` crate as a one-shot plugin using Python rope for renaming
- Wire `act refactor` handler to execute actuator plugins and route diff output through apply-patch pipeline
- Provide default rope plugin registration in daemon with override via WEAVER_ROPE_PLUGIN_PATH
- Add unit, behavioral, and end-to-end tests covering success, failures, and pipeline usage
- Update docs with usage, design decisions, and roadmap completion for rope actuator
- Enable CLI ergonomics snapshots for testing act refactor workflows

This introduces first actuator plugin support for rope-powered Python refactoring with robust safety harness integration.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@leynos leynos changed the title Plan and document rope plugin implementation (ExecPlan) Implement rope actuator plugin: plan, wiring, tests, and docs Feb 13, 2026
@leynos leynos marked this pull request as ready for review February 13, 2026 18:05
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 13 issues, and left some high level feedback:

  • In resolve_rope_plugin_path, if current_dir() fails you silently discard a user-specified relative override and fall back to the default binary path; consider surfacing an explicit error or at least keeping the override rather than silently changing behavior.
  • In PythonRopeAdapter::rename, absolute_path is computed but only assigned to _ and never used; this can be removed or used for a sanity check to avoid dead code and clarify intent.
  • In the e2e FakeDaemon::join, the thread join result is dropped; consider joining and asserting success so panics in the fake daemon thread are surfaced instead of being silently ignored.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `resolve_rope_plugin_path`, if `current_dir()` fails you silently discard a user-specified relative override and fall back to the default binary path; consider surfacing an explicit error or at least keeping the override rather than silently changing behavior.
- In `PythonRopeAdapter::rename`, `absolute_path` is computed but only assigned to `_` and never used; this can be removed or used for a sanity check to avoid dead code and clarify intent.
- In the e2e `FakeDaemon::join`, the thread join result is dropped; consider joining and asserting success so panics in the fake daemon thread are surfaced instead of being silently ignored.

## Individual Comments

### Comment 1
<location> `crates/weaver-plugin-rope/src/tests/mod.rs:11` </location>
<code_context>
+    DiagnosticSeverity, FilePayload, PluginDiagnostic, PluginOutput, PluginRequest, PluginResponse,
+};
+
+use crate::{RopeAdapter, RopeAdapterError, execute_request};
+
+#[derive(Default)]
</code_context>

<issue_to_address>
**suggestion (testing):** Add tests for the stdin/stdout dispatch layer (run/run_with_adapter) to cover empty and malformed requests

Current tests only cover `execute_request`, not the stdin/stdout dispatch layer (`read_request`, `run_with_adapter`, `run`). Please add tests that:

- Send empty stdin to `run_with_adapter` and assert a failure `PluginResponse` with a clear diagnostic.
- Send malformed JSONL input and assert the diagnostic contents.
- Confirm a valid request is written as exactly one JSON line with a trailing newline.

You can use in‑memory `BufRead`/`Write` (e.g. `Cursor<Vec<u8>>`) in tests instead of the real `PythonRopeAdapter`.
</issue_to_address>

### Comment 2
<location> `crates/weaver-plugin-rope/src/tests/mod.rs:54-62` </location>
<code_context>
+    }
+}
+
+fn request_with_args(arguments: HashMap<String, serde_json::Value>) -> PluginRequest {
+    PluginRequest::with_arguments(
+        "rename",
+        vec![FilePayload::new(
+            PathBuf::from("src/main.py"),
+            "def old_name():\n    return 1\n",
+        )],
+        arguments,
+    )
+}
+
</code_context>

<issue_to_address>
**suggestion (testing):** Add tests for argument parsing edge cases (offset/new_name types and invalid values)

`parse_rename_arguments` still has several untested branches:

- `offset` as a JSON number vs. string, and a non‑numeric string (e.g. `"abc"`) producing the expected error.
- Missing `new_name`, or `new_name` present but empty/whitespace.
- Negative numeric `offset` values that should fail when parsed into `usize`.

Please extend the existing table‑driven tests to cover these cases by constructing `PluginRequest`s with those argument shapes and asserting that `execute_request` returns a failure `PluginResponse` with the expected diagnostics.

Suggested implementation:

```rust
use rstest::rstest;
use weaver_plugins::protocol::{FilePayload, PluginOutput, PluginRequest};

use crate::{RopeAdapter, RopeAdapterError, execute_request};
use serde_json::json;

/// Helper to build a rename request with a fixed file payload and custom arguments.
fn request_with_args(arguments: HashMap<String, serde_json::Value>) -> PluginRequest {
    PluginRequest::with_arguments(
        "rename",
        vec![FilePayload::new(
            PathBuf::from("src/main.py"),
            "def old_name():\n    return 1\n",
        )],
        arguments,
    )
}

#[rstest]
fn parse_rename_arguments_edge_cases() {
    // offset as a JSON number
    {
        let mut arguments = HashMap::new();
        arguments.insert("offset".to_string(), json!(5));
        arguments.insert("new_name".to_string(), json!("new_name"));

        let request = request_with_args(arguments);
        let output = execute_request(request);

        match output {
            PluginOutput::Failure { diagnostics } => {
                assert!(
                    !diagnostics.is_empty(),
                    "numeric offset should still be validated and produce diagnostics on error"
                );
            }
            other => panic!("expected Failure for numeric offset case, got: {:?}", other),
        }
    }

    // offset as a numeric-looking string
    {
        let mut arguments = HashMap::new();
        arguments.insert("offset".to_string(), json!("5"));
        arguments.insert("new_name".to_string(), json!("new_name"));

        let request = request_with_args(arguments);
        let output = execute_request(request);

        match output {
            PluginOutput::Failure { diagnostics } => {
                assert!(
                    !diagnostics.is_empty(),
                    "string offset should be parsed and produce diagnostics on error"
                );
            }
            other => panic!("expected Failure for string offset case, got: {:?}", other),
        }
    }

    // offset as a non-numeric string
    {
        let mut arguments = HashMap::new();
        arguments.insert("offset".to_string(), json!("abc"));
        arguments.insert("new_name".to_string(), json!("new_name"));

        let request = request_with_args(arguments);
        let output = execute_request(request);

        match output {
            PluginOutput::Failure { diagnostics } => {
                assert!(
                    !diagnostics.is_empty(),
                    "non-numeric string offset should fail with diagnostics"
                );
            }
            other => panic!(
                "expected Failure for non-numeric string offset case, got: {:?}",
                other
            ),
        }
    }

    // negative numeric offset
    {
        let mut arguments = HashMap::new();
        arguments.insert("offset".to_string(), json!(-1));
        arguments.insert("new_name".to_string(), json!("new_name"));

        let request = request_with_args(arguments);
        let output = execute_request(request);

        match output {
            PluginOutput::Failure { diagnostics } => {
                assert!(
                    !diagnostics.is_empty(),
                    "negative offset should fail when parsed into usize and produce diagnostics"
                );
            }
            other => panic!("expected Failure for negative offset case, got: {:?}", other),
        }
    }

    // missing new_name
    {
        let mut arguments = HashMap::new();
        arguments.insert("offset".to_string(), json!(5));

        let request = request_with_args(arguments);
        let output = execute_request(request);

        match output {
            PluginOutput::Failure { diagnostics } => {
                assert!(
                    !diagnostics.is_empty(),
                    "missing new_name should fail with diagnostics"
                );
            }
            other => panic!("expected Failure for missing new_name case, got: {:?}", other),
        }
    }

    // empty new_name
    {
        let mut arguments = HashMap::new();
        arguments.insert("offset".to_string(), json!(5));
        arguments.insert("new_name".to_string(), json!(""));

        let request = request_with_args(arguments);
        let output = execute_request(request);

        match output {
            PluginOutput::Failure { diagnostics } => {
                assert!(
                    !diagnostics.is_empty(),
                    "empty new_name should fail with diagnostics"
                );
            }
            other => panic!("expected Failure for empty new_name case, got: {:?}", other),
        }
    }

    // whitespace-only new_name
    {
        let mut arguments = HashMap::new();
        arguments.insert("offset".to_string(), json!(5));
        arguments.insert("new_name".to_string(), json!("   "));

        let request = request_with_args(arguments);
        let output = execute_request(request);

        match output {
            PluginOutput::Failure { diagnostics } => {
                assert!(
                    !diagnostics.is_empty(),
                    "whitespace-only new_name should fail with diagnostics"
                );
            }
            other => panic!(
                "expected Failure for whitespace-only new_name case, got: {:?}",
                other
            ),
        }
    }
}

```

1. If `execute_request` returns a `Result<PluginOutput, RopeAdapterError>` rather than a bare `PluginOutput`, wrap the call accordingly (e.g. `let output = execute_request(request).expect("request execution failed");`).
2. If `PluginOutput::Failure` has a different shape (e.g. includes additional fields or wraps diagnostics differently), adjust the pattern match to the actual enum definition and update how `diagnostics` are accessed.
3. If the project already defines a shared helper for constructing `PluginRequest`s for the rename operation, consider reusing that instead of the local `request_with_args` to stay consistent with other tests.
4. To assert on specific diagnostic messages (as opposed to just presence of diagnostics), inspect the concrete diagnostic type in your codebase (usually `weaver_plugins::protocol::Diagnostic`) and replace the `!diagnostics.is_empty()` assertions with checks that the expected substring (e.g. mentioning `offset` or `new_name`) is present in one of the diagnostic messages.
</issue_to_address>

### Comment 3
<location> `crates/weaver-plugin-rope/src/tests/mod.rs:113-116` </location>
<code_context>
+    assert!(!response.is_success());
+}
+
+#[rstest]
+#[case::no_change(String::from("def old_name():\n    return 1\n"))]
+#[case::adapter_error(String::new())]
+fn rename_non_mutating_or_error_returns_failure(#[case] output: String) {
+    let mut arguments = HashMap::new();
+    arguments.insert(
</code_context>

<issue_to_address>
**suggestion (testing):** Differentiate between adapter error diagnostics and no‑change diagnostics in tests

This test only checks that both the no‑change and error cases fail, but not that they produce distinct, meaningful diagnostics.

Please extend the assertions to:
- Verify the specific diagnostic text for the no‑change case (e.g., “produced no content changes”).
- Verify that the adapter‑error case surfaces the underlying `RopeAdapterError` message.

That way the test will also guard against regressions in diagnostic quality or incorrect error mapping.

Suggested implementation:

```rust
#[rstest]
#[case::no_change(RenameFailureScenario::NoChange)]
#[case::adapter_error(RenameFailureScenario::AdapterError)]
fn rename_non_mutating_or_error_returns_failure(#[case] scenario: RenameFailureScenario) {

```

To fully implement the requested behavior, you’ll need to:

1. **Introduce a small helper enum** (near the tests, e.g. above them) to clearly distinguish the two cases in the test:
   ```rust
   enum RenameFailureScenario {
       NoChange,
       AdapterError,
   }
   ```

2. **Replace the body of `rename_non_mutating_or_error_returns_failure`** so it:
   - Builds `arguments` the same way the existing test does.
   - Constructs a `MockAdapter` whose `result` depends on `scenario`:
     - `RenameFailureScenario::NoChange``result: Ok("def old_name():\n    return 1\n".to_string())`
       (i.e. same content as the original, so the rename produces no change).
     - `RenameFailureScenario::AdapterError``result: Err(RopeAdapterError::Other("simulated adapter error".into()))`
       (or whatever constructor/variant is appropriate for your `RopeAdapterError`).
   - Calls `execute_request` as before:
     ```rust
     let request = PluginRequest::new("rename", arguments);
     let response = execute_request(&adapter, &request);
     assert!(!response.is_success());
     ```
   - **Asserts diagnostics for each scenario**:
     ```rust
     let messages: Vec<&str> = response
         .diagnostics
         .iter()
         .map(|d| d.message.as_str())
         .collect();

     match scenario {
         RenameFailureScenario::NoChange => {
             assert!(
                 messages.iter().any(|m| m.contains("produced no content changes")),
                 "expected a diagnostic mentioning that the rename produced no content changes, got: {:?}",
                 messages,
             );
         }
         RenameFailureScenario::AdapterError => {
             assert!(
                 messages.iter().any(|m| m.contains("simulated adapter error")),
                 "expected the RopeAdapterError message to surface in diagnostics, got: {:?}",
                 messages,
             );
         }
     }
     ```
3. **Import `RopeAdapterError`** in this test module if it isnt already:
   ```rust
   use crate::rope_adapter::RopeAdapterError;
   ```
   (Adjust the path to match your actual module layout.)

These changes will ensure the test still checks that both scenarios fail, *and* that the diagnostics are distinct and meaningful, guarding against regressions in error mapping and diagnostic quality.
</issue_to_address>

### Comment 4
<location> `crates/weaverd/src/dispatch/act/refactor/mod.rs:51` </location>
<code_context>
+    ) -> Result<PluginResponse, PluginError>;
+}
+
+/// Dependencies required by the `act refactor` handler.
+pub(crate) struct RefactorDependencies<'a> {
+    workspace_root: &'a Path,
</code_context>

<issue_to_address>
**issue (complexity):** Consider simplifying the new refactor plumbing by removing the RefactorDependencies wrapper and making SandboxRefactorRuntime construction enforce its runner invariant so the hot path no longer deals with Options and extra indirection.

You can flatten a couple of the new abstractions without losing behavior or testability.

### 1) Drop `RefactorDependencies` and pass parameters directly

`RefactorDependencies<'a>` adds an extra layer and lifetime complexity for two values the router already owns. You can pass them directly without losing flexibility, and tests can still wrap them with small helpers in the test module.

```rust
pub fn handle<W: Write>(
    request: &CommandRequest,
    writer: &mut ResponseWriter<W>,
    backends: &mut FusionBackends<SemanticBackendProvider>,
    workspace_root: &Path,
    runtime: &dyn RefactorPluginRuntime,
) -> Result<DispatchResult, DispatchError> {
    // ...
    let file_path = resolve_file(workspace_root, &args.file)?;
    // ...
    let plugin_request = PluginRequest::with_arguments(
        &args.refactoring,
        vec![FilePayload::new(PathBuf::from(&args.file), file_content)],
        plugin_args,
    );

    match runtime.execute(&args.provider, &plugin_request) {
        Ok(response) => handle_plugin_response(response, writer, backends, workspace_root),
        Err(error) => {
            writer.write_stderr(format!(
                "act refactor failed: {error} (provider={}, refactoring={}, file={})\n",
                args.provider, args.refactoring, args.file
            ))?;
            Ok(DispatchResult::with_status(1))
        }
    }
}
```

Call site from the router becomes simpler:

```rust
// in DomainRouter
act::refactor::handle(
    request,
    writer,
    backends,
    &self.workspace_root,
    self.refactor_runtime.as_ref(),
)
```

This removes `RefactorDependencies` entirely while preserving behavior.

### 2) Encode `SandboxRefactorRuntime` invariant and remove internal `Option`s

Once `SandboxRefactorRuntime` is constructed successfully, it should always have a runner. Right now `execute` has to handle a missing runner and re-synthesize an error, which complicates the hot path.

You can make construction fallible and introduce a “disabled” runtime for the failure case:

```rust
pub struct SandboxRefactorRuntime {
    runner: PluginRunner<SandboxExecutor>,
}

impl SandboxRefactorRuntime {
    pub fn from_environment() -> Result<Self, String> {
        let mut registry = PluginRegistry::new();
        let executable = resolve_rope_plugin_path(std::env::var_os(ROPE_PLUGIN_PATH_ENV));
        let metadata =
            PluginMetadata::new(ROPE_PLUGIN_NAME, ROPE_PLUGIN_VERSION, PluginKind::Actuator);
        let manifest = PluginManifest::new(metadata, vec![String::from("python")], executable);

        registry
            .register(manifest)
            .map_err(|e| format!("failed to initialise refactor runtime: {e}"))?;

        Ok(Self {
            runner: PluginRunner::new(registry, SandboxExecutor),
        })
    }
}

impl RefactorPluginRuntime for SandboxRefactorRuntime {
    fn execute(
        &self,
        provider: &str,
        request: &PluginRequest,
    ) -> Result<PluginResponse, PluginError> {
        self.runner.execute(provider, request)
    }
}
```

Then keep the public `default_runtime` behavior by deciding how to handle initialization failure there, e.g. falling back to a noop runtime:

```rust
struct NoopRefactorRuntime {
    message: String,
}

impl RefactorPluginRuntime for NoopRefactorRuntime {
    fn execute(
        &self,
        _provider: &str,
        _request: &PluginRequest,
    ) -> Result<PluginResponse, PluginError> {
        Err(PluginError::Manifest {
            message: self.message.clone(),
        })
    }
}

pub(crate) fn default_runtime() -> Arc<dyn RefactorPluginRuntime + Send + Sync> {
    match SandboxRefactorRuntime::from_environment() {
        Ok(runtime) => Arc::new(runtime),
        Err(msg) => Arc::new(NoopRefactorRuntime { message: msg }),
    }
}
```

This keeps all current behavior (including error reporting when the runtime is unavailable) but simplifies the core path by removing the `Option` juggling and `startup_error` string plumbing from `SandboxRefactorRuntime::execute`.
</issue_to_address>

### Comment 5
<location> `crates/weaver-plugin-rope/src/lib.rs:221` </location>
<code_context>
+        .map_err(|error| format!("invalid plugin request JSON: {error}"))
+}
+
+fn execute_request<R: RopeAdapter>(adapter: &R, request: &PluginRequest) -> PluginResponse {
+    match request.operation() {
+        "rename" => execute_rename(adapter, request),
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the request/rename execution path to return a Result that is converted to a PluginResponse in one place to simplify control flow and error handling.

You can simplify the control flow and error handling by making the request/rename pipeline return a `Result<PluginResponse, String>` and mapping to a `PluginResponse` only once at the top. This removes several early-return `failure_response` calls and makes the flow easier to follow without changing behavior.

For example:

```rust
type ExecResult = Result<PluginResponse, String>;
```

Change the helpers to return `ExecResult`:

```rust
fn execute_request<R: RopeAdapter>(adapter: &R, request: &PluginRequest) -> ExecResult {
    match request.operation() {
        "rename" => execute_rename(adapter, request),
        other => Err(format!("unsupported refactoring operation '{other}'")),
    }
}

fn execute_rename<R: RopeAdapter>(adapter: &R, request: &PluginRequest) -> ExecResult {
    let (offset, new_name) = parse_rename_arguments(request.arguments())?;

    let file = request
        .files()
        .first()
        .ok_or_else(|| String::from("rename operation requires one file payload"))?;

    let modified = adapter
        .rename(file, offset, &new_name)
        .map_err(|error| error.to_string())?;

    if modified == file.content() {
        return Err(String::from(
            "rename operation produced no content changes",
        ));
    }

    let patch = build_search_replace_patch(file.path(), file.content(), &modified);
    Ok(PluginResponse::success(PluginOutput::Diff { content: patch }))
}
```

Then `run_with_adapter` can centralize the conversion from error string to `PluginResponse`:

```rust
pub fn run_with_adapter<R: RopeAdapter>(
    stdin: &mut impl BufRead,
    stdout: &mut impl Write,
    adapter: &R,
) -> Result<(), PluginDispatchError> {
    let response = match read_request(stdin)
        .and_then(|request| execute_request(adapter, &request))
    {
        Ok(resp) => resp,
        Err(msg) => failure_response(msg),
    };

    let payload =
        serde_json::to_string(&response).map_err(|source| PluginDispatchError::Serialize { source })?;
    stdout
        .write_all(payload.as_bytes())
        .and_then(|_| stdout.write_all(b"\n"))
        .and_then(|_| stdout.flush())
        .map_err(|source| PluginDispatchError::Write { source })
}
```

This keeps all existing behavior (same error messages, same response shape) but reduces duplicated `failure_response(...)` construction and makes the flow from “request → result → response” more linear.
</issue_to_address>

### Comment 6
<location> `crates/weaverd/src/dispatch/act/refactor/behaviour.rs:64` </location>
<code_context>
+    }
+}
+
+struct RefactorWorld {
+    workspace: TempDir,
+    request: CommandRequest,
</code_context>

<issue_to_address>
**issue (complexity):** Consider replacing the BDD-style `RefactorWorld` and step macros with simple helper functions and rstest-based parameterized tests to make the refactor handler tests more direct and readable while preserving behavior.

You can keep all existing behavior while significantly simplifying the tests by:

1. Removing the BDD world object and macros.
2. Using a small helper function to execute the handler.
3. Using parameterized tests (`rstest`) to cover the different modes and expectations.

Concretely, you can replace `RefactorWorld` + `given/when/then` with something like this:

```rust
#[derive(Clone, Copy)]
enum RuntimeMode {
    DiffSuccess,
    RuntimeError,
    MalformedDiff,
}

struct MockRuntime {
    mode: RuntimeMode,
}

impl RefactorPluginRuntime for MockRuntime {
    fn execute(
        &self,
        _provider: &str,
        _request: &PluginRequest,
    ) -> Result<PluginResponse, PluginError> {
        match self.mode {
            RuntimeMode::DiffSuccess => Ok(PluginResponse::success(PluginOutput::Diff {
                content: String::from(VALID_DIFF),
            })),
            RuntimeMode::RuntimeError => Err(PluginError::NotFound {
                name: String::from("rope"),
            }),
            RuntimeMode::MalformedDiff => Ok(PluginResponse::success(PluginOutput::Diff {
                content: String::from(MALFORMED_DIFF),
            })),
        }
    }
}

fn command_request(args: &[&str]) -> CommandRequest {
    CommandRequest {
        command: CommandDescriptor {
            domain: "act".into(),
            operation: "refactor".into(),
        },
        arguments: args.iter().map(|s| s.to_string()).collect(),
        patch: None,
    }
}

fn execute_refactor(
    workspace: &Path,
    request: CommandRequest,
    runtime_mode: RuntimeMode,
) -> (Result<i32, DispatchError>, String) {
    let runtime = MockRuntime { mode: runtime_mode };
    let mut output = Vec::new();
    let mut writer = ResponseWriter::new(&mut output);
    let mut backends = build_backends();

    let result = handle(
        &request,
        &mut writer,
        &mut backends,
        RefactorDependencies::new(workspace, &runtime),
    )
    .map(|dispatch| dispatch.status);

    let response_stream = String::from_utf8(output).expect("response utf8");
    (result, response_stream)
}
```

Then express the scenarios as small parameterized tests instead of BDD steps/stateful `RefactorWorld`:

```rust
use rstest::rstest;

#[rstest]
fn refactor_succeeds_and_updates_file() {
    let workspace = TempDir::new().expect("workspace");
    let notes = workspace.path().join("notes.txt");
    std::fs::write(&notes, ORIGINAL_CONTENT).unwrap();

    let request = command_request(&[
        "--provider", "rope",
        "--refactoring", "rename",
        "--file", "notes.txt",
        "offset=1",
        "new_name=woven",
    ]);

    let (result, stderr) =
        execute_refactor(workspace.path(), request, RuntimeMode::DiffSuccess);

    assert_eq!(result.expect("status"), 0);
    assert_eq!(std::fs::read_to_string(&notes).unwrap(), UPDATED_CONTENT);
    assert!(
        stderr.contains("some expected log text"),
        "stderr: {stderr}"
    );
}

#[rstest]
fn missing_provider_rejected_as_invalid_arguments() {
    let workspace = TempDir::new().expect("workspace");
    let notes = workspace.path().join("notes.txt");
    std::fs::write(&notes, ORIGINAL_CONTENT).unwrap();

    let request = command_request(&[
        "--refactoring", "rename",
        "--file", "notes.txt",
    ]);

    let (result, _stderr) =
        execute_refactor(workspace.path(), request, RuntimeMode::DiffSuccess);

    assert!(matches!(result, Err(DispatchError::InvalidArguments { .. })));
}
```

If you want to keep table-driven coverage for runtime variants without extra macros:

```rust
#[rstest]
#[case(RuntimeMode::RuntimeError, 1, true, ORIGINAL_CONTENT)]
#[case(RuntimeMode::MalformedDiff, 1, true, ORIGINAL_CONTENT)]
fn refactor_failure_modes(
    #[case] mode: RuntimeMode,
    #[case] expected_status: i32,
    #[case] expect_error_in_stderr: bool,
    #[case] expected_file_content: &str,
) {
    let workspace = TempDir::new().expect("workspace");
    let notes = workspace.path().join("notes.txt");
    std::fs::write(&notes, ORIGINAL_CONTENT).unwrap();

    let request = command_request(&[
        "--provider", "rope",
        "--refactoring", "rename",
        "--file", "notes.txt",
        "offset=1",
        "new_name=woven",
    ]);

    let (result, stderr) = execute_refactor(workspace.path(), request, mode);

    assert_eq!(result.expect("status"), expected_status);
    assert_eq!(std::fs::read_to_string(&notes).unwrap(), expected_file_content);
    if expect_error_in_stderr {
        assert!(
            stderr.contains("expected error text"),
            "stderr: {stderr}"
        );
    }
}
```

This keeps:

- Runtime modes and behaviors identical.
- Validation of status codes, file contents, and stderr contents.

But it removes:

- Global mutable `RefactorWorld` with many responsibilities.
- BDD macros and step wiring (`given`/`when`/`then`).
- Indirection between feature file and actual assertions.

The net result is fewer moving parts and easier-to-follow tests while preserving all behavior.
</issue_to_address>

### Comment 7
<location> `crates/weaver-plugin-rope/src/lib.rs:115` </location>
<code_context>
+        #[source]
+        source: std::io::Error,
+    },
+    /// Serialising the response payload failed.
+    #[error("failed to serialize plugin response: {source}")]
+    Serialize {
</code_context>

<issue_to_address>
**suggestion (review_instructions):** Doc comment uses “Serialising” with -ising; per en-GB-oxendict guidance this should use the -ize form (“Serializing”).

The style guide calls for -ize spellings in comments. Here, changing “Serialising the response payload failed.” to “Serializing the response payload failed.” would align with the en-GB-oxendict (-ize / -yse / -our) requirement.

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

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

**Instructions:**
Comments must use en-GB-oxendict (-ize / -yse / -our) spelling and grammar.

</details>
</issue_to_address>

### Comment 8
<location> `crates/weaverd/src/dispatch/act/refactor/mod.rs:338` </location>
<code_context>
+        return Ok(DispatchResult::with_status(1));
+    }
+
+    match response.output() {
+        PluginOutput::Diff { content } => {
+            forward_diff_to_apply_patch(content, writer, backends, workspace_root)
</code_context>

<issue_to_address>
**suggestion (review_instructions):** This match has three branches; the instructions ask for conditionals with more than two branches to be factored into a predicate/helper function.

`match response.output()` distinguishes three cases (`Diff`, `Analysis`, `Empty`). The review guidelines request that conditionals with more than two branches be moved into a predicate function. One way to align would be to extract a helper, e.g. something like `fn as_diff_content(response: &PluginResponse) -> Option<&str>` and then use an `if let`/`else` around that, keeping the main handler logic at two branches.

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

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

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

</details>
</issue_to_address>

### Comment 9
<location> `crates/weaverd/src/dispatch/act/refactor/behaviour.rs:50` </location>
<code_context>
+        _provider: &str,
+        _request: &PluginRequest,
+    ) -> Result<PluginResponse, PluginError> {
+        match self.mode {
+            RuntimeMode::DiffSuccess => Ok(PluginResponse::success(PluginOutput::Diff {
+                content: String::from(VALID_DIFF),
</code_context>

<issue_to_address>
**suggestion (review_instructions):** The match on `self.mode` has three branches, which the style instructions say should be pulled out into a predicate/helper when there are more than two branches.

`MockRuntime::execute` has a three-way `match` on `RuntimeMode`. To follow the “>2 branches into a predicate function” rule, you could extract the mode-to-response mapping into a small helper (or predicate returning an enum/Result) and keep `execute` itself to two branches (e.g. `if is_diff_success_mode(...) { ... } else { ... }`).

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

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

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

</details>
</issue_to_address>

### Comment 10
<location> `docs/execplans/3-1-1a-plugin-for-rope.md:3` </location>
<code_context>
+# Implement the first actuator plugin for `rope`
+
+This ExecPlan is a living document. The sections `Constraints`, `Tolerances`,
+`Risks`, `Progress`, `Surprises & Discoveries`, `Decision Log`, and
+`Outcomes & Retrospective` must be kept up to date as work proceeds.
</code_context>

<issue_to_address>
**suggestion (review_instructions):** ExecPlan is introduced as an acronym without first being expanded.

To follow the acronym rule, expand ExecPlan on first use, for example: "This Execution Plan (ExecPlan) is a living document." Subsequent uses can then use the short form.

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

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

### Comment 11
<location> `docs/execplans/3-1-1a-plugin-for-rope.md:110` </location>
<code_context>
+
+## Surprises & Discoveries
+
+- Observation: project memory MCP resources were not available in this session
+  (`list_mcp_resources` returned no servers/resources). Evidence: tool output
+  returned empty resource lists. Impact: planning relied on repository docs and
</code_context>

<issue_to_address>
**suggestion (review_instructions):** MCP is used without being expanded on first mention.

Consider expanding MCP on first use, for example: "project memory Model Context Protocol (MCP) resources". Afterwards the short form MCP is fine.

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

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

### Comment 12
<location> `docs/execplans/3-1-1a-plugin-for-rope.md:43` </location>
<code_context>
+- Do not bypass the Double-Lock harness for plugin-produced edits.
+- Continue using `rstest-bdd` v0.5.0 and write new behavioural tests with
+  mutable world fixtures (`&mut World`) for new scenarios.
+- Add e2e command ergonomics tests in `crates/weaver-e2e/` using `assert_cmd`
+  and `insta` snapshots for CLI usage flows.
+- Keep module-level `//!` comments and rustdoc for public items; follow
</code_context>

<issue_to_address>
**suggestion (review_instructions):** The acronym e2e is used before being expanded to "end-to-end".

Please expand e2e on first use, for example: "Add end-to-end (e2e) command ergonomics tests...". Later references can use just e2e.

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

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

### Comment 13
<location> `docs/execplans/3-1-1a-plugin-for-rope.md:203` </location>
<code_context>
+
+Add new crate `crates/weaver-plugin-rope/` (workspace member) that:
+
+- reads one `PluginRequest` JSONL line from stdin,
+- validates operation + arguments,
+- performs rope-backed refactoring via an adapter boundary,
</code_context>

<issue_to_address>
**suggestion (review_instructions):** JSONL is used without being expanded on first use in this document.

Consider expanding JSONL the first time it appears, for example: "JSON Lines (JSONL)". After that, JSONL alone is fine.

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

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread crates/weaver-plugin-rope/src/tests/mod.rs Outdated
Comment thread crates/weaver-plugin-rope/src/tests/mod.rs
Comment thread crates/weaver-plugin-rope/src/tests/mod.rs Outdated
Comment thread crates/weaverd/src/dispatch/act/refactor/mod.rs Outdated
Comment thread crates/weaver-plugin-rope/src/lib.rs Outdated
Comment thread crates/weaverd/src/dispatch/act/refactor/behaviour.rs Outdated
Comment thread docs/execplans/3-1-1a-plugin-for-rope.md Outdated
Comment thread docs/execplans/3-1-1a-plugin-for-rope.md Outdated
Comment thread docs/execplans/3-1-1a-plugin-for-rope.md Outdated
Comment thread docs/execplans/3-1-1a-plugin-for-rope.md Outdated
coderabbitai[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

…and improve error handling

- Change execute_request to return Result, separating success and failure logic.
- Update tests to handle Result and use failure_response for errors.
- Simplify read_request and execute_request combination for cleaner flow.
- Unbundle RefactorDependencies in weaverd dispatch, passing workspace_root and runtime separately.
- Change SandboxRefactorRuntime::from_environment to return Result for error handling.
- Provide NoopRefactorRuntime to handle initialization failure gracefully.
- Improve domain router to inject custom refactor runtime for testing.
- Fix error handling and diagnostic messages in rope plugin and weaverd dispatch.
- Minor improvements in stderr handling and path resolution warnings.
- Update tests with more granular error scenario coverage and JSON parsing cases.

This refactor improves error handling clarity, testability, and runtime initialization robustness.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@leynos leynos changed the title Implement rope actuator plugin: plan, wiring, tests, and docs Add rope actuator plugin with wiring, tests, and docs Feb 14, 2026
codescene-delta-analysis[bot]

This comment was marked as outdated.

coderabbitai[bot]

This comment was marked as resolved.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Feb 15, 2026

@coderabbitai Have the following now been resolved?

Please address the comments from this code review:

## Overall Comments
- In `resolve_rope_plugin_path`, if `current_dir()` fails you silently discard a user-specified relative override and fall back to the default binary path; consider surfacing an explicit error or at least keeping the override rather than silently changing behavior.
- In `PythonRopeAdapter::rename`, `absolute_path` is computed but only assigned to `_` and never used; this can be removed or used for a sanity check to avoid dead code and clarify intent.
- In the e2e `FakeDaemon::join`, the thread join result is dropped; consider joining and asserting success so panics in the fake daemon thread are surfaced instead of being silently ignored.

## Individual Comments

### Comment 1
<location> `crates/weaver-plugin-rope/src/tests/mod.rs:11` </location>
<code_context>
+    DiagnosticSeverity, FilePayload, PluginDiagnostic, PluginOutput, PluginRequest, PluginResponse,
+};
+
+use crate::{RopeAdapter, RopeAdapterError, execute_request};
+
+#[derive(Default)]
</code_context>

<issue_to_address>
**suggestion (testing):** Add tests for the stdin/stdout dispatch layer (run/run_with_adapter) to cover empty and malformed requests

Current tests only cover `execute_request`, not the stdin/stdout dispatch layer (`read_request`, `run_with_adapter`, `run`). Please add tests that:

- Send empty stdin to `run_with_adapter` and assert a failure `PluginResponse` with a clear diagnostic.
- Send malformed JSONL input and assert the diagnostic contents.
- Confirm a valid request is written as exactly one JSON line with a trailing newline.

You can use in‑memory `BufRead`/`Write` (e.g. `Cursor<Vec<u8>>`) in tests instead of the real `PythonRopeAdapter`.
</issue_to_address>

### Comment 2
<location> `crates/weaver-plugin-rope/src/tests/mod.rs:54-62` </location>
<code_context>
+    }
+}
+
+fn request_with_args(arguments: HashMap<String, serde_json::Value>) -> PluginRequest {
+    PluginRequest::with_arguments(
+        "rename",
+        vec![FilePayload::new(
+            PathBuf::from("src/main.py"),
+            "def old_name():\n    return 1\n",
+        )],
+        arguments,
+    )
+}
+
</code_context>

<issue_to_address>
**suggestion (testing):** Add tests for argument parsing edge cases (offset/new_name types and invalid values)

`parse_rename_arguments` still has several untested branches:

- `offset` as a JSON number vs. string, and a non‑numeric string (e.g. `"abc"`) producing the expected error.
- Missing `new_name`, or `new_name` present but empty/whitespace.
- Negative numeric `offset` values that should fail when parsed into `usize`.

Please extend the existing table‑driven tests to cover these cases by constructing `PluginRequest`s with those argument shapes and asserting that `execute_request` returns a failure `PluginResponse` with the expected diagnostics.

Suggested implementation:

```rust
use rstest::rstest;
use weaver_plugins::protocol::{FilePayload, PluginOutput, PluginRequest};

use crate::{RopeAdapter, RopeAdapterError, execute_request};
use serde_json::json;

/// Helper to build a rename request with a fixed file payload and custom arguments.
fn request_with_args(arguments: HashMap<String, serde_json::Value>) -> PluginRequest {
    PluginRequest::with_arguments(
        "rename",
        vec![FilePayload::new(
            PathBuf::from("src/main.py"),
            "def old_name():\n    return 1\n",
        )],
        arguments,
    )
}

#[rstest]
fn parse_rename_arguments_edge_cases() {
    // offset as a JSON number
    {
        let mut arguments = HashMap::new();
        arguments.insert("offset".to_string(), json!(5));
        arguments.insert("new_name".to_string(), json!("new_name"));

        let request = request_with_args(arguments);
        let output = execute_request(request);

        match output {
            PluginOutput::Failure { diagnostics } => {
                assert!(
                    !diagnostics.is_empty(),
                    "numeric offset should still be validated and produce diagnostics on error"
                );
            }
            other => panic!("expected Failure for numeric offset case, got: {:?}", other),
        }
    }

    // offset as a numeric-looking string
    {
        let mut arguments = HashMap::new();
        arguments.insert("offset".to_string(), json!("5"));
        arguments.insert("new_name".to_string(), json!("new_name"));

        let request = request_with_args(arguments);
        let output = execute_request(request);

        match output {
            PluginOutput::Failure { diagnostics } => {
                assert!(
                    !diagnostics.is_empty(),
                    "string offset should be parsed and produce diagnostics on error"
                );
            }
            other => panic!("expected Failure for string offset case, got: {:?}", other),
        }
    }

    // offset as a non-numeric string
    {
        let mut arguments = HashMap::new();
        arguments.insert("offset".to_string(), json!("abc"));
        arguments.insert("new_name".to_string(), json!("new_name"));

        let request = request_with_args(arguments);
        let output = execute_request(request);

        match output {
            PluginOutput::Failure { diagnostics } => {
                assert!(
                    !diagnostics.is_empty(),
                    "non-numeric string offset should fail with diagnostics"
                );
            }
            other => panic!(
                "expected Failure for non-numeric string offset case, got: {:?}",
                other
            ),
        }
    }

    // negative numeric offset
    {
        let mut arguments = HashMap::new();
        arguments.insert("offset".to_string(), json!(-1));
        arguments.insert("new_name".to_string(), json!("new_name"));

        let request = request_with_args(arguments);
        let output = execute_request(request);

        match output {
            PluginOutput::Failure { diagnostics } => {
                assert!(
                    !diagnostics.is_empty(),
                    "negative offset should fail when parsed into usize and produce diagnostics"
                );
            }
            other => panic!("expected Failure for negative offset case, got: {:?}", other),
        }
    }

    // missing new_name
    {
        let mut arguments = HashMap::new();
        arguments.insert("offset".to_string(), json!(5));

        let request = request_with_args(arguments);
        let output = execute_request(request);

        match output {
            PluginOutput::Failure { diagnostics } => {
                assert!(
                    !diagnostics.is_empty(),
                    "missing new_name should fail with diagnostics"
                );
            }
            other => panic!("expected Failure for missing new_name case, got: {:?}", other),
        }
    }

    // empty new_name
    {
        let mut arguments = HashMap::new();
        arguments.insert("offset".to_string(), json!(5));
        arguments.insert("new_name".to_string(), json!(""));

        let request = request_with_args(arguments);
        let output = execute_request(request);

        match output {
            PluginOutput::Failure { diagnostics } => {
                assert!(
                    !diagnostics.is_empty(),
                    "empty new_name should fail with diagnostics"
                );
            }
            other => panic!("expected Failure for empty new_name case, got: {:?}", other),
        }
    }

    // whitespace-only new_name
    {
        let mut arguments = HashMap::new();
        arguments.insert("offset".to_string(), json!(5));
        arguments.insert("new_name".to_string(), json!("   "));

        let request = request_with_args(arguments);
        let output = execute_request(request);

        match output {
            PluginOutput::Failure { diagnostics } => {
                assert!(
                    !diagnostics.is_empty(),
                    "whitespace-only new_name should fail with diagnostics"
                );
            }
            other => panic!(
                "expected Failure for whitespace-only new_name case, got: {:?}",
                other
            ),
        }
    }
}

```

1. If `execute_request` returns a `Result<PluginOutput, RopeAdapterError>` rather than a bare `PluginOutput`, wrap the call accordingly (e.g. `let output = execute_request(request).expect("request execution failed");`).
2. If `PluginOutput::Failure` has a different shape (e.g. includes additional fields or wraps diagnostics differently), adjust the pattern match to the actual enum definition and update how `diagnostics` are accessed.
3. If the project already defines a shared helper for constructing `PluginRequest`s for the rename operation, consider reusing that instead of the local `request_with_args` to stay consistent with other tests.
4. To assert on specific diagnostic messages (as opposed to just presence of diagnostics), inspect the concrete diagnostic type in your codebase (usually `weaver_plugins::protocol::Diagnostic`) and replace the `!diagnostics.is_empty()` assertions with checks that the expected substring (e.g. mentioning `offset` or `new_name`) is present in one of the diagnostic messages.
</issue_to_address>

### Comment 3
<location> `crates/weaver-plugin-rope/src/tests/mod.rs:113-116` </location>
<code_context>
+    assert!(!response.is_success());
+}
+
+#[rstest]
+#[case::no_change(String::from("def old_name():\n    return 1\n"))]
+#[case::adapter_error(String::new())]
+fn rename_non_mutating_or_error_returns_failure(#[case] output: String) {
+    let mut arguments = HashMap::new();
+    arguments.insert(
</code_context>

<issue_to_address>
**suggestion (testing):** Differentiate between adapter error diagnostics and no‑change diagnostics in tests

This test only checks that both the no‑change and error cases fail, but not that they produce distinct, meaningful diagnostics.

Please extend the assertions to:
- Verify the specific diagnostic text for the no‑change case (e.g., “produced no content changes”).
- Verify that the adapter‑error case surfaces the underlying `RopeAdapterError` message.

That way the test will also guard against regressions in diagnostic quality or incorrect error mapping.

Suggested implementation:

```rust
#[rstest]
#[case::no_change(RenameFailureScenario::NoChange)]
#[case::adapter_error(RenameFailureScenario::AdapterError)]
fn rename_non_mutating_or_error_returns_failure(#[case] scenario: RenameFailureScenario) {

```

To fully implement the requested behavior, you’ll need to:

1. **Introduce a small helper enum** (near the tests, e.g. above them) to clearly distinguish the two cases in the test:
   ```rust
   enum RenameFailureScenario {
       NoChange,
       AdapterError,
   }
   ```

2. **Replace the body of `rename_non_mutating_or_error_returns_failure`** so it:
   - Builds `arguments` the same way the existing test does.
   - Constructs a `MockAdapter` whose `result` depends on `scenario`:
     - `RenameFailureScenario::NoChange` → `result: Ok("def old_name():\n    return 1\n".to_string())`
       (i.e. same content as the original, so the rename produces no change).
     - `RenameFailureScenario::AdapterError` → `result: Err(RopeAdapterError::Other("simulated adapter error".into()))`
       (or whatever constructor/variant is appropriate for your `RopeAdapterError`).
   - Calls `execute_request` as before:
     ```rust
     let request = PluginRequest::new("rename", arguments);
     let response = execute_request(&adapter, &request);
     assert!(!response.is_success());
     ```
   - **Asserts diagnostics for each scenario**:
     ```rust
     let messages: Vec<&str> = response
         .diagnostics
         .iter()
         .map(|d| d.message.as_str())
         .collect();

     match scenario {
         RenameFailureScenario::NoChange => {
             assert!(
                 messages.iter().any(|m| m.contains("produced no content changes")),
                 "expected a diagnostic mentioning that the rename produced no content changes, got: {:?}",
                 messages,
             );
         }
         RenameFailureScenario::AdapterError => {
             assert!(
                 messages.iter().any(|m| m.contains("simulated adapter error")),
                 "expected the RopeAdapterError message to surface in diagnostics, got: {:?}",
                 messages,
             );
         }
     }
     ```
3. **Import `RopeAdapterError`** in this test module if it isn’t already:
   ```rust
   use crate::rope_adapter::RopeAdapterError;
   ```
   (Adjust the path to match your actual module layout.)

These changes will ensure the test still checks that both scenarios fail, *and* that the diagnostics are distinct and meaningful, guarding against regressions in error mapping and diagnostic quality.
</issue_to_address>

### Comment 4
<location> `crates/weaverd/src/dispatch/act/refactor/mod.rs:51` </location>
<code_context>
+    ) -> Result<PluginResponse, PluginError>;
+}
+
+/// Dependencies required by the `act refactor` handler.
+pub(crate) struct RefactorDependencies<'a> {
+    workspace_root: &'a Path,
</code_context>

<issue_to_address>
**issue (complexity):** Consider simplifying the new refactor plumbing by removing the RefactorDependencies wrapper and making SandboxRefactorRuntime construction enforce its runner invariant so the hot path no longer deals with Options and extra indirection.

You can flatten a couple of the new abstractions without losing behavior or testability.

### 1) Drop `RefactorDependencies` and pass parameters directly

`RefactorDependencies<'a>` adds an extra layer and lifetime complexity for two values the router already owns. You can pass them directly without losing flexibility, and tests can still wrap them with small helpers in the test module.

```rust
pub fn handle<W: Write>(
    request: &CommandRequest,
    writer: &mut ResponseWriter<W>,
    backends: &mut FusionBackends<SemanticBackendProvider>,
    workspace_root: &Path,
    runtime: &dyn RefactorPluginRuntime,
) -> Result<DispatchResult, DispatchError> {
    // ...
    let file_path = resolve_file(workspace_root, &args.file)?;
    // ...
    let plugin_request = PluginRequest::with_arguments(
        &args.refactoring,
        vec![FilePayload::new(PathBuf::from(&args.file), file_content)],
        plugin_args,
    );

    match runtime.execute(&args.provider, &plugin_request) {
        Ok(response) => handle_plugin_response(response, writer, backends, workspace_root),
        Err(error) => {
            writer.write_stderr(format!(
                "act refactor failed: {error} (provider={}, refactoring={}, file={})\n",
                args.provider, args.refactoring, args.file
            ))?;
            Ok(DispatchResult::with_status(1))
        }
    }
}
```

Call site from the router becomes simpler:

```rust
// in DomainRouter
act::refactor::handle(
    request,
    writer,
    backends,
    &self.workspace_root,
    self.refactor_runtime.as_ref(),
)
```

This removes `RefactorDependencies` entirely while preserving behavior.

### 2) Encode `SandboxRefactorRuntime` invariant and remove internal `Option`s

Once `SandboxRefactorRuntime` is constructed successfully, it should always have a runner. Right now `execute` has to handle a missing runner and re-synthesize an error, which complicates the hot path.

You can make construction fallible and introduce a “disabled” runtime for the failure case:

```rust
pub struct SandboxRefactorRuntime {
    runner: PluginRunner<SandboxExecutor>,
}

impl SandboxRefactorRuntime {
    pub fn from_environment() -> Result<Self, String> {
        let mut registry = PluginRegistry::new();
        let executable = resolve_rope_plugin_path(std::env::var_os(ROPE_PLUGIN_PATH_ENV));
        let metadata =
            PluginMetadata::new(ROPE_PLUGIN_NAME, ROPE_PLUGIN_VERSION, PluginKind::Actuator);
        let manifest = PluginManifest::new(metadata, vec![String::from("python")], executable);

        registry
            .register(manifest)
            .map_err(|e| format!("failed to initialise refactor runtime: {e}"))?;

        Ok(Self {
            runner: PluginRunner::new(registry, SandboxExecutor),
        })
    }
}

impl RefactorPluginRuntime for SandboxRefactorRuntime {
    fn execute(
        &self,
        provider: &str,
        request: &PluginRequest,
    ) -> Result<PluginResponse, PluginError> {
        self.runner.execute(provider, request)
    }
}
```

Then keep the public `default_runtime` behavior by deciding how to handle initialization failure there, e.g. falling back to a noop runtime:

```rust
struct NoopRefactorRuntime {
    message: String,
}

impl RefactorPluginRuntime for NoopRefactorRuntime {
    fn execute(
        &self,
        _provider: &str,
        _request: &PluginRequest,
    ) -> Result<PluginResponse, PluginError> {
        Err(PluginError::Manifest {
            message: self.message.clone(),
        })
    }
}

pub(crate) fn default_runtime() -> Arc<dyn RefactorPluginRuntime + Send + Sync> {
    match SandboxRefactorRuntime::from_environment() {
        Ok(runtime) => Arc::new(runtime),
        Err(msg) => Arc::new(NoopRefactorRuntime { message: msg }),
    }
}
```

This keeps all current behavior (including error reporting when the runtime is unavailable) but simplifies the core path by removing the `Option` juggling and `startup_error` string plumbing from `SandboxRefactorRuntime::execute`.
</issue_to_address>

### Comment 5
<location> `crates/weaver-plugin-rope/src/lib.rs:221` </location>
<code_context>
+        .map_err(|error| format!("invalid plugin request JSON: {error}"))
+}
+
+fn execute_request<R: RopeAdapter>(adapter: &R, request: &PluginRequest) -> PluginResponse {
+    match request.operation() {
+        "rename" => execute_rename(adapter, request),
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring the request/rename execution path to return a Result that is converted to a PluginResponse in one place to simplify control flow and error handling.

You can simplify the control flow and error handling by making the request/rename pipeline return a `Result<PluginResponse, String>` and mapping to a `PluginResponse` only once at the top. This removes several early-return `failure_response` calls and makes the flow easier to follow without changing behavior.

For example:

```rust
type ExecResult = Result<PluginResponse, String>;
```

Change the helpers to return `ExecResult`:

```rust
fn execute_request<R: RopeAdapter>(adapter: &R, request: &PluginRequest) -> ExecResult {
    match request.operation() {
        "rename" => execute_rename(adapter, request),
        other => Err(format!("unsupported refactoring operation '{other}'")),
    }
}

fn execute_rename<R: RopeAdapter>(adapter: &R, request: &PluginRequest) -> ExecResult {
    let (offset, new_name) = parse_rename_arguments(request.arguments())?;

    let file = request
        .files()
        .first()
        .ok_or_else(|| String::from("rename operation requires one file payload"))?;

    let modified = adapter
        .rename(file, offset, &new_name)
        .map_err(|error| error.to_string())?;

    if modified == file.content() {
        return Err(String::from(
            "rename operation produced no content changes",
        ));
    }

    let patch = build_search_replace_patch(file.path(), file.content(), &modified);
    Ok(PluginResponse::success(PluginOutput::Diff { content: patch }))
}
```

Then `run_with_adapter` can centralize the conversion from error string to `PluginResponse`:

```rust
pub fn run_with_adapter<R: RopeAdapter>(
    stdin: &mut impl BufRead,
    stdout: &mut impl Write,
    adapter: &R,
) -> Result<(), PluginDispatchError> {
    let response = match read_request(stdin)
        .and_then(|request| execute_request(adapter, &request))
    {
        Ok(resp) => resp,
        Err(msg) => failure_response(msg),
    };

    let payload =
        serde_json::to_string(&response).map_err(|source| PluginDispatchError::Serialize { source })?;
    stdout
        .write_all(payload.as_bytes())
        .and_then(|_| stdout.write_all(b"\n"))
        .and_then(|_| stdout.flush())
        .map_err(|source| PluginDispatchError::Write { source })
}
```

This keeps all existing behavior (same error messages, same response shape) but reduces duplicated `failure_response(...)` construction and makes the flow from “request → result → response” more linear.
</issue_to_address>

### Comment 6
<location> `crates/weaverd/src/dispatch/act/refactor/behaviour.rs:64` </location>
<code_context>
+    }
+}
+
+struct RefactorWorld {
+    workspace: TempDir,
+    request: CommandRequest,
</code_context>

<issue_to_address>
**issue (complexity):** Consider replacing the BDD-style `RefactorWorld` and step macros with simple helper functions and rstest-based parameterized tests to make the refactor handler tests more direct and readable while preserving behavior.

You can keep all existing behavior while significantly simplifying the tests by:

1. Removing the BDD world object and macros.
2. Using a small helper function to execute the handler.
3. Using parameterized tests (`rstest`) to cover the different modes and expectations.

Concretely, you can replace `RefactorWorld` + `given/when/then` with something like this:

```rust
#[derive(Clone, Copy)]
enum RuntimeMode {
    DiffSuccess,
    RuntimeError,
    MalformedDiff,
}

struct MockRuntime {
    mode: RuntimeMode,
}

impl RefactorPluginRuntime for MockRuntime {
    fn execute(
        &self,
        _provider: &str,
        _request: &PluginRequest,
    ) -> Result<PluginResponse, PluginError> {
        match self.mode {
            RuntimeMode::DiffSuccess => Ok(PluginResponse::success(PluginOutput::Diff {
                content: String::from(VALID_DIFF),
            })),
            RuntimeMode::RuntimeError => Err(PluginError::NotFound {
                name: String::from("rope"),
            }),
            RuntimeMode::MalformedDiff => Ok(PluginResponse::success(PluginOutput::Diff {
                content: String::from(MALFORMED_DIFF),
            })),
        }
    }
}

fn command_request(args: &[&str]) -> CommandRequest {
    CommandRequest {
        command: CommandDescriptor {
            domain: "act".into(),
            operation: "refactor".into(),
        },
        arguments: args.iter().map(|s| s.to_string()).collect(),
        patch: None,
    }
}

fn execute_refactor(
    workspace: &Path,
    request: CommandRequest,
    runtime_mode: RuntimeMode,
) -> (Result<i32, DispatchError>, String) {
    let runtime = MockRuntime { mode: runtime_mode };
    let mut output = Vec::new();
    let mut writer = ResponseWriter::new(&mut output);
    let mut backends = build_backends();

    let result = handle(
        &request,
        &mut writer,
        &mut backends,
        RefactorDependencies::new(workspace, &runtime),
    )
    .map(|dispatch| dispatch.status);

    let response_stream = String::from_utf8(output).expect("response utf8");
    (result, response_stream)
}
```

Then express the scenarios as small parameterized tests instead of BDD steps/stateful `RefactorWorld`:

```rust
use rstest::rstest;

#[rstest]
fn refactor_succeeds_and_updates_file() {
    let workspace = TempDir::new().expect("workspace");
    let notes = workspace.path().join("notes.txt");
    std::fs::write(&notes, ORIGINAL_CONTENT).unwrap();

    let request = command_request(&[
        "--provider", "rope",
        "--refactoring", "rename",
        "--file", "notes.txt",
        "offset=1",
        "new_name=woven",
    ]);

    let (result, stderr) =
        execute_refactor(workspace.path(), request, RuntimeMode::DiffSuccess);

    assert_eq!(result.expect("status"), 0);
    assert_eq!(std::fs::read_to_string(&notes).unwrap(), UPDATED_CONTENT);
    assert!(
        stderr.contains("some expected log text"),
        "stderr: {stderr}"
    );
}

#[rstest]
fn missing_provider_rejected_as_invalid_arguments() {
    let workspace = TempDir::new().expect("workspace");
    let notes = workspace.path().join("notes.txt");
    std::fs::write(&notes, ORIGINAL_CONTENT).unwrap();

    let request = command_request(&[
        "--refactoring", "rename",
        "--file", "notes.txt",
    ]);

    let (result, _stderr) =
        execute_refactor(workspace.path(), request, RuntimeMode::DiffSuccess);

    assert!(matches!(result, Err(DispatchError::InvalidArguments { .. })));
}
```

If you want to keep table-driven coverage for runtime variants without extra macros:

```rust
#[rstest]
#[case(RuntimeMode::RuntimeError, 1, true, ORIGINAL_CONTENT)]
#[case(RuntimeMode::MalformedDiff, 1, true, ORIGINAL_CONTENT)]
fn refactor_failure_modes(
    #[case] mode: RuntimeMode,
    #[case] expected_status: i32,
    #[case] expect_error_in_stderr: bool,
    #[case] expected_file_content: &str,
) {
    let workspace = TempDir::new().expect("workspace");
    let notes = workspace.path().join("notes.txt");
    std::fs::write(&notes, ORIGINAL_CONTENT).unwrap();

    let request = command_request(&[
        "--provider", "rope",
        "--refactoring", "rename",
        "--file", "notes.txt",
        "offset=1",
        "new_name=woven",
    ]);

    let (result, stderr) = execute_refactor(workspace.path(), request, mode);

    assert_eq!(result.expect("status"), expected_status);
    assert_eq!(std::fs::read_to_string(&notes).unwrap(), expected_file_content);
    if expect_error_in_stderr {
        assert!(
            stderr.contains("expected error text"),
            "stderr: {stderr}"
        );
    }
}
```

This keeps:

- Runtime modes and behaviors identical.
- Validation of status codes, file contents, and stderr contents.

But it removes:

- Global mutable `RefactorWorld` with many responsibilities.
- BDD macros and step wiring (`given`/`when`/`then`).
- Indirection between feature file and actual assertions.

The net result is fewer moving parts and easier-to-follow tests while preserving all behavior.
</issue_to_address>

### Comment 7
<location> `crates/weaver-plugin-rope/src/lib.rs:115` </location>
<code_context>
+        #[source]
+        source: std::io::Error,
+    },
+    /// Serialising the response payload failed.
+    #[error("failed to serialize plugin response: {source}")]
+    Serialize {
</code_context>

<issue_to_address>
**suggestion (review_instructions):** Doc comment uses “Serialising” with -ising; per en-GB-oxendict guidance this should use the -ize form (“Serializing”).

The style guide calls for -ize spellings in comments. Here, changing “Serialising the response payload failed.” to “Serializing the response payload failed.” would align with the en-GB-oxendict (-ize / -yse / -our) requirement.

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

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

**Instructions:**
Comments must use en-GB-oxendict (-ize / -yse / -our) spelling and grammar.

</details>
</issue_to_address>

### Comment 8
<location> `crates/weaverd/src/dispatch/act/refactor/mod.rs:338` </location>
<code_context>
+        return Ok(DispatchResult::with_status(1));
+    }
+
+    match response.output() {
+        PluginOutput::Diff { content } => {
+            forward_diff_to_apply_patch(content, writer, backends, workspace_root)
</code_context>

<issue_to_address>
**suggestion (review_instructions):** This match has three branches; the instructions ask for conditionals with more than two branches to be factored into a predicate/helper function.

`match response.output()` distinguishes three cases (`Diff`, `Analysis`, `Empty`). The review guidelines request that conditionals with more than two branches be moved into a predicate function. One way to align would be to extract a helper, e.g. something like `fn as_diff_content(response: &PluginResponse) -> Option<&str>` and then use an `if let`/`else` around that, keeping the main handler logic at two branches.

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

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

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

</details>
</issue_to_address>

### Comment 9
<location> `crates/weaverd/src/dispatch/act/refactor/behaviour.rs:50` </location>
<code_context>
+        _provider: &str,
+        _request: &PluginRequest,
+    ) -> Result<PluginResponse, PluginError> {
+        match self.mode {
+            RuntimeMode::DiffSuccess => Ok(PluginResponse::success(PluginOutput::Diff {
+                content: String::from(VALID_DIFF),
</code_context>

<issue_to_address>
**suggestion (review_instructions):** The match on `self.mode` has three branches, which the style instructions say should be pulled out into a predicate/helper when there are more than two branches.

`MockRuntime::execute` has a three-way `match` on `RuntimeMode`. To follow the “>2 branches into a predicate function” rule, you could extract the mode-to-response mapping into a small helper (or predicate returning an enum/Result) and keep `execute` itself to two branches (e.g. `if is_diff_success_mode(...) { ... } else { ... }`).

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

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

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

</details>
</issue_to_address>

### Comment 10
<location> `docs/execplans/3-1-1a-plugin-for-rope.md:3` </location>
<code_context>
+# Implement the first actuator plugin for `rope`
+
+This ExecPlan is a living document. The sections `Constraints`, `Tolerances`,
+`Risks`, `Progress`, `Surprises & Discoveries`, `Decision Log`, and
+`Outcomes & Retrospective` must be kept up to date as work proceeds.
</code_context>

<issue_to_address>
**suggestion (review_instructions):** ExecPlan is introduced as an acronym without first being expanded.

To follow the acronym rule, expand ExecPlan on first use, for example: "This Execution Plan (ExecPlan) is a living document." Subsequent uses can then use the short form.

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

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

### Comment 11
<location> `docs/execplans/3-1-1a-plugin-for-rope.md:110` </location>
<code_context>
+
+## Surprises & Discoveries
+
+- Observation: project memory MCP resources were not available in this session
+  (`list_mcp_resources` returned no servers/resources). Evidence: tool output
+  returned empty resource lists. Impact: planning relied on repository docs and
</code_context>

<issue_to_address>
**suggestion (review_instructions):** MCP is used without being expanded on first mention.

Consider expanding MCP on first use, for example: "project memory Model Context Protocol (MCP) resources". Afterwards the short form MCP is fine.

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

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

### Comment 12
<location> `docs/execplans/3-1-1a-plugin-for-rope.md:43` </location>
<code_context>
+- Do not bypass the Double-Lock harness for plugin-produced edits.
+- Continue using `rstest-bdd` v0.5.0 and write new behavioural tests with
+  mutable world fixtures (`&mut World`) for new scenarios.
+- Add e2e command ergonomics tests in `crates/weaver-e2e/` using `assert_cmd`
+  and `insta` snapshots for CLI usage flows.
+- Keep module-level `//!` comments and rustdoc for public items; follow
</code_context>

<issue_to_address>
**suggestion (review_instructions):** The acronym e2e is used before being expanded to "end-to-end".

Please expand e2e on first use, for example: "Add end-to-end (e2e) command ergonomics tests...". Later references can use just e2e.

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

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

### Comment 13
<location> `docs/execplans/3-1-1a-plugin-for-rope.md:203` </location>
<code_context>
+
+Add new crate `crates/weaver-plugin-rope/` (workspace member) that:
+
+- reads one `PluginRequest` JSONL line from stdin,
+- validates operation + arguments,
+- performs rope-backed refactoring via an adapter boundary,
</code_context>

<issue_to_address>
**suggestion (review_instructions):** JSONL is used without being expanded on first use in this document.

Consider expanding JSONL the first time it appears, for example: "JSON Lines (JSONL)". After that, JSONL alone is fine.

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

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 15, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

I'll check the current state of the codebase to verify which comments have been resolved.


🏁 Script executed:

#!/bin/bash
# Check Overall Comment 1: resolve_rope_plugin_path current_dir() handling
echo "=== Checking resolve_rope_plugin_path ==="
rg -A 15 "fn resolve_rope_plugin_path" crates/weaverd/src/dispatch/act/refactor/mod.rs

Length of output: 646


🏁 Script executed:

#!/bin/bash
# Check Overall Comment 2: PythonRopeAdapter::rename absolute_path usage
echo "=== Checking PythonRopeAdapter::rename for absolute_path ==="
rg -A 30 "impl RopeAdapter for PythonRopeAdapter" crates/weaver-plugin-rope/src/lib.rs

Length of output: 1273


🏁 Script executed:

#!/bin/bash
# Check Overall Comment 3: FakeDaemon::join thread handling
echo "=== Checking FakeDaemon::join ==="
rg -A 10 "fn join" crates/weaver-e2e/tests/refactor_rope_cli_snapshots.rs

Length of output: 268


🏁 Script executed:

#!/bin/bash
# Check Comment 1: Tests for stdin/stdout dispatch layer
echo "=== Checking for stdin/stdout dispatch tests ==="
rg -C 3 "run_with_adapter.*empty|malformed.*run_with_adapter" crates/weaver-plugin-rope/src/tests/mod.rs

Length of output: 203


🏁 Script executed:

#!/bin/bash
# Check Comment 2: Tests for argument parsing edge cases
echo "=== Checking for parse_rename_arguments edge case tests ==="
rg -C 3 "parse_rename_arguments_edge_cases|negative.*offset|empty.*new_name" crates/weaver-plugin-rope/src/tests/mod.rs

Length of output: 869


🏁 Script executed:

#!/bin/bash
# Check Comment 3: Differentiate diagnostics in tests
echo "=== Checking RenameFailureScenario ==="
rg -C 5 "RenameFailureScenario|rename_non_mutating_or_error_returns_failure" crates/weaver-plugin-rope/src/tests/mod.rs

Length of output: 391


🏁 Script executed:

#!/bin/bash
# Check Comment 4: RefactorDependencies wrapper
echo "=== Checking for RefactorDependencies ==="
rg -C 3 "struct RefactorDependencies|RefactorDependencies" crates/weaverd/src/dispatch/act/refactor/mod.rs | head -30

Length of output: 289


🏁 Script executed:

#!/bin/bash
# Check Comment 5: execute_request return type
echo "=== Checking execute_request signature ==="
rg -A 5 "fn execute_request" crates/weaver-plugin-rope/src/lib.rs

Length of output: 248


🏁 Script executed:

#!/bin/bash
# Check Comment 6: RefactorWorld BDD style
echo "=== Checking for RefactorWorld ==="
rg -C 3 "struct RefactorWorld" crates/weaverd/src/dispatch/act/refactor/behaviour.rs

Length of output: 150


🏁 Script executed:

#!/bin/bash
# Check Comment 7: "Serialising" vs "Serializing"
echo "=== Checking for Serialising spelling ==="
rg "Serialising|Serializing" crates/weaver-plugin-rope/src/lib.rs

Length of output: 90


🏁 Script executed:

#!/bin/bash
# Check Comment 8: 3-branch match in handle_plugin_response
echo "=== Checking handle_plugin_response match branches ==="
rg -A 15 "fn handle_plugin_response|match response.output\(\)" crates/weaverd/src/dispatch/act/refactor/mod.rs | head -40

Length of output: 1200


🏁 Script executed:

#!/bin/bash
# Check Comment 9: MockRuntime 3-branch match
echo "=== Checking MockRuntime execute method ==="
rg -A 15 "impl RefactorPluginRuntime for MockRuntime" crates/weaverd/src/dispatch/act/refactor/behaviour.rs

Length of output: 698


🏁 Script executed:

#!/bin/bash
# Check Comments 10-13: Acronym expansions in docs
echo "=== Checking acronym expansions ==="
echo "--- ExecPlan expansion ---"
grep -n "ExecPlan\|Execution Plan" docs/execplans/3-1-1a-plugin-for-rope.md | head -5
echo "--- MCP expansion ---"
grep -n "MCP\|Model Context Protocol" docs/execplans/3-1-1a-plugin-for-rope.md | head -5
echo "--- e2e expansion ---"
grep -n "e2e\|end-to-end" docs/execplans/3-1-1a-plugin-for-rope.md | head -5
echo "--- JSONL expansion ---"
grep -n "JSONL\|JSON Lines" docs/execplans/3-1-1a-plugin-for-rope.md | head -5

Length of output: 1011


@leynos, I've checked the current state of the codebase. Here's the resolution status:

Resolved (10 comments)

Individual Comments:

  • Comment 1 (stdin/stdout dispatch tests): Added test run_with_adapter_returns_failure_for_empty_stdin
  • Comment 2 (argument parsing edge cases): Added tests for empty new_name, negative offset, etc.
  • Comment 3 (diagnostics differentiation): Implemented with FailureScenario enum
  • Comment 4 (RefactorDependencies): Removed; parameters now passed directly (code comment confirms "unbundled per review")
  • Comment 5 (execute_request return type): Changed to return Result<PluginResponse, String>
  • Comment 7 (spelling): Changed to "Serializing"
  • Comment 8 (3-branch match): Combined Analysis and Empty cases using pattern matching, reducing to 2 branches
  • Comment 10 (ExecPlan): Expanded to "Execution Plan (ExecPlan)"
  • Comment 11 (MCP): Expanded to "Model Context Protocol (MCP)"
  • Comment 12 (e2e): Expanded to "end-to-end (e2e)"
  • Comment 13 (JSONL): Expanded to "JSON Lines (JSONL)"

🟡 Partially Resolved (1 comment)

Overall Comment 1 (resolve_rope_plugin_path): Improved by logging a warning when current_dir() fails, but still uses the path as-is rather than surfacing an explicit error as suggested. The current approach is reasonable for runtime robustness, though it differs from the original suggestion.

Not Resolved (4 comments)

Overall Comments:

  • Overall Comment 2: PythonRopeAdapter::rename still computes _absolute_path but never uses it (dead code)
  • Overall Comment 3: FakeDaemon::join still drops the thread join result with .ok(), not surfacing panics

Individual Comments:

  • Comment 6: RefactorWorld BDD-style tests still present (not converted to simpler rstest-based tests)
  • Comment 9: MockRuntime::execute still has 3-way match on RuntimeMode

The majority of the review feedback has been addressed, particularly the testing gaps, complexity issues, and documentation improvements. The remaining items are suggestions for further refinement rather than critical issues.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Feb 16, 2026

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

crates/weaver-plugin-rope/src/tests/mod.rs

Comment on lines +306 to +327

fn rename_negative_offset_string_returns_error() {
    let mut arguments = HashMap::new();
    arguments.insert(
        String::from("offset"),
        serde_json::Value::String(String::from("-1")),
    );
    arguments.insert(
        String::from("new_name"),
        serde_json::Value::String(String::from("new_name")),
    );

    let adapter = MockAdapter {
        result: Ok(String::from("unused")),
    };

    let err = execute_request(&adapter, &request_with_args(arguments))
        .expect_err("negative offset should fail");
    assert!(
        err.contains("non-negative integer"),
        "expected error mentioning 'non-negative integer', got: {err}"
    );
}

❌ New issue: Code Duplication
The module contains 9 functions with similar structure: rename_empty_new_name_returns_error,rename_missing_offset_returns_error,rename_negative_offset_string_returns_error,rename_offset_as_boolean_returns_error and 5 more functions

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Feb 16, 2026

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

crates/weaverd/src/dispatch/act/refactor/mod.rs

Comment on file

    writer: &mut ResponseWriter<W>,
    backends: &mut FusionBackends<SemanticBackendProvider>,
    workspace_root: &Path,
    runtime: &dyn RefactorPluginRuntime,

❌ New issue: Excess Number of Function Arguments
handle has 5 arguments, max arguments = 4

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

… tests

- Replace manual mock adapter with `mockall` mocks in `behaviour.rs` tests.
- Parametrize rename argument validation tests with `rstest`.
- Simplify test setup by extracting argument fixtures.
- Add tests for edge cases: missing, boolean, and negative `offset`, empty `new_name`.
- Improve error message checks in rename tests.

This enhances test coverage, clarity, and maintenance by using mockall for mocking and rstest for parameterized testing.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@leynos leynos changed the title Add rope actuator plugin with wiring, tests, and docs Add rope actuator plugin with runtime wiring, tests, and docs Feb 16, 2026
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.


In `@crates/weaver-e2e/tests/refactor_rope_cli_snapshots.rs`:
- Around line 109-111: The current code silently ignores a poisoned mutex by
using `if let Ok(mut guard) = requests.lock()`; replace this with a fail-fast
call so tests surface locking failures — e.g., change to `let mut guard =
requests.lock().expect("failed to lock requests mutex");
guard.push(parsed_request.clone());` referencing the `requests.lock()` call and
the `parsed_request.clone()` usage so a poisoned mutex causes the test to panic
with a clear message.
- Around line 64-69: The tests currently hide a poisoned Mutex by calling
unwrap_or_default() in the requests() method; update requests() to call expect()
on the result of self.requests.lock() (e.g.,
self.requests.lock().expect("requests mutex poisoned")) so a poisoned mutex
surfaces as a test failure instead of returning an empty Vec; keep the rest of
requests() logic (cloning the items) unchanged and reference the requests()
function and the self.requests Mutex to locate the change.
- Around line 119-128: Extract the match that builds payload into a new helper
function response_payload_for_operation(operation: &str) -> String, move the
three-branch logic (the "get-definition", "refactor", and default cases using
json!(...).to_string()) into that function, and replace the inline match with a
call to response_payload_for_operation(operation) where payload is currently
assigned; ensure the helper returns a String and uses the same json! constructs
so behavior is unchanged.

In `@crates/weaver-plugin-rope/src/lib.rs`:
- Around line 65-66: Centralize path validation by calling
validate_relative_path(file.path()) at the dispatch boundary inside
execute_rename so all adapters (including custom ones) are protected from
traversal/absolute paths; then remove the duplicate validate_relative_path
invocation from PythonRopeAdapter (and any other adapter-level checks you find
in the same module) to keep the invariant enforced only in execute_rename and
avoid redundant validation.
- Around line 237-241: The current rename handling silently takes the first
payload via request.files().first() and assigns it to file, but we must enforce
exactly one file; change the logic that creates file (replace the .first() use)
to check request.files().len() == 1 and return an Err (e.g.,
String::from("rename operation requires exactly one file payload")) when the
count is not 1, then safely extract that single entry into the variable file for
the rename operation so extra inputs are rejected rather than ignored.

In `@crates/weaver-plugin-rope/src/tests/mod.rs`:
- Around line 8-26: The tests currently define a handwritten MockAdapter
implementing RopeAdapter (struct MockAdapter and its rename method); replace
this with a mockall-generated mock using mockall::mock! for RopeAdapter and add
a small helper like adapter_returning(Ok(...)) / adapter_returning(Err(...))
used across tests; update tests to instantiate the mock via the mockall macro
and configure its rename expectation/return value instead of using MockAdapter,
and remove the manual MockAdapter type and its impl to align with project
mocking conventions.

In `@crates/weaverd/src/dispatch/act/refactor/behaviour.rs`:
- Around line 157-164: The test helper build_backends currently hardcodes a
Unix-only socket path via SocketEndpoint::unix("/tmp/weaver-test/socket.sock"),
which breaks non-Unix CI; change build_backends (and the Config construction) to
create a temporary directory (e.g., via TempDir) and use a socket path inside
it, or alternatively gate the function with #[cfg(unix)] so it only compiles on
Unix. Update references to Config and SocketEndpoint::unix in build_backends to
accept the temp path (or add a #[cfg(unix)] wrapper) so tests are cross-platform
safe.
- Around line 52-65: The function request_has_required_flags currently iterates
request.arguments three times to check for "--provider", "--refactoring", and
"--file"; refactor it to perform a single pass by either converting
request.arguments to a collection that supports contains (e.g., a HashSet) or
iterating once and checking membership against a slice of required flags
(["--provider","--refactoring","--file"]), then return whether all required
flags were found; update the function request_has_required_flags to use that
single-pass approach for clarity and performance.

Comment thread crates/weaver-e2e/tests/refactor_rope_cli_snapshots.rs
Comment thread crates/weaver-e2e/tests/refactor_rope_cli_snapshots.rs Outdated
Comment thread crates/weaver-e2e/tests/refactor_rope_cli_snapshots.rs Outdated
Comment thread crates/weaver-plugin-rope/src/lib.rs Outdated
Comment thread crates/weaver-plugin-rope/src/lib.rs Outdated
Comment thread crates/weaver-plugin-rope/src/tests/mod.rs
Comment thread crates/weaverd/src/dispatch/act/refactor/behaviour.rs
Comment thread crates/weaverd/src/dispatch/act/refactor/behaviour.rs Outdated
- Introduce RefactorContext struct to bundle dependencies for refactor operations.
- Update act refactor handle function and callers to use RefactorContext.
- Pass socket path dynamically in refactor backend tests to avoid hardcoded paths.
- Refactor rope plugin to improve argument validation and mocking in tests.
- Enhance test suite for act refactor with rstest and fixtures.
- Use clippy expect annotations in weaver-e2e test for clearer error handling.
- Improve code clarity and error propagation in act refactor command handling.

This improves modularity, testability, and code clarity in the act refactor functionality.

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

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.


In `@crates/weaver-e2e/tests/refactor_rope_cli_snapshots.rs`:
- Around line 83-96: serve_requests currently blocks forever on
listener.accept(); change it to use non-blocking accept with a deadline so the
test fails fast if the CLI never connects: call listener.set_nonblocking(true)
before the loop, compute a deadline (e.g. Instant::now() +
Duration::from_secs(...)) and inside the for loop attempt listener.accept(),
treating Err(e) where e.kind() == io::ErrorKind::WouldBlock by checking the
deadline, sleeping briefly (e.g. thread::sleep(Duration::from_millis(10))) and
retrying until the deadline, and treating other errors as failures; reference
the serve_requests function and the listener.accept() call when making these
changes.

In `@crates/weaver-plugin-rope/src/lib.rs`:
- Around line 262-293: The parse_rename_arguments function currently allows
numeric new_name values because json_value_to_string accepts numbers; change it
to enforce a string-only contract by extracting new_name with
new_name_value.as_str().ok_or_else(...) (or use a new helper that only accepts
strings) and validate non-empty trimmed string, leaving json_value_to_string
unchanged for offset parsing (or use it only for offset). Update references in
parse_rename_arguments to stop converting numeric new_name values to strings so
the error "new_name argument must be a string" is accurate.

Comment thread crates/weaver-e2e/tests/refactor_rope_cli_snapshots.rs
Comment thread crates/weaver-plugin-rope/src/lib.rs
…daemon

Introduce a helper function accept_before_deadline to poll TCP listener.accept() in non-blocking mode until a connection arrives or a timeout elapses. This replaces the previous blocking accept call and adds a deadline mechanism for more robust test daemon behavior.

Additionally, update the new_name argument parsing to accept only string types strictly and add test cases validating rejection of non-string new_name values.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@leynos leynos changed the title Add rope actuator plugin with runtime wiring, tests, and docs Add rope actuator plugin with runtime wiring, e2e tests, and docs Feb 16, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.


In `@crates/weaver-e2e/tests/refactor_rope_cli_snapshots.rs`:
- Around line 282-287: The test spawns bash with a login flag "-l" via
Command::new("bash").args(["-lc", shell_script]) which pulls in user profile
files and causes non-determinism; change the invocation to drop the "-l" and use
only "-c" so the Command uses .args(["-c", shell_script]) (locate the
Command::new("bash") call and update the args passed to remove "-l").

In `@crates/weaver-plugin-rope/src/tests/mod.rs`:
- Around line 76-78: The function remove_offset unnecessarily wraps
HashMap::remove in drop; remove the explicit drop and simply call
arguments.remove("offset") (or ignore the return value directly) inside
remove_offset to let the Option returned by HashMap::remove be dropped
implicitly; update the remove_offset function body to call
arguments.remove("offset") without drop.

Comment thread crates/weaver-e2e/tests/refactor_rope_cli_snapshots.rs
Comment thread crates/weaver-plugin-rope/src/tests/mod.rs
- Changed bash command argument from '-lc' to '-c' in e2e test to correct shell invocation.
- Removed unnecessary drop() call around HashMap::remove in plugin-rope tests for clarity.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@leynos leynos merged commit da26739 into main Feb 17, 2026
9 of 10 checks passed
@leynos leynos deleted the plugin-rope-python-refactor-wuecqo branch February 17, 2026 00:33
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.

1 participant