---
phase: 12-redis-backend
plan: 02
type: execute
wave: 2
depends_on: ["12-01"]
files_modified:
  - crates/pmcp-tasks/src/store/redis.rs
autonomous: true
requirements: [TEST-03]

must_haves:
  truths:
    - "All 6 StorageBackend methods are tested against a real Redis instance"
    - "Tests are gated behind the redis-tests feature flag and do not run by default"
    - "Each test run is isolated via unique key prefix (no interference between test runs)"
    - "TTL tests verify both EXPIREAT-based Redis auto-deletion setup and application-level filtering"
    - "CAS tests verify atomic version checking and VersionConflict error on mismatch"
    - "Sorted set index tests verify owner-scoped listing returns correct tasks"
  artifacts:
    - path: "crates/pmcp-tasks/src/store/redis.rs"
      provides: "Integration test module with 18+ tests behind redis-tests feature flag"
      contains: "mod integration_tests"
  key_links:
    - from: "crates/pmcp-tasks/src/store/redis.rs (integration_tests)"
      to: "RedisBackend"
      via: "test_backend() helper creating isolated backend instance"
      pattern: "async fn test_backend"
    - from: "crates/pmcp-tasks/src/store/redis.rs (integration_tests)"
      to: "StorageBackend trait"
      via: "direct method calls on RedisBackend"
      pattern: "backend\\.(get|put|put_if_version|delete|list_by_prefix|cleanup_expired)"
---

<objective>
Add integration tests for RedisBackend against a real Redis instance, gated behind the `redis-tests` feature flag, with UUID-based test isolation per test run.

Purpose: Verify that all 6 StorageBackend methods work correctly against real Redis, including Lua script execution, hash storage, sorted set indexing, TTL behavior, and CAS atomicity. This mirrors Phase 11's DynamoDB integration test approach.

Output: An `integration_tests` module inside `redis.rs` with 18+ tests covering all StorageBackend methods, gated behind `redis-tests` feature flag.
</objective>

<execution_context>
@./.claude/get-shit-done/workflows/execute-plan.md
@./.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/12-redis-backend/12-CONTEXT.md
@.planning/phases/12-redis-backend/12-RESEARCH.md
@.planning/phases/12-redis-backend/12-01-SUMMARY.md

# Reference: DynamoDB integration tests (follow same patterns)
@crates/pmcp-tasks/src/store/dynamodb.rs
@crates/pmcp-tasks/src/store/redis.rs
</context>

<tasks>

<task type="auto">
  <name>Task 1: Redis integration test module with all StorageBackend contract tests</name>
  <files>crates/pmcp-tasks/src/store/redis.rs</files>
  <action>
Add an `integration_tests` module at the bottom of `redis.rs`, gated behind `redis-tests` feature flag. Follow the DynamoDB integration test patterns from `dynamodb.rs` exactly.

**Test helper:**
```rust
#[cfg(all(test, feature = "redis-tests"))]
mod integration_tests {
    use super::*;

    /// Creates a test backend with a unique key prefix for isolation.
    async fn test_backend() -> RedisBackend {
        let url = std::env::var("REDIS_URL")
            .unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
        let backend = RedisBackend::new(&url)
            .await
            .expect("Redis connection failed -- is Redis running on localhost:6379?");
        let test_prefix = format!("test-{}", uuid::Uuid::new_v4());
        backend.with_prefix(test_prefix)
    }
    // ... tests
}
```

Note: Unlike the DynamoDB test helper which returned `(DynamoDbBackend, String)`, the Redis test helper returns just `RedisBackend` since the key prefix is embedded in the struct. Tests that need a unique owner can use format!("{}-owner", uuid::Uuid::new_v4()) or similar.

**Test naming convention:** Use `redis_` prefix for all test names (consistent with `ddb_` prefix in DynamoDB tests).

**Tests to implement (mirror DynamoDB test suite):**

**get tests (2):**
- `redis_get_missing_key_returns_not_found` -- get on nonexistent key returns StorageError::NotFound
- `redis_get_returns_stored_data` -- put then get returns correct data and version

**put tests (3):**
- `redis_put_new_key_returns_version_1` -- first put returns version 1
- `redis_put_existing_key_increments_version` -- second put returns version 2, data updated
- `redis_put_overwrites_data` -- put twice, get returns latest data

**put_if_version tests (4):**
- `redis_put_if_version_succeeds_on_match` -- CAS with correct version succeeds
- `redis_put_if_version_fails_on_mismatch` -- CAS with wrong version returns VersionConflict
- `redis_put_if_version_fails_on_missing_key` -- CAS on nonexistent key returns VersionConflict (consistent with DynamoDB behavior)
- `redis_put_if_version_updates_data` -- CAS success updates stored data

**delete tests (3):**
- `redis_delete_existing_returns_true` -- delete existing key returns true
- `redis_delete_missing_returns_false` -- delete nonexistent key returns false
- `redis_delete_then_get_returns_not_found` -- delete then get returns NotFound

**list_by_prefix tests (3):**
- `redis_list_by_prefix_returns_matching` -- two owners, listing one returns only their tasks
- `redis_list_by_prefix_empty_on_no_match` -- listing nonexistent owner returns empty
- `redis_list_by_prefix_returns_correct_data_and_versions` -- multiple puts, list returns correct data and versions

**cleanup_expired tests (1):**
- `redis_cleanup_expired_returns_zero` -- cleanup_expired returns 0 (no-op)

**TTL tests (2):**
- `redis_get_filters_expired_task` -- Create a TaskRecord with a very short TTL (e.g., 1ms TTL in the past by manually setting expiresAt to a past timestamp in the JSON), put it, then get should return NotFound due to application-level expiry filtering. To test this properly: serialize a TaskRecord, modify the expiresAt to a past epoch, put the raw data, then get should return NotFound.
- `redis_put_sets_expires_at_hash_field` -- Create a TaskRecord with 1-hour TTL, put it, then use raw HGETALL to verify the expires_at hash field is present and has a reasonable epoch value.

For the TTL tests, you will need access to the raw redis connection. Add a test-only accessor on RedisBackend:
```rust
#[cfg(test)]
impl RedisBackend {
    pub(crate) fn conn(&self) -> MultiplexedConnection {
        self.conn.clone()
    }
    pub(crate) fn prefix(&self) -> &str {
        &self.key_prefix
    }
}
```

For `redis_put_sets_expires_at_hash_field`: use `::redis::AsyncCommands::hgetall` on the raw task key to inspect the hash field directly.

**Test isolation approach:** Each test creates a new `test_backend()` with a unique UUID prefix, ensuring zero collision between concurrent test runs. No FLUSHDB needed.

**Key format in tests:** Tests create composite keys like `"owner-a:task-1"`. The RedisBackend internally prepends the key_prefix to create the actual Redis key.

**Import for TaskRecord in TTL tests:**
```rust
use crate::domain::TaskRecord;
```
  </action>
  <verify>
1. `cargo test -p pmcp-tasks --features redis-tests -- redis_ --test-threads=1` -- all Redis integration tests pass (requires local Redis running)
2. `cargo test -p pmcp-tasks` -- all existing tests still pass (redis-tests not activated)
3. `cargo clippy -p pmcp-tasks --features redis -- -D warnings` -- zero clippy warnings
4. `cargo fmt -p pmcp-tasks --check` -- clean formatting

If Redis is not running locally, the tests will panic with a clear message. That is expected.
  </verify>
  <done>
Integration test module exists in redis.rs with 18+ tests covering all 6 StorageBackend methods. Tests are gated behind `redis-tests` feature flag. Each test uses UUID-based key prefix isolation. Tests pass against local Redis with `cargo test -p pmcp-tasks --features redis-tests -- redis_ --test-threads=1`. All existing 561+ tests continue to pass without modification.
  </done>
</task>

</tasks>

<verification>
1. `cargo test -p pmcp-tasks --features redis-tests -- redis_ --test-threads=1` passes all 18+ Redis tests
2. `cargo test -p pmcp-tasks` passes all existing tests (no regressions)
3. `cargo clippy -p pmcp-tasks --features redis -- -D warnings` passes (zero warnings)
4. `cargo fmt -p pmcp-tasks --check` passes (clean formatting)
5. `grep -c "async fn redis_" crates/pmcp-tasks/src/store/redis.rs` shows 18+ test functions
6. `grep "redis-tests" crates/pmcp-tasks/src/store/redis.rs` confirms feature gate
</verification>

<success_criteria>
- 18+ integration tests cover all 6 StorageBackend methods for RedisBackend
- Tests gated behind `redis-tests` feature flag (not run by default)
- UUID-based test isolation (no FLUSHDB, no cleanup needed)
- TTL tests verify both hash field presence and application-level filtering
- CAS tests verify atomic version checking behavior
- Sorted set index tests verify owner-scoped isolation
- All tests pass against local Redis
- All existing 561+ tests pass unchanged
</success_criteria>

<output>
After completion, create `.planning/phases/12-redis-backend/12-02-SUMMARY.md`
</output>
