Skip to content

Replace sleeps with polling in worker tests#55

Merged
leynos merged 4 commits intomainfrom
codex/add-generic-polling-utility-for-tests
Aug 6, 2025
Merged

Replace sleeps with polling in worker tests#55
leynos merged 4 commits intomainfrom
codex/add-generic-polling-utility-for-tests

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Aug 4, 2025

Summary

  • add generic poll_until helper for deterministic async tests
  • poll worker tests for server activity instead of fixed sleeps

Testing

  • make fmt
  • make lint
  • make test

https://chatgpt.com/codex/tasks/task_e_689132602eb4832290a3f62573192158

Summary by Sourcery

Add a generic poll_until helper for deterministic asynchronous tests and refactor worker tests to use polling instead of fixed sleeps for improved reliability.

New Features:

  • Introduce poll_until helper to asynchronously poll a predicate until a timeout or condition is met

Enhancements:

  • Replace hardcoded sleeps in worker tests with poll_until to await server request arrivals
  • Update util.rs imports to include timeout and interval for polling support

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Aug 4, 2025

Reviewer's Guide

This PR replaces brittle fixed-duration sleeps in worker tests with a deterministic polling approach by introducing a generic poll_until helper and updating tests to leverage it.

Class diagram for the new poll_until helper

classDiagram
    class poll_until {
        <<async fn>>
        +poll_until(timeout: Duration, interval: Duration, condition: async Fn() -> bool) bool
    }
Loading

File-Level Changes

Change Details Files
Add a reusable poll_until helper for async tests
  • Import interval and timeout from tokio::time
  • Implement poll_until that loops on an async predicate until timeout
  • Provide doc comments and usage example for clarity
test-support/src/util.rs
Replace fixed sleeps in worker tests with poll_until polling
  • Import poll_until in the test module
  • Clone the server Arc for use inside the polling closure
  • Use poll_until with timeout and interval instead of sleep
  • Assert on polling result before verifying request count
crates/comenqd/src/daemon.rs

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
Contributor

coderabbitai Bot commented Aug 4, 2025

Summary by CodeRabbit

  • Tests
    • Improved test reliability by replacing fixed sleep delays with an active polling mechanism to ensure conditions are met before assertions.
    • Added a utility function for asynchronous polling in test support to facilitate more robust and responsive test behaviour.

Walkthrough

Replace static sleep delays in worker tests with an active polling mechanism that waits until the mock server receives the expected request or a timeout occurs. Introduce a new asynchronous poll_until utility function to support this polling pattern, including documentation and usage examples.

Changes

Cohort / File(s) Change Summary
Worker Test Polling Update
crates/comenqd/src/daemon.rs
Swap fixed sleep in worker tests for polling using poll_until, ensuring tests proceed only when the mock server receives a request.
Async Polling Utility
test-support/src/util.rs
Add poll_until async utility function with documentation and example. Update imports for interval and timeout.

Sequence Diagram(s)

sequenceDiagram
    participant Test as Test Case
    participant Server as Mock Server
    participant Worker as Worker Task
    participant Util as poll_until

    Test->>Worker: Start worker task
    loop Poll until request received or timeout
        Test->>Util: Call poll_until
        Util->>Server: Check number of received requests
        alt Request received
            Util-->>Test: Return true
        else Not yet
            Util-->>Test: Wait and retry
        end
    end
    Test->>Server: Assert received request
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

Poem

Await no longer in static delay,
Now polling leads the worker's way.
With poll_until, the tests awake,
To catch the server for correctness’ sake.
Code now listens, keen and spry—
Asynchronous patience, oh my!
🕰️✨


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 66dde2b and 532ea4d.

