---
phase: 11-dynamodb-backend
plan: 02
type: execute
wave: 2
depends_on: ["11-01"]
files_modified:
  - crates/pmcp-tasks/src/store/dynamodb.rs
autonomous: true
requirements: [DYNA-06, TEST-02]
user_setup:
  - service: aws-dynamodb
    why: "Integration tests run against real DynamoDB in AWS"
    env_vars:
      - name: AWS_ACCESS_KEY_ID
        source: "AWS IAM Console -> Users -> Security credentials -> Create access key"
      - name: AWS_SECRET_ACCESS_KEY
        source: "AWS IAM Console -> Users -> Security credentials -> Create access key"
      - name: AWS_DEFAULT_REGION
        source: "Set to the region where your DynamoDB table is provisioned (e.g., us-east-1)"
    dashboard_config:
      - task: "Create DynamoDB table named pmcp_tasks"
        location: "AWS Console -> DynamoDB -> Create table"
        details: >
          Table name: pmcp_tasks.
          Partition key: PK (String).
          Sort key: SK (String).
          Settings: On-demand capacity mode recommended for testing.
          After creation: Enable TTL on attribute 'expires_at' via
          Table -> Additional settings -> Time to live -> Turn on TTL -> Attribute name: expires_at.

must_haves:
  truths:
    - "Integration tests exercise all 6 StorageBackend methods against real DynamoDB"
    - "Tests are gated behind dynamodb-tests feature flag and do not run by default"
    - "Each test run uses a unique owner prefix for isolation (no table cleanup needed)"
    - "TTL verification checks expires_at attribute is set correctly on items (does not wait for actual deletion)"
    - "CAS conflict test verifies VersionConflict error on version mismatch"
  artifacts:
    - path: "crates/pmcp-tasks/src/store/dynamodb.rs"
      provides: "Integration test module with StorageBackend contract tests for DynamoDbBackend"
      contains: "dynamodb-tests"
  key_links:
    - from: "crates/pmcp-tasks/src/store/dynamodb.rs (integration_tests)"
      to: "crates/pmcp-tasks/src/store/dynamodb.rs (DynamoDbBackend)"
      via: "StorageBackend trait method calls"
      pattern: "backend\\.(get|put|put_if_version|delete|list_by_prefix|cleanup_expired)"
    - from: "crates/pmcp-tasks/src/store/dynamodb.rs (integration_tests)"
      to: "AWS DynamoDB"
      via: "from_env() constructor using AWS SDK config chain"
      pattern: "DynamoDbBackend::from_env"
---

<objective>
Add integration tests for DynamoDbBackend that run against real DynamoDB, gated behind the `dynamodb-tests` feature flag.

Purpose: Validate that DynamoDbBackend correctly implements the StorageBackend contract against real AWS infrastructure, matching the per-backend contract test pattern established in Phase 10 for InMemoryBackend.
Output: Complete integration test module covering all 6 StorageBackend methods, ready for CI execution when AWS credentials are available.
</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/11-dynamodb-backend/11-CONTEXT.md
@.planning/phases/11-dynamodb-backend/11-RESEARCH.md
@.planning/phases/11-dynamodb-backend/11-01-SUMMARY.md
@crates/pmcp-tasks/src/store/dynamodb.rs
@crates/pmcp-tasks/src/store/memory.rs
@crates/pmcp-tasks/src/store/backend.rs
</context>

<tasks>

<task type="auto">
  <name>Task 1: DynamoDB integration test module with all StorageBackend contract tests</name>
  <files>crates/pmcp-tasks/src/store/dynamodb.rs</files>
  <action>
Add an integration test module at the bottom of `dynamodb.rs`, gated behind `#[cfg(all(test, feature = "dynamodb-tests"))]`. Follow the same contract test pattern established in `memory.rs::backend_tests` (Phase 10).

1. **Test helper function:**
   ```rust
   async fn test_backend() -> (DynamoDbBackend, String) {
       let backend = DynamoDbBackend::from_env().await;
       let test_prefix = format!("test-{}", uuid::Uuid::new_v4());
       (backend, test_prefix)
   }
   ```
   Each test gets a unique owner prefix (random UUID) for isolation -- no shared state between tests, no cleanup needed.

2. **Test coverage -- mirror the InMemoryBackend contract tests from memory.rs::backend_tests:**

   **get tests (2):**
   - `get_missing_key_returns_not_found` -- get a key that doesn't exist, expect `StorageError::NotFound`
   - `get_returns_stored_data` -- put then get, verify data and version match

   **put tests (3):**
   - `put_new_key_returns_version_1` -- first put to a new key returns version 1
   - `put_existing_key_increments_version` -- second put returns version 2, data updated
   - `put_overwrites_data` -- verify data is replaced on second put

   **put_if_version tests (4):**
   - `put_if_version_succeeds_on_match` -- put, then put_if_version with matching version, expect success and version+1
   - `put_if_version_fails_on_mismatch` -- put, then put_if_version with wrong version, expect `StorageError::VersionConflict`
   - `put_if_version_fails_on_missing_key` -- put_if_version on nonexistent key, expect `StorageError::NotFound`
   - `put_if_version_updates_data` -- verify data is updated after successful CAS

   **delete tests (3):**
   - `delete_existing_returns_true` -- put then delete, expect true
   - `delete_missing_returns_false` -- delete nonexistent key, expect false
   - `delete_then_get_returns_not_found` -- put, delete, get, expect NotFound

   **list_by_prefix tests (3):**
   - `list_by_prefix_returns_matching` -- put keys with two different owners, list one owner, verify only their keys returned
   - `list_by_prefix_empty_on_no_match` -- list with a prefix that has no items, expect empty vec
   - `list_by_prefix_returns_correct_data_and_versions` -- put multiple items, update one, verify data and versions correct

   **cleanup_expired tests (1):**
   - `cleanup_expired_returns_zero` -- verify no-op returns Ok(0)

   **TTL verification tests (2):**
   - `put_sets_expires_at_attribute_when_ttl_present` -- create a TaskRecord with TTL, serialize it, put it, then use `self.client.get_item()` directly to inspect the raw DynamoDB item and verify `expires_at` attribute exists with correct epoch seconds value
   - `put_omits_expires_at_when_no_ttl` -- create a TaskRecord without TTL, serialize, put, get_item directly, verify `expires_at` attribute is absent

   Total: ~18 tests matching the InMemoryBackend contract test count.

3. **Test naming convention:** Use `ddb_` prefix to distinguish from in-memory tests (e.g., `ddb_get_missing_key_returns_not_found`).

4. **Key construction in tests:** All keys use the format `{test_prefix}:task-{N}` where `test_prefix` is the unique UUID-based prefix from `test_backend()`. For list_by_prefix tests, use different prefixes (e.g., `{test_prefix}-a` and `{test_prefix}-b`) to test owner isolation.

5. **TTL verification approach:** For the TTL tests, construct a real `TaskRecord` with and without expiry, serialize via `serde_json::to_vec`, and put the bytes. Then use the backend's client directly (`backend.client.get_item()`) to inspect raw DynamoDB attributes. Verify `expires_at` is a Number attribute with the correct epoch seconds. Do NOT wait for DynamoDB's actual TTL deletion (up to 48 hours).

6. **All tests must be `#[tokio::test]` and `async`.**

7. **Add a module doc comment explaining:** tests require AWS credentials and a `pmcp_tasks` table, run with `cargo test -p pmcp-tasks --features dynamodb-tests`, each test is isolated via unique owner prefix.
  </action>
  <verify>
Run `cargo check -p pmcp-tasks --features dynamodb-tests` (test module compiles).
Run `cargo clippy -p pmcp-tasks --features dynamodb-tests --tests -- -D warnings` (zero warnings in tests).
Run `cargo test -p pmcp-tasks` (existing tests still pass, dynamodb tests NOT run without feature).
Optionally, if AWS credentials are configured: `cargo test -p pmcp-tasks --features dynamodb-tests -- ddb_ --test-threads=1` (all integration tests pass against real DynamoDB).
  </verify>
  <done>Integration test module exists with ~18 StorageBackend contract tests for DynamoDbBackend, gated behind dynamodb-tests feature flag. Tests compile. Existing tests unaffected. Test isolation via unique UUID owner prefix per test run. TTL tests verify expires_at attribute presence/absence without waiting for actual deletion.</done>
</task>

<task type="auto">
  <name>Task 2: Cleanup test helper access and final verification</name>
  <files>crates/pmcp-tasks/src/store/dynamodb.rs</files>
  <action>
1. If the integration tests need to access `DynamoDbBackend.client` directly (for TTL attribute inspection), add a `#[cfg(test)]` accessor method:
   ```rust
   #[cfg(test)]
   pub(crate) fn client(&self) -> &aws_sdk_dynamodb::Client {
       &self.client
   }
   ```
   This follows the same pattern as `InMemoryTaskStore::backend()` which is `#[cfg(test)]`.

2. Add a `#[cfg(test)]` accessor for `table_name` if needed by tests:
   ```rust
   #[cfg(test)]
   pub(crate) fn table_name(&self) -> &str {
       &self.table_name
   }
   ```

3. Run the full quality gate:
   - `cargo fmt -p pmcp-tasks --check`
   - `cargo clippy -p pmcp-tasks --features dynamodb -- -D warnings`
   - `cargo clippy -p pmcp-tasks --features dynamodb-tests --tests -- -D warnings`
   - `cargo test -p pmcp-tasks` (all existing tests pass)
   - `cargo doc -p pmcp-tasks --features dynamodb --no-deps` (docs clean)

4. Ensure the module structure matches the research recommendation:
   ```
   crates/pmcp-tasks/src/store/
     mod.rs        -- #[cfg(feature = "dynamodb")] pub mod dynamodb;
     dynamodb.rs   -- DynamoDbBackend + StorageBackend impl + integration_tests
   ```
  </action>
  <verify>
Run `cargo fmt -p pmcp-tasks --check` (clean).
Run `cargo clippy -p pmcp-tasks --features dynamodb-tests --tests -- -D warnings` (zero warnings).
Run `cargo test -p pmcp-tasks` (all 561+ existing tests pass).
Run `cargo doc -p pmcp-tasks --features dynamodb --no-deps` (no doc warnings).
  </verify>
  <done>DynamoDbBackend is complete with full StorageBackend implementation and integration tests. All quality gates pass. Feature flag isolation verified. Ready for production use with GenericTaskStore.</done>
</task>

</tasks>

<verification>
1. `cargo check -p pmcp-tasks --features dynamodb-tests` compiles (tests included)
2. `cargo clippy -p pmcp-tasks --features dynamodb-tests --tests -- -D warnings` zero warnings
3. `cargo test -p pmcp-tasks` all existing tests pass (dynamodb tests excluded)
4. Integration test module contains ~18 tests covering all 6 StorageBackend methods
5. Tests use unique UUID owner prefix for isolation
6. TTL tests verify expires_at attribute correctness without waiting for deletion
7. `cargo doc -p pmcp-tasks --features dynamodb --no-deps` produces clean documentation
</verification>

<success_criteria>
- Integration test module exists in dynamodb.rs, gated behind `dynamodb-tests` feature flag
- ~18 contract tests cover get, put, put_if_version, delete, list_by_prefix, cleanup_expired, and TTL attribute verification
- Tests do not run without the feature flag (no CI breakage without AWS credentials)
- Each test uses unique UUID-based owner prefix for isolation
- TTL tests verify attribute presence/absence, not actual DynamoDB TTL deletion timing
- All existing 561+ tests pass unchanged
- Zero clippy warnings across all feature flag combinations
</success_criteria>

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