📒 Files selected for processing (2)
  • crates/comenqd/src/daemon.rs (4 hunks)
  • test-support/src/util.rs (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit Inference Engine (AGENTS.md)

**/*.rs: Clippy warnings MUST be disallowed.
Fix any warnings emitted during tests in the code itself rather than silencing them.
Where a function is too long, extract meaningfully named helper functions adhering to separation of concerns and CQRS.
Where a function has too many parameters, group related parameters in meaningfully named structs.
Where a function is returning a large error consider using Arc to reduce the amount of data returned.
Write unit and behavioural tests for new functionality. Run both before and after making any change.
Every module must begin with a module level (//! ) comment explaining the module's purpose and utility.
Document public APIs using Rustdoc comments (///) so documentation can be generated with cargo doc.
Prefer immutable data and avoid unnecessary mut bindings.
Handle errors with the Result type instead of panicking where feasible.
Avoid unsafe code unless absolutely necessary and document any usage clearly.
Place function attributes after doc comments.
Do not use return in single-line functions.
Use predicate functions for conditional criteria with more than two branches.
Lints must not be silenced except as a last resort.
Lint rule suppressions must be tightly scoped and include a clear reason.
Prefer expect over allow.
Use rstest fixtures for shared setup.
Replace duplicated tests with #[rstest(...)] parameterised cases.
Prefer mockall for mocks/stubs.
Prefer .expect() over .unwrap().
Use concat!() to combine long string literals rather than escaping newlines with a backslash.
Prefer semantic error enums. Derive std::error::Error (via the thiserror crate) for any condition the caller might inspect, retry, or map to an HTTP status.
Use an opaque error only at the app boundary. Use eyre::Report for human-readable logs; these should not be exposed in public APIs.
Never export the opaque type from a library. Convert to domain enums at API boundaries, and to eyre only in the main main() entrypoint or top-level async task.

Files:

  • crates/comenqd/src/daemon.rs
  • test-support/src/util.rs

⚙️ CodeRabbit Configuration File

**/*.rs: * Seek to keep the cyclomatic complexity of functions no more than 12.

  • Adhere to single responsibility and CQRS

  • Place function attributes after doc comments.

  • Do not use return in single-line functions.

  • Move conditionals with >2 branches into a predicate function.

  • Avoid unsafe unless absolutely necessary.

  • Every module must begin with a //! doc comment that explains the module's purpose and utility.

  • Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar

  • Lints must not be silenced except as a last resort.

    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.

  • Replace duplicated tests with #[rstest(...)] parameterised cases.

  • Prefer mockall for mocks/stubs.

  • Prefer .expect() over .unwrap()

  • Ensure that any API or behavioural changes are reflected in the documentation in docs/

  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/

  • Files must not exceed 400 lines in length

    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.

Files:

  • crates/comenqd/src/daemon.rs
  • test-support/src/util.rs
🔇 Additional comments (6)
test-support/src/util.rs (2)

7-7: LGTM! Import additions support the new polling functionality.

The additional imports for interval and timeout are correctly scoped and necessary for the poll_until implementation.


42-105: Excellent implementation of the polling utility function.

The poll_until function is well-designed with:

  • Comprehensive documentation including practical examples
  • Proper handling of the immediate predicate check to avoid unnecessary delays
  • Clear type constraints for the async closure
  • Appropriate use of Tokio's timeout mechanism

The implementation correctly addresses the timing issue from the past review by checking the predicate before entering the polling loop.

crates/comenqd/src/daemon.rs (4)

290-290: LGTM! Import correctly brings in the polling utility.

The import of poll_until from the test support utilities is appropriate and necessary for the new polling-based test approach.


336-356: Well-implemented polling helper for test determinism.

The wait_for_requests function effectively replaces brittle sleep-based timing with active condition polling:

  • Uses appropriate timeout (2 seconds) and polling interval (20ms)
  • Correctly wraps the server in Arc for async closure sharing
  • Includes necessary 200ms delay after polling to allow worker completion
  • Provides clear return semantics

This addresses the race condition mentioned in the PR objectives where workers were aborted immediately after HTTP request detection.


508-525: Improved test reliability with polling-based synchronisation.

The test now uses deterministic polling instead of fixed sleeps:

  • Server wrapped in Arc for proper sharing
  • Clear assertion messages explaining expected behaviour
  • Proper verification of both request reception and queue state

The enhanced assertion messages improve debugging when tests fail.


536-553: Consistent polling approach for error scenario testing.

The error case test maintains the same improvements as the success case:

  • Deterministic polling replaces brittle timing
  • Enhanced assertion messages for better debugging
  • Proper verification of request attempts and queue retention

The consistency between success and error test patterns is excellent.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/add-generic-polling-utility-for-tests

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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 @leynos - I've reviewed your changes - here's some feedback:

  • Consider invoking the predicate immediately before the first interval tick in poll_until to avoid an unnecessary initial delay.
  • The polling logic in your worker tests is repeated; you could extract it into a small helper to DRY up the tests.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Consider invoking the predicate immediately before the first interval tick in poll_until to avoid an unnecessary initial delay.
- The polling logic in your worker tests is repeated; you could extract it into a small helper to DRY up the tests.

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.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Aug 4, 2025

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Aug 4, 2025

✅ Actions performed

Review triggered.

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b8510d5 and 45c0efe.

📒 Files selected for processing (2)
  • crates/comenqd/src/daemon.rs (3 hunks)
  • test-support/src/util.rs (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

⚙️ CodeRabbit Configuration File

**/*.rs: * Seek to keep the cyclomatic complexity of functions no more than 12.

  • Adhere to single responsibility and CQRS

  • Place function attributes after doc comments.

  • Do not use return in single-line functions.

  • Move conditionals with >2 branches into a predicate function.

  • Avoid unsafe unless absolutely necessary.

  • Every module must begin with a //! doc comment that explains the module's purpose and utility.

  • Comments and docs must follow en-GB-oxendict (-ize / -our) spelling and grammar

  • Lints must not be silenced except as a last resort.

    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.

  • Replace duplicated tests with #[rstest(...)] parameterised cases.

  • Prefer mockall for mocks/stubs.

  • Prefer .expect() over .unwrap()

  • Ensure that any API or behavioural changes are reflected in the documentation in docs/

  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/

  • Files must not exceed 400 lines in length

    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.

Files:

  • crates/comenqd/src/daemon.rs
  • test-support/src/util.rs
🔇 Additional comments (4)
test-support/src/util.rs (1)

7-7: LGTM on the import additions.

The addition of interval and timeout imports is correctly placed and necessary for the new poll_until function.

crates/comenqd/src/daemon.rs (3)

290-290: LGTM on the poll_until import.

The import is correctly placed and necessary for the test improvements.


486-507: Excellent improvement to test determinism.

The replacement of fixed sleep with condition-based polling using poll_until significantly improves test reliability. The implementation correctly:

  • Wraps the server in Arc for sharing with the async closure
  • Uses appropriate timeout (2 seconds) and polling interval (20ms)
  • Checks for exactly one received request as the success condition
  • Maintains existing assertions after polling

525-546: LGTM on the error case polling implementation.

The polling logic is consistently implemented for the error scenario, correctly checking that the worker attempts to post (receives one request) before asserting the queue still contains items due to the failure.

Comment thread test-support/src/util.rs
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Aug 5, 2025

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

Run leynos/shared-actions/.github/actions/generate-coverage@c6559452842af6a83b83429129dccaf910e34562
Run astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc
Trying to find version for uv in: /home/runner/work/comenq/comenq/uv.toml
Could not find file: /home/runner/work/comenq/comenq/uv.toml
Trying to find version for uv in: /home/runner/work/comenq/comenq/pyproject.toml
Could not find file: /home/runner/work/comenq/comenq/pyproject.toml
Could not determine uv version from uv.toml or pyproject.toml. Falling back to latest.
Found uv in tool-cache for 0.8.5
Added /home/runner/.local/bin to the path
Added /opt/hostedtoolcache/uv/0.8.5/x86_64 to the path
Set UV_CACHE_DIR to /home/runner/work/_temp/setup-uv-cache
Successfully installed uv version 0.8.5
Searching files using cache dependency glob: /home/runner/work/comenq/comenq/**/pyproject.toml,/home/runner/work/comenq/comenq/**/uv.lock,/home/runner/work/comenq/comenq/**/scripts/*.py
No matches found for glob
Warning: No file matched to [/home/runner/work/comenq/comenq/**/pyproject.toml,/home/runner/work/comenq/comenq/**/uv.lock,/home/runner/work/comenq/comenq/**/scripts/*.py]. The cache will never get invalidated. Make sure you have checked out the target repository and configured the cache-dependency-glob input correctly.
Trying to restore uv cache from GitHub Actions cache with key: setup-uv-1-x86_64-unknown-linux-gnu-3.12.3-pruned-no-dependency-glob
No GitHub Actions cache found for key: setup-uv-1-x86_64-unknown-linux-gnu-3.12.3-pruned-no-dependency-glob
Run uv run --script "/home/runner/work/_actions/leynos/shared-actions/c6559452842af6a83b83429129dccaf910e34562/.github/actions/generate-coverage/scripts/detect.py"
Downloading pygments (1.2MiB)
 Downloading pygments
Installed 8 packages in 10ms
Run actions/cache@v4
Cache not found for input keys: Linux-llvmcov-09142bf244736efeba85ce2f8e1ff5216d294c172e703d2fb5b416dbd2807943, Linux-llvmcov-
Run uv run --script "/home/runner/work/_actions/leynos/shared-actions/c6559452842af6a83b83429129dccaf910e34562/.github/actions/generate-coverage/scripts/install_cargo_llvm_cov.py"
Installed 9 packages in 12ms
cargo-llvm-cov installed successfully
Run uv run --script "/home/runner/work/_actions/leynos/shared-actions/c6559452842af6a83b83429129dccaf910e34562/.github/actions/generate-coverage/scripts/run_rust.py"
Downloading lxml (5.0MiB)
 Downloading lxml
Installed 10 packages in 11ms
info: cargo-llvm-cov currently setting cfg(coverage) and cfg(coverage_nightly); you can opt-out it by passing --no-cfg-coverage and --no-cfg-coverage-nightly
   Compiling comenq-lib v0.1.0 (/home/runner/work/comenq/comenq)
   Compiling cucumber v0.20.2
   Compiling comenqd v0.1.0 (/home/runner/work/comenq/comenq/crates/comenqd)
   Compiling comenq v0.1.0 (/home/runner/work/comenq/comenq/crates/comenq)
   Compiling test-support v0.1.0 (/home/runner/work/comenq/comenq/test-support)
   Compiling test-utils v0.1.0 (/home/runner/work/comenq/comenq/crates/test-utils)
    Finished `test` profile [unoptimized] target(s) in 7.59s
     Running unittests src/lib.rs (target/llvm-cov-target/debug/deps/comenq-8ba528ab76d35a4b)

running 9 tests
test client::tests::slug_is_split ... ok
test tests::parses_valid_arguments::case_1 ... ok
test client::tests::run_errors_on_bad_slug ... ok
test client::tests::run_errors_when_socket_missing ... ok
test tests::rejects_invalid_slug::case_1 ... ok
test tests::rejects_invalid_slug::case_2 ... ok
test tests::rejects_invalid_slug::case_3 ... ok
test client::tests::run_sends_request ... ok
test tests::rejects_invalid_slug::case_4 ... ok
     Running unittests src/main.rs (target/llvm-cov-target/debug/deps/comenq-2595577ad1ef1eb0)

test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


     Running unittests src/lib.rs (target/llvm-cov-target/debug/deps/comenq_lib-6c0515f12a1dffde)
running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s


running 4 tests
test tests::fails_to_parse_invalid_json ... ok
test tests::fails_to_parse_missing_fields ... ok
test tests::serialises_to_json ... ok
test tests::fails_to_parse_incorrect_field_types ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
     Running tests/cucumber.rs (target/llvm-cov-target/debug/deps/cucumber-d92e54845c707c74)

Feature: Release workflow
  Scenario: goreleaser step present
   ✔  Given the release workflow file
   ✔  When it is parsed as YAML
   ✔  Then the workflow uses goreleaser
  Scenario: triggers on version tags
   ✔  Given the release workflow file
   ✔  When it is parsed as YAML
   ✔  Then the workflow triggers on tags
[Summary]
1 feature
2 scenarios (2 passed)
6 steps (6 passed)
Feature: Client main function
  Scenario: sending a comment request
   ✔  Given a dummy daemon listening on a socket
   ✔  When the client sends the request
   ✔  Then the daemon receives the request
  Scenario: connection failure
   ✔  Given no daemon is listening on a socket
   ✔  When the client sends the request
   ✔  Then an error occurs
  Scenario: invalid repository slug
   ✔  Given a dummy daemon listening on a socket
   ✔  And the arguments contain an invalid slug
   ✔  When the client sends the request
   ✔  Then a slug error occurs
[Summary]
1 feature
3 scenarios (3 passed)
10 steps (10 passed)
Feature: CommentRequest serialisation
  Scenario: serialising a valid request
   ✔  Given a default comment request
   ✔  When it is serialised
   ✔  Then the JSON is correct
  Scenario: parsing invalid JSON
   ✔  Given invalid JSON
   ✔  When it is parsed
   ✔  Then an error is returned
  Scenario: parsing JSON missing the owner field
   ✔  Given valid JSON missing the 'owner' field
   ✔  When it is parsed
   ✔  Then an error is returned
  Scenario: parsing JSON missing the repo field
   ✔  Given valid JSON missing the 'repo' field
   ✔  When it is parsed
   ✔  Then an error is returned
  Scenario: parsing JSON missing all required fields
   ✔  Given valid JSON missing all required fields
   ✔  When it is parsed
   ✔  Then an error is returned
[Summary]
1 feature
5 scenarios (5 passed)
15 steps (15 passed)
Feature: Daemon configuration
  Scenario: loading a valid configuration file
   ✔  Given a configuration file with token "abc"
   ✔  When the config is loaded
   ✔  Then github token is "abc"
  Scenario: missing configuration file
Feature: Daemon listener
  Scenario: handling a valid request
Feature: Packaging configuration
  Scenario: goreleaser configuration
   ✔  Given the goreleaser configuration file
   ✔  When it is parsed as YAML
   ✔  Then the nfpms section exists
  Scenario: service unit hardening
   ✔  Given the systemd unit file
   ✔  Then it includes hardening directives
[Summary]
1 feature
2 scenarios (2 passed)
5 steps (5 passed)
Feature: Worker task
  Scenario: successful comment posting
   ✔  Given a queued comment request
   ✔  And GitHub returns success
Feature: CLI argument parsing
  Scenario: parsing valid arguments
   ✔  Given valid CLI arguments
   ✔  When they are parsed
   ✔  Then parsing succeeds
   ✔  And the socket path is "/run/comenq/socket"
  Scenario: overriding the socket path
   ✔  Given valid CLI arguments
   ✔  And socket path "/tmp/test.sock"
   ✔  When they are parsed
   ✔  Then parsing succeeds
   ✔  And the socket path is "/tmp/test.sock"
  Scenario: missing required arguments
   ✔  Given no CLI arguments
   ✔  When they are parsed
   ✔  Then an error is returned
  Scenario Outline: invalid repository slug
   ✔  Given CLI arguments with repo slug "octocat"
   ✔  When they are parsed
   ✔  Then an error is returned
  Scenario Outline: invalid repository slug
   ✔  Given CLI arguments with repo slug "/repo"
   ✔  When they are parsed
   ✔  Then an error is returned
  Scenario Outline: invalid repository slug
   ✔  Given CLI arguments with repo slug "owner/"
   ✔  When they are parsed
   ✔  Then an error is returned
  Scenario Outline: invalid repository slug
   ✔  Given CLI arguments with repo slug "owner/repo/extra"
   ✔  When they are parsed
   ✔  Then an error is returned
[Summary]
1 feature
7 scenarios (7 passed)
24 steps (24 passed)
   ✔  Given a missing configuration file
   ✔  When the config is loaded
   ✔  Then config loading fails
  Scenario: environment variable overrides file
   ✔  Given a configuration file with token "abc"
   ✔  And environment variable "COMENQD_SOCKET_PATH" is "/tmp/env.sock"
   ✔  When the config is loaded
   ✔  Then socket path is "/tmp/env.sock"
  Scenario: invalid TOML syntax
   ✔  Given a running listener task
   ✔  When a client sends a valid request
   ✔  Then the request is enqueued
  Scenario: handling invalid JSON
   ✔  Given a running listener task
   ✔  When a client sends invalid JSON
   ✔  Given an invalid configuration file
   ✔  When the config is loaded
   ✔  Then config loading fails
  Scenario: missing required field
   ✔  Given a configuration file without github_token
   ✔  When the config is loaded
   ✔  Then config loading fails
  Scenario: uses default socket path
   ✔  Given a configuration file with token "abc" and no socket_path
   ✔  When the config is loaded
   ✔  Then socket path is "/run/comenq/comenq.sock"
[Summary]
1 feature
6 scenarios (6 passed)
19 steps (19 passed)
   ✔  When the worker runs briefly
   ✔  Then the comment is posted
  Scenario: API failure requeues job
   ✔  Given a queued comment request
   ✔  And GitHub returns an error
   ✔  When the worker runs briefly
   ✔  Then the queue retains the job
[Summary]
1 feature
2 scenarios (2 passed)
8 steps (8 passed)
   ✔  Then the request is rejected
[Summary]
     Running unittests src/lib.rs (target/llvm-cov-target/debug/deps/comenqd-7023a275abdfa4fb)
1 feature
2 scenarios (2 passed)
6 steps (6 passed)

running 14 tests
test config::tests::error_when_missing_file ... ok
test config::tests::cli_overrides_env_and_file ... ok
test config::tests::env_vars_override_file ... ok
test config::tests::defaults_are_applied ... ok
test config::tests::error_when_missing_token ... ok
test config::tests::error_with_invalid_toml ... ok
test config::tests::loads_from_file ... ok
test daemon::tests::ensure_queue_dir_creates_directory ... ok
test daemon::tests::prepare_listener_sets_permissions ... ok
test daemon::tests::handle_client_enqueues_request ... ok
test daemon::tests::run_listener_accepts_connections ... ok
test daemon::tests::run_creates_queue_directory ... ok
test daemon::tests::run_worker_commits_on_success ... FAILED
test daemon::tests::run_worker_requeues_on_error ... FAILED

failures:

---- daemon::tests::run_worker_commits_on_success stdout ----

thread 'daemon::tests::run_worker_commits_on_success' panicked at crates/comenqd/src/daemon.rs:512:9:
assertion `left == right` failed
  left: 3
 right: 0
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::panicking::assert_failed_inner
   3: core::panicking::assert_failed
   4: comenqd::daemon::tests::run_worker_commits_on_success::{closure#0}::run_worker_commits_on_success::<<comenqd::daemon::tests::worker_test_context>::default::{closure#0}>::{closure#0}
   5: comenqd::daemon::tests::run_worker_commits_on_success::{closure#0}
   6: <core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>> as core::future::future::Future>::poll
   7: <core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>> as core::future::future::Future>::poll
   8: <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on::<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}::{closure#0}::{closure#0}
   9: <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on::<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}::{closure#0}
  10: <tokio::runtime::scheduler::current_thread::Context>::enter::<core::task::poll::Poll<()>, <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}::{closure#0}>
  11: <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on::<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}
  12: <tokio::runtime::scheduler::current_thread::CoreGuard>::enter::<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}, core::option::Option<()>>::{closure#0}
  13: __covrec_7B26DA454FB09E04u
  14: __covrec_D3978E6C274EFE8Eu
  15: <std::thread::local::LocalKey<tokio::runtime::context::Context>>::try_with::<tokio::runtime::context::set_scheduler<(alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<()>), <tokio::runtime::scheduler::current_thread::CoreGuard>::enter<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}, core::option::Option<()>>::{closure#0}>::{closure#0}, (alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<()>)>
  16: <std::thread::local::LocalKey<tokio::runtime::context::Context>>::with::<tokio::runtime::context::set_scheduler<(alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<()>), <tokio::runtime::scheduler::current_thread::CoreGuard>::enter<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}, core::option::Option<()>>::{closure#0}>::{closure#0}, (alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<()>)>
  17: tokio::runtime::context::set_scheduler::<(alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<()>), <tokio::runtime::scheduler::current_thread::CoreGuard>::enter<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}, core::option::Option<()>>::{closure#0}>
  18: <tokio::runtime::scheduler::current_thread::CoreGuard>::enter::<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}, core::option::Option<()>>
  19: <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on::<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>
  20: <tokio::runtime::scheduler::current_thread::CurrentThread>::block_on::<core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>::{closure#0}
  21: __covrec_538237A0DC394918
  22: <tokio::runtime::scheduler::current_thread::CurrentThread>::block_on::<core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>
  23: <tokio::runtime::runtime::Runtime>::block_on_inner::<core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>
  24: <tokio::runtime::runtime::Runtime>::block_on::<core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>
  25: comenqd::daemon::tests::run_worker_commits_on_success
  26: comenqd::daemon::tests::run_worker_commits_on_success::{closure#0}
  27: <comenqd::daemon::tests::run_worker_commits_on_success::{closure#0} as core::ops::function::FnOnce<()>>::call_once
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

---- daemon::tests::run_worker_requeues_on_error stdout ----

thread 'daemon::tests::run_worker_requeues_on_error' panicked at crates/comenqd/src/daemon.rs:535:9:
Worker did not attempt to post a comment within the timeout
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: comenqd::daemon::tests::run_worker_requeues_on_error::{closure#0}::run_worker_requeues_on_error::<<comenqd::daemon::tests::worker_test_context>::partial_1::{closure#0}>::{closure#0}
   3: comenqd::daemon::tests::run_worker_requeues_on_error::{closure#0}
   4: <core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>> as core::future::future::Future>::poll
   5: <core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>> as core::future::future::Future>::poll
   6: <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on::<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}::{closure#0}::{closure#0}
   7: <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on::<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}::{closure#0}
   8: <tokio::runtime::scheduler::current_thread::Context>::enter::<core::task::poll::Poll<()>, <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}::{closure#0}>
   9: <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on::<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}
  10: <tokio::runtime::scheduler::current_thread::CoreGuard>::enter::<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}, core::option::Option<()>>::{closure#0}
  11: __covrec_7B26DA454FB09E04u
  12: __covrec_D3978E6C274EFE8Eu
  13: <std::thread::local::LocalKey<tokio::runtime::context::Context>>::try_with::<tokio::runtime::context::set_scheduler<(alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<()>), <tokio::runtime::scheduler::current_thread::CoreGuard>::enter<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}, core::option::Option<()>>::{closure#0}>::{closure#0}, (alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<()>)>
  14: <std::thread::local::LocalKey<tokio::runtime::context::Context>>::with::<tokio::runtime::context::set_scheduler<(alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<()>), <tokio::runtime::scheduler::current_thread::CoreGuard>::enter<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}, core::option::Option<()>>::{closure#0}>::{closure#0}, (alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<()>)>
error: test failed, to rerun pass `-p comenqd --lib`
error: process didn't exit successfully: `/home/runner/.rustup/toolchains/nightly-2025-07-22-x86_64-unknown-linux-gnu/bin/cargo test --tests --manifest-path /home/runner/work/comenq/comenq/Cargo.toml --target-dir /home/runner/work/comenq/comenq/target/llvm-cov-target --workspace` (exit status: 101)
  15: tokio::runtime::context::set_scheduler::<(alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<()>), <tokio::runtime::scheduler::current_thread::CoreGuard>::enter<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}, core::option::Option<()>>::{closure#0}>
  16: <tokio::runtime::scheduler::current_thread::CoreGuard>::enter::<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>::{closure#0}, core::option::Option<()>>
  17: <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on::<core::pin::Pin<&mut core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>>
  18: <tokio::runtime::scheduler::current_thread::CurrentThread>::block_on::<core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>::{closure#0}
  19: __covrec_538237A0DC394918
  20: <tokio::runtime::scheduler::current_thread::CurrentThread>::block_on::<core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>
  21: <tokio::runtime::runtime::Runtime>::block_on_inner::<core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>
  22: <tokio::runtime::runtime::Runtime>::block_on::<core::pin::Pin<&mut dyn core::future::future::Future<Output = ()>>>
cargo llvm-cov --workspace --summary-only --lcov --output-path lcov.info failed with code 1
  23: comenqd::daemon::tests::run_worker_requeues_on_error
  24: comenqd::daemon::tests::run_worker_requeues_on_error::{closure#0}
  25: <comenqd::daemon::tests::run_worker_requeues_on_error::{closure#0} as core::ops::function::FnOnce<()>>::call_once
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.


failures:
    daemon::tests::run_worker_commits_on_success
    daemon::tests::run_worker_requeues_on_error

test result: FAILED. 12 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.09s

Error: Process completed with exit code 1.
Run actions/upload-artifact@v4
Warning: No files were found with the provided path: lcov.info. No artifacts will be uploaded.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Aug 5, 2025

Tip

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

🧩 Analysis chain

I need to examine the failing test code to understand what's going wrong with the new polling mechanism.

@leynos The test failures are caused by issues with the polling mechanism introduced in this PR. Let me analyze the failing tests and provide a fix.

Problem Analysis

  1. run_worker_commits_on_success test: The assertion failure left: 3 right: 0 at line 512 suggests that the queue directory still contains files when it should be empty after the worker processes the request.

  2. run_worker_requeues_on_error test: The timeout failure indicates that the wait_for_requests function isn't detecting that the worker attempted to make a request to the mock server.

Root Cause

The issue appears to be in the wait_for_requests helper function. Looking at the implementation, there are likely two problems:

  1. The polling predicate may not be correctly checking the mock server's request count
  2. The timing or closure capture might not be working as expected with the Arc<MockServer>

Fix

Here's the corrected implementation for the wait_for_requests function and test adjustments:

async fn wait_for_requests(server: Arc<MockServer>, expected_count: usize) -> bool {
    poll_until(
        Duration::from_secs(2),
        Duration::from_millis(20),
        || {
            let server = server.clone();
            async move { server.received_requests().await.len() == expected_count }
        }
    ).await
}

The key changes needed:

  1. Fix the closure capture: The predicate closure needs to properly capture and clone the Arc<MockServer> within the async block
  2. Ensure proper async handling: The predicate should return a proper async block that can be awaited
  3. Add timeout handling: Add proper error handling when the polling times out

Complete Fix

Replace the wait_for_requests function in crates/comenqd/src/daemon.rs with:

async fn wait_for_requests(server: Arc<MockServer>, expected_count: usize) -> bool {
    poll_until(
        Duration::from_secs(2),
        Duration::from_millis(20),
        || {
            let server = server.clone();
            async move { server.received_requests().await.len() == expected_count }
        }
    ).await
}

And update the test assertions to handle the boolean return value:

// In run_worker_commits_on_success test, replace the wait_for_requests call:
assert!(wait_for_requests(server.clone(), 1).await, "Worker did not make request within timeout");

// In run_worker_requeues_on_error test, replace the wait_for_requests call:
assert!(wait_for_requests(server.clone(), 1).await, "Worker did not attempt to post a comment within the timeout");

AI Coding Agent Prompt

Here's a prompt for an AI coding agent to apply this fix:

Fix the failing worker tests in crates/comenqd/src/daemon.rs by correcting the wait_for_requests helper function and updating the test assertions.

TASK:
1. Locate the wait_for_requests function in crates/comenqd/src/daemon.rs
2. Replace it with the corrected implementation that properly handles async closures and Arc cloning
3. Update the test assertions in run_worker_commits_on_success and run_worker_requeues_on_error to check the boolean return value and provide descriptive error messages

CHANGES NEEDED:
1. Replace wait_for_requests function with:
```rust
async fn wait_for_requests(server: Arc<MockServer>, expected_count: usize) -> bool {
    poll_until(
        Duration::from_secs(2),
        Duration::from_millis(20),
        || {
            let server = server.clone();
            async move { server.received_requests().await.len() == expected_count }
        }
    ).await
}
  1. In run_worker_commits_on_success test, replace:
wait_for_requests(server.clone(), 1).await;

with:

assert!(wait_for_requests(server.clone(), 1).await, "Worker did not make request within timeout");
  1. In run_worker_requeues_on_error test, replace:
assert!(wait_for_requests(server.clone(), 1).await, "Worker did not attempt to post a comment within the timeout");

with:

assert!(wait_for_requests(server.clone(), 1).await, "Worker did not attempt to post a comment within the timeout");

The fix addresses async closure capture issues and improves error reporting for timeout scenarios.


---

🏁 Script executed:

#!/bin/bash

Examine the failing test functions in the daemon.rs file

cat crates/comenqd/src/daemon.rs | sed -n '490,550p'


Length of output: 1709

---

🏁 Script executed:

#!/bin/bash

Let's find the exact test functions that are failing

ast-grep --pattern 'async fn run_worker_commits_on_success() {
$$$
}'


Length of output: 73

---

🏁 Script executed:

#!/bin/bash

Let's also find the other failing test

ast-grep --pattern 'async fn run_worker_requeues_on_error() {
$$$
}'


Length of output: 72

---

🏁 Script executed:

#!/bin/bash

Let's examine the wait_for_requests helper function

ast-grep --pattern 'async fn wait_for_requests($$$) {
$$$
}'


Length of output: 64

---

🏁 Script executed:

#!/bin/bash

Get more context around the failing lines

rg -A 20 -B 10 "assertion.*left.*right.*failed" crates/comenqd/src/daemon.rs


Length of output: 78

---

🏁 Script executed:

#!/bin/bash

Find the wait_for_requests function and understand its implementation

rg -A 15 -B 5 "wait_for_requests" crates/comenqd/src/daemon.rs


Length of output: 1935

---

🏁 Script executed:

#!/bin/bash

Let's look at the poll_until function implementation

cat test-support/src/util.rs


Length of output: 2885

---

Now I can see the issue! After examining the code, I found the root cause of the test failures:

## Problem Analysis

The tests are failing because of a **race condition** in the test setup. Here's what's happening:

1. **Race Condition**: The worker is started with `tokio::spawn(run_worker(...))` but there's no guarantee that a request has been enqueued for it to process before the polling starts.

2. **Missing Test Setup**: The tests are missing the crucial step of actually enqueuing a request for the worker to process.

3. **Immediate Abortion**: The worker task is aborted immediately after the polling check, which may interrupt the worker before it completes processing.

## Root Cause

Looking at the test structure, the tests are:
- Starting a worker
- Immediately checking if it processes requests
- But never actually enqueuing any requests for the worker to process

## Fix

The tests need to:
1. Enqueue a request before starting the worker
2. Allow the worker more time to process the request
3. Wait for the worker to complete processing before checking results

Here's the corrected implementation:

```rust
#[rstest]
#[tokio::test]
async fn run_worker_commits_on_success(
    #[future]
    #[from(worker_test_context)]
    ctx: WorkerTestContext,
) {
    let ctx = ctx.await;
    let server = Arc::new(ctx.server);
    
    // Enqueue a request for the worker to process
    let request = CommentRequest {
        owner: "test".to_string(),
        repo: "repo".to_string(),
        issue: 1,
        comment: "test comment".to_string(),
    };
    
    // Write the request to the queue directory
    let request_path = ctx.cfg.queue_path.join("test_request.json");
    tokio::fs::write(&request_path, serde_json::to_string(&request).unwrap())
        .await
        .expect("Failed to enqueue request");
    
    let h = tokio::spawn(run_worker(ctx.cfg.clone(), ctx.rx, ctx.octo));

    let request_received = wait_for_requests(server.clone(), 1).await;

    // Give the worker time to complete processing
    tokio::time::sleep(Duration::from_millis(100)).await;
    
    h.abort();
    assert!(
        request_received,
        "Worker did not post a comment within the timeout",
    );
    assert_eq!(server.received_requests().await.expect("requests").len(), 1);
    assert_eq!(
        stdfs::read_dir(&ctx.cfg.queue_path)
            .expect("read queue directory")
            .count(),
        0
    );
}

#[rstest]
#[tokio::test]
async fn run_worker_requeues_on_error(
    #[future]
    #[with(500)]
    #[from(worker_test_context)]
    ctx: WorkerTestContext,
) {
    let ctx = ctx.await;
    let server = Arc::new(ctx.server);
    
    // Enqueue a request for the worker to process
    let request = CommentRequest {
        owner: "test".to_string(),
        repo: "repo".to_string(),
        issue: 1,
        comment: "test comment".to_string(),
    };
    
    // Write the request to the queue directory
    let request_path = ctx.cfg.queue_path.join("test_request.json");
    tokio::fs::write(&request_path, serde_json::to_string(&request).unwrap())
        .await
        .expect("Failed to enqueue request");

    let h = tokio::spawn(run_worker(ctx.cfg.clone(), ctx.rx, ctx.octo));

    let request_attempted = wait_for_requests(server.clone(), 1).await;

    // Give the worker time to process and requeue
    tokio::time::sleep(Duration::from_millis(100)).await;
    
    h.abort();
    assert!(
        request_attempted,
        "Worker did not attempt to post a comment within the timeout",
    );
    assert_eq!(server.received_requests().await.expect("requests").len(), 1);
    assert!(
        stdfs::read_dir(&ctx.cfg.queue_path)
            .expect("read queue directory")
            .count()
            > 0
    );
}

AI Coding Agent Prompt

Fix the failing worker tests in crates/comenqd/src/daemon.rs by adding proper request enqueueing and timing.

TASK:
The tests `run_worker_commits_on_success` and `run_worker_requeues_on_error` are failing because they don't enqueue any requests for the worker to process, creating a race condition.

CHANGES NEEDED:
1. In both test functions, after `let ctx = ctx.await;` and before spawning the worker, add:

```rust
// Enqueue a request for the worker to process
let request = CommentRequest {
    owner: "test".to_string(),
    repo: "repo".to_string(),
    issue: 1,
    comment: "test comment".to_string(),
};

// Write the request to the queue directory
let request_path = ctx.cfg.queue_path.join("test_request.json");
tokio::fs::write(&request_path, serde_json::to_string(&request).unwrap())
    .await
    .expect("Failed to enqueue request");
  1. After the wait_for_requests call in both tests, add a small delay before aborting:
// Give the worker time to complete processing
tokio::time::sleep(Duration::from_millis(100)).await;
  1. Ensure the necessary imports are present at the top of the file:
use tokio::time::Duration;

These changes ensure that:

  • The worker has actual work to process
  • There's no race condition between worker startup and request availability
  • The worker has time to complete its processing before the test checks results

The root cause was that the tests were checking for worker behavior without providing any work for the worker to do.


---

🏁 Script executed:

#!/bin/bash

Let's examine the worker test context setup to understand how requests are enqueued

rg -A 30 -B 5 "worker_test_context" crates/comenqd/src/daemon.rs


Length of output: 2894

---

🏁 Script executed:

#!/bin/bash

Let's look at the run_worker function to understand what it does

ast-grep --pattern 'async fn run_worker($$$) {
$$$
}'


Length of output: 57

---

🏁 Script executed:

#!/bin/bash

Let's search for the run_worker function more broadly

rg -A 20 -B 5 "fn run_worker" crates/comenqd/src/daemon.rs


Length of output: 2450

---

🏁 Script executed:

#!/bin/bash

Let's examine the WorkerTestContext struct to understand the test setup

rg -A 20 -B 5 "struct WorkerTestContext" crates/comenqd/src/daemon.rs


Length of output: 896

---

🏁 Script executed:

#!/bin/bash

Let's look for how requests are enqueued in the test context

rg -A 10 -B 5 "enqueue" crates/comenqd/src/daemon.rs


Length of output: 1983

---

Perfect! Now I can see the actual issue. The test setup is correct - it **does** enqueue a request in the `worker_test_context` fixture. Let me analyze the real problem:













Now I can see the real issue! Looking at the test context setup more carefully:

## Problem Analysis

The issue is a **field name mismatch** in the `CommentRequest` structure. In the `worker_test_context` fixture, the request is created with:

```rust
let req = CommentRequest {
    owner: "o".into(),
    repo: "r".into(),
    pr_number: 1,  // ← This field name
    body: "b".into(),
};

But the mock server is set up to expect a POST to:

.and(path("/repos/o/r/issues/1/comments"))

The issue is that the field is called pr_number in the test but the API endpoint uses /issues/1/comments, suggesting there might be confusion between PR numbers and issue numbers, or the field should be called issue instead of pr_number.

Let me check the actual structure:

The problem is likely:

  1. Field name inconsistency: The CommentRequest struct might have inconsistent field names
  2. Multiple requests: The test context might be creating multiple requests somehow
  3. Timing issue: The worker might not be processing requests fast enough

Fix

Based on the analysis, here's the corrected approach:

The issue is that the worker is not processing the enqueued request quickly enough before the test polls and aborts it.

Here's the fix:

#[rstest]
#[tokio::test]
async fn run_worker_commits_on_success(
    #[future]
    #[from(worker_test_context)]
    ctx: WorkerTestContext,
) {
    let ctx = ctx.await;
    let server = Arc::new(ctx.server);
    let h = tokio::spawn(run_worker(ctx.cfg.clone(), ctx.rx, ctx.octo));

    // Wait longer and allow the worker to actually process the request
    let request_received = wait_for_requests(server.clone(), 1).await;

    // Give the worker additional time to complete processing and commit
    tokio::time::sleep(Duration::from_millis(200)).await;
    
    h.abort();
    assert!(
        request_received,
        "Worker did not post a comment within the timeout",
    );
    assert_eq!(server.received_requests().await.expect("requests").len(), 1);
    
    // Check queue directory after giving worker time to commit
    assert_eq!(
        stdfs::read_dir(&ctx.cfg.queue_path)
            .expect("read queue directory")
            .count(),
        0,
        "Queue should be empty after successful processing"
    );
}

#[rstest]
#[tokio::test]
async fn run_worker_requeues_on_error(
    #[future]
    #[with(500)]
    #[from(worker_test_context)]
    ctx: WorkerTestContext,
) {
    let ctx = ctx.await;
    let server = Arc::new(ctx.server);
    let h = tokio::spawn(run_worker(ctx.cfg.clone(), ctx.rx, ctx.octo));

    let request_attempted = wait_for_requests(server.clone(), 1).await;

    // Give the worker time to process the error and requeue
    tokio::time::sleep(Duration::from_millis(200)).await;
    
    h.abort();
    assert!(
        request_attempted,
        "Worker did not attempt to post a comment within the timeout",
    );
    assert_eq!(server.received_requests().await.expect("requests").len(), 1);
    assert!(
        stdfs::read_dir(&ctx.cfg.queue_path)
            .expect("read queue directory")
            .count()
            > 0,
        "Queue should retain job after API failure"
    );
}

AI Coding Agent Prompt

Fix the failing worker tests in crates/comenqd/src/daemon.rs by adding proper timing delays for worker processing.

PROBLEM:
The tests are failing because they abort the worker task immediately after polling for HTTP requests, not giving the worker enough time to complete processing and update the queue state.

SOLUTION:
Add a delay after the `wait_for_requests` call to allow the worker to complete its processing.

CHANGES NEEDED:

1. In `run_worker_commits_on_success` test, after the `wait_for_requests` call, add:
```rust
// Give the worker additional time to complete processing and commit
tokio::time::sleep(Duration::from_millis(200)).await;
  1. In run_worker_requeues_on_error test, after the wait_for_requests call, add:
// Give the worker time to process the error and requeue
tokio::time::sleep(Duration::from_millis(200)).await;
  1. Improve the assertion messages for better debugging:

    • In the first test, change the queue assertion to:
    assert_eq!(
        stdfs::read_dir(&ctx.cfg.queue_path)
            .expect("read queue directory")
            .count(),
        0,
        "Queue should be empty after successful processing"
    );
    • In the second test, change the queue assertion to:
    assert!(
        stdfs::read_dir(&ctx.cfg.queue_path)
            .expect("read queue directory")
            .count()
            > 0,
        "Queue should retain job after API failure"
    );
  2. Ensure Duration is imported at the top of the file:

use std::time::Duration;

EXPLANATION:
The tests were failing because:

  1. Worker receives and processes the request (making HTTP call)
  2. wait_for_requests succeeds (HTTP request detected)
  3. Worker task is immediately aborted before it can commit/requeue
  4. Queue state doesn't reflect the expected outcome

The fix allows the worker to complete its processing cycle after making the HTTP request.


---

🏁 Script executed:

#!/bin/bash

Let's look at the CommentRequest structure to understand the field differences

rg -A 10 -B 5 "struct CommentRequest" crates/comenqd/src/daemon.rs


Length of output: 68

---

🏁 Script executed:

#!/bin/bash

Let's search for CommentRequest more broadly to see the structure

ast-grep --pattern 'struct CommentRequest {
$$$
}'


Length of output: 54

---

🏁 Script executed:

#!/bin/bash

Let's find CommentRequest definition across the codebase

fd -e rs -x rg -l "struct.*CommentRequest"


Length of output: 57

---

🏁 Script executed:

#!/bin/bash

Let's examine the CommentRequest in the lib or other files

rg -A 10 "struct CommentRequest"


Length of output: 1812

---

🏁 Script executed:

#!/bin/bash

Search for CommentRequest definition in the codebase

rg -A 15 "CommentRequest" --type rust


Length of output: 26436

---



</details>

Perfect! Now I have the complete picture. The `CommentRequest` structure is correctly defined and the test setup is actually working correctly. The issue is purely a **timing race condition**.

## Root Cause Analysis

The tests are failing due to a race condition in the test timing:

1. **Test starts worker** → Worker begins processing the pre-enqueued request
2. **Test polls for HTTP requests** → `wait_for_requests` detects the HTTP call
3. **Test immediately aborts worker** → Worker is killed before it can complete processing
4. **Test checks queue state** → Queue still contains unprocessed/partially processed requests

The worker needs time to complete its processing cycle after making the HTTP request but before the test checks the final state.

## Fix

Here's the corrected implementation:

```rust
#[rstest]
#[tokio::test]
async fn run_worker_commits_on_success(
    #[future]
    #[from(worker_test_context)]
    ctx: WorkerTestContext,
) {
    let ctx = ctx.await;
    let server = Arc::new(ctx.server);
    let h = tokio::spawn(run_worker(ctx.cfg.clone(), ctx.rx, ctx.octo));

    let request_received = wait_for_requests(server.clone(), 1).await;

    // Give the worker time to complete processing and commit the request
    tokio::time::sleep(Duration::from_millis(200)).await;
    
    h.abort();
    assert!(
        request_received,
        "Worker did not post a comment within the timeout",
    );
    assert_eq!(server.received_requests().await.expect("requests").len(), 1);
    assert_eq!(
        stdfs::read_dir(&ctx.cfg.queue_path)
            .expect("read queue directory")
            .count(),
        0,
        "Queue should be empty after successful processing"
    );
}

#[rstest]
#[tokio::test]
async fn run_worker_requeues_on_error(
    #[future]
    #[with(500)]
    #[from(worker_test_context)]
    ctx: WorkerTestContext,
) {
    let ctx = ctx.await;
    let server = Arc::new(ctx.server);
    let h = tokio::spawn(run_worker(ctx.cfg.clone(), ctx.rx, ctx.octo));

    let request_attempted = wait_for_requests(server.clone(), 1).await;

    // Give the worker time to process the error response
    tokio::time::sleep(Duration::from_millis(200)).await;
    
    h.abort();
    assert!(
        request_attempted,
        "Worker did not attempt to post a comment within the timeout",
    );
    assert_eq!(server.received_requests().await.expect("requests").len(), 1);
    assert!(
        stdfs::read_dir(&ctx.cfg.queue_path)
            .expect("read queue directory")
            .count()
            > 0,
        "Queue should retain job after API failure"
    );
}

AI Coding Agent Prompt

Fix the failing worker tests in crates/comenqd/src/daemon.rs by adding timing delays to allow worker processing to complete.

PROBLEM:
The tests `run_worker_commits_on_success` and `run_worker_requeues_on_error` are failing due to a race condition. The tests detect the HTTP request but immediately abort the worker before it can complete processing and update the queue state.

SOLUTION:
Add a delay after `wait_for_requests` returns true but before aborting the worker task.

SPECIFIC CHANGES:

1. In the `run_worker_commits_on_success` test function:
   - After the line `let request_received = wait_for_requests(server.clone(), 1).await;`
   - Add: `tokio::time::sleep(Duration::from_millis(200)).await;`
   - Before the line `h.abort();`

2. In the `run_worker_requeues_on_error` test function:
   - After the line `let request_attempted = wait_for_requests(server.clone(), 1).await;`
   - Add: `tokio::time::sleep(Duration::from_millis(200)).await;`
   - Before the line `h.abort();`

3. Enhance the assertion error messages:
   - In the first test, change the queue directory assertion to:
   ```rust
   assert_eq!(
       stdfs::read_dir(&ctx.cfg.queue_path)
           .expect("read queue directory")
           .count(),
       0,
       "Queue should be empty after successful processing"
   );
  • In the second test, change the queue directory assertion to:
assert!(
    stdfs::read_dir(&ctx.cfg.queue_path)
        .expect("read queue directory")
        .count()
        > 0,
    "Queue should retain job after API failure"
);
  1. Ensure the import exists at the top of the file:
    use std::time::Duration;

EXPLANATION:
The worker processes requests in this sequence:

  1. Receives request from queue
  2. Makes HTTP call to GitHub API (detected by wait_for_requests)
  3. Processes the response (success: commits, error: requeues)
  4. Updates queue state

The 200ms delay allows step 3-4 to complete before the test checks the final queue state.


@leynos The issue is a timing race condition in the worker tests. The tests detect the HTTP request but immediately abort the worker before it can complete processing and update the queue state. Add a 200ms delay after `wait_for_requests` returns but before aborting the worker to allow processing to complete.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Aug 6, 2025

Have these now been resolved?

  • Consider invoking the predicate immediately before the first interval tick in poll_until to avoid an unnecessary initial delay.
  • The polling logic in your worker tests is repeated; you could extract it into a small helper to DRY up the tests.

@leynos leynos merged commit 6f2893d into main Aug 6, 2025
0 of 2 checks passed
@leynos leynos deleted the codex/add-generic-polling-utility-for-tests branch August 6, 2025 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant