---
phase: 01-foundation-types-and-store-contract
plan: 03
type: tdd
wave: 3
depends_on:
  - 01-01
  - 01-02
files_modified:
  - crates/pmcp-tasks/tests/protocol_types.rs
  - crates/pmcp-tasks/tests/state_machine.rs
  - crates/pmcp-tasks/tests/property_tests.rs
autonomous: true
requirements:
  - TEST-01
  - TEST-02

must_haves:
  truths:
    - "All wire types round-trip through serde_json and produce JSON matching the MCP 2025-11-25 spec exactly"
    - "Task serializes ttl as null when None (not omitted) and omits pollInterval when None"
    - "CreateTaskResult wraps task in a task field; GetTaskResult is flat task fields"
    - "TaskStatus state machine rejects all invalid transitions including self-transitions and transitions from terminal states"
    - "TaskStatus state machine accepts all valid transitions: working->4 targets, input_required->4 targets"
    - "State machine tests cover every cell of the 5x5 transition matrix"
    - "Property tests verify state machine invariants, serde round-trip stability, and TTL correctness under arbitrary inputs"
    - "Fuzz-style deserialization tests verify Task and TaskStatus handle arbitrary JSON inputs without panicking"
  artifacts:
    - path: "crates/pmcp-tasks/tests/protocol_types.rs"
      provides: "Serialization round-trip tests for all wire types"
      contains: "test_task_serialization"
    - path: "crates/pmcp-tasks/tests/state_machine.rs"
      provides: "State machine transition tests (valid and invalid)"
      contains: "test_valid_transitions"
    - path: "crates/pmcp-tasks/tests/property_tests.rs"
      provides: "Property-based tests using proptest for state machine invariants, serde round-trips, TTL correctness, and fuzz deserialization"
      contains: "proptest!"
  key_links:
    - from: "crates/pmcp-tasks/tests/protocol_types.rs"
      to: "crates/pmcp-tasks/src/types/task.rs"
      via: "Tests Task, CreateTaskResult serialization"
      pattern: "use pmcp_tasks::"
    - from: "crates/pmcp-tasks/tests/state_machine.rs"
      to: "crates/pmcp-tasks/src/types/task.rs"
      via: "Tests TaskStatus::can_transition_to and validate_transition"
      pattern: "TaskStatus::.*can_transition_to"
    - from: "crates/pmcp-tasks/tests/property_tests.rs"
      to: "crates/pmcp-tasks/src/types/task.rs"
      via: "Property tests over arbitrary TaskStatus and Task instances"
      pattern: "proptest!"
---

<objective>
Write comprehensive serialization, state machine, and property-based tests using TDD (RED-GREEN-REFACTOR) to verify all wire types match the MCP 2025-11-25 spec, the state machine correctly validates all transitions, and invariants hold under arbitrary inputs.

Purpose: Tests are the proof that types match the spec. Serialization bugs (like omitting ttl instead of serializing null) are subtle and only caught by testing against known-good JSON. State machine bugs allow invalid transitions that corrupt task lifecycle. Property tests verify invariants hold for ALL possible inputs, not just hand-picked examples. Fuzz-style deserialization tests ensure no panics on untrusted input.

Output: Three integration test files covering TEST-01 (serialization round-trips), TEST-02 (state machine transitions), and property/fuzz testing (per CLAUDE.md ALWAYS requirements: property testing and fuzz testing for all new features).
</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/01-foundation-types-and-store-contract/01-CONTEXT.md
@.planning/phases/01-foundation-types-and-store-contract/01-RESEARCH.md
@.planning/phases/01-foundation-types-and-store-contract/01-01-SUMMARY.md
@.planning/phases/01-foundation-types-and-store-contract/01-02-SUMMARY.md
@crates/pmcp-tasks/src/types/task.rs
@crates/pmcp-tasks/src/types/params.rs
@crates/pmcp-tasks/src/types/capabilities.rs
@crates/pmcp-tasks/src/types/notification.rs
@crates/pmcp-tasks/src/types/execution.rs
@crates/pmcp-tasks/src/error.rs
@crates/pmcp-tasks/src/domain/record.rs
@crates/pmcp-tasks/src/domain/variables.rs
</context>

<tasks>

<task type="auto">
  <name>Task 1: Protocol type serialization round-trip tests (TEST-01)</name>
  <files>
    crates/pmcp-tasks/tests/protocol_types.rs
  </files>
  <action>
Create `crates/pmcp-tasks/tests/protocol_types.rs` with comprehensive serialization tests.

Use TDD approach: write tests first, run to confirm they pass (types already implemented in Plan 01). If any test fails, fix the type definition.

**Test categories:**

1. **TaskStatus serialization**:
   - Each variant serializes to correct snake_case string: "working", "input_required", "completed", "failed", "cancelled"
   - Round-trip: serialize then deserialize produces same variant
   - Unknown status string produces deserialization error

2. **Task serialization (critical -- spec compliance)**:
   - Full Task with all fields serializes with correct camelCase keys: taskId, status, statusMessage, createdAt, lastUpdatedAt, ttl, pollInterval
   - `ttl: None` serializes as `"ttl": null` (NOT omitted) -- this is the #1 pitfall from RESEARCH.md
   - `poll_interval: None` is omitted from JSON (skip_serializing_if works)
   - `status_message: None` is omitted from JSON
   - `_meta: None` is omitted from JSON
   - `_meta: Some(map)` is included in JSON
   - Round-trip: serialize -> JSON string -> deserialize produces equivalent Task
   - Deserialize from spec example JSON (from RESEARCH.md code examples)

3. **CreateTaskResult serialization**:
   - Task is wrapped in `"task"` field: `{"task": {"taskId": "...", ...}}`
   - _meta field included when present, omitted when None
   - Round-trip with spec example JSON

4. **GetTaskResult / CancelTaskResult**:
   - These are type aliases for Task, so they serialize flat (no wrapper)
   - Verify that serde_json::to_value produces flat taskId at top level

5. **TaskParams serialization**:
   - All fields optional, serializes with camelCase
   - Empty params serializes to `{}`

6. **Request param types** (TaskGetParams, TaskResultParams, TaskListParams, TaskCancelParams):
   - TaskGetParams: `{"taskId": "abc"}` round-trip
   - TaskListParams: cursor omitted when None, included when Some
   - TaskCancelParams: `{"taskId": "abc"}` round-trip

7. **Capability types**:
   - `ServerTaskCapabilities::full()` serializes to correct nested structure with list, cancel, and requests.tools.call
   - `ServerTaskCapabilities::tools_only()` serializes with only requests.tools.call (no list/cancel)
   - ClientTaskCapabilities serializes with `"supported": true/false`
   - Round-trip all capability types

8. **TaskStatusNotification serialization**:
   - All fields with correct camelCase keys
   - ttl serializes as null when None (same as Task)
   - pollInterval omitted when None

9. **TaskSupport/ToolExecution serialization**:
   - TaskSupport::Forbidden serializes as `"forbidden"`, Optional as `"optional"`, Required as `"required"`
   - ToolExecution with default TaskSupport serializes correctly
   - Round-trip

10. **Related-task metadata helper**:
    - `related_task_meta("some-id")` produces `{"io.modelcontextprotocol/related-task": {"taskId": "some-id"}}`

11. **Constants verification**:
    - RELATED_TASK_META_KEY equals "io.modelcontextprotocol/related-task"
    - MODEL_IMMEDIATE_RESPONSE_META_KEY equals "io.modelcontextprotocol/model-immediate-response"
    - Method name constants match spec

Use `pretty_assertions` for readable diffs on JSON comparison failures.
Use `serde_json::json!()` macro for expected JSON values.

Each test function should be named descriptively: `test_task_status_serializes_snake_case`, `test_task_ttl_null_not_omitted`, `test_create_task_result_wraps_in_task_field`, etc.
  </action>
  <verify>
    Run `cargo test --package pmcp-tasks --test protocol_types` -- all tests pass.
    Run `cargo test --package pmcp-tasks --test protocol_types -- --list 2>&1 | wc -l` -- at least 15 test functions.
  </verify>
  <done>
    Protocol type serialization tests pass. All wire types round-trip correctly through serde_json. Critical spec compliance verified: ttl serializes as null, CreateTaskResult wraps in task field, GetTaskResult is flat. At least 15 test cases covering all wire types.
  </done>
</task>

<task type="auto">
  <name>Task 2: State machine transition tests (TEST-02)</name>
  <files>
    crates/pmcp-tasks/tests/state_machine.rs
  </files>
  <action>
Create `crates/pmcp-tasks/tests/state_machine.rs` with exhaustive state machine tests.

**Test categories:**

1. **is_terminal tests**:
   - Working is NOT terminal
   - InputRequired is NOT terminal
   - Completed IS terminal
   - Failed IS terminal
   - Cancelled IS terminal

2. **Valid transitions (8 total)**:
   - Working -> InputRequired: ALLOWED
   - Working -> Completed: ALLOWED
   - Working -> Failed: ALLOWED
   - Working -> Cancelled: ALLOWED
   - InputRequired -> Working: ALLOWED
   - InputRequired -> Completed: ALLOWED
   - InputRequired -> Failed: ALLOWED
   - InputRequired -> Cancelled: ALLOWED
   - Test each with `can_transition_to` returning true
   - Test each with `validate_transition` returning Ok(())

3. **Invalid transitions -- self-transitions (5 total)**:
   - Working -> Working: REJECTED
   - InputRequired -> InputRequired: REJECTED
   - Completed -> Completed: REJECTED
   - Failed -> Failed: REJECTED
   - Cancelled -> Cancelled: REJECTED
   - Test each with `can_transition_to` returning false
   - Test each with `validate_transition` returning Err(InvalidTransition)

4. **Invalid transitions -- from terminal states (12 total)**:
   - Completed -> {Working, InputRequired, Failed, Cancelled}: ALL REJECTED
   - Failed -> {Working, InputRequired, Completed, Cancelled}: ALL REJECTED
   - Cancelled -> {Working, InputRequired, Completed, Failed}: ALL REJECTED
   - Test each with `can_transition_to` returning false

5. **Exhaustive 5x5 matrix test**:
   - Create a test that iterates ALL 25 possible (from, to) combinations
   - Verify each cell matches the expected allow/reject table
   - This catches any future regressions if someone adds a state

6. **validate_transition error quality**:
   - When InvalidTransition is returned, verify it contains:
     - Correct task_id
     - Correct `from` status
     - Correct `to` status
   - Verify error Display message is readable

7. **TaskRecord constructor tests** (verifying state machine integration):
   - `TaskRecord::new()` creates task in Working state
   - `TaskRecord::new()` generates valid UUID v4 task_id
   - `TaskRecord::new()` sets created_at and last_updated_at to same value
   - `TaskRecord::is_expired()` returns false for fresh task with TTL
   - `TaskRecord::is_expired()` returns false for task with None TTL (never expires)

8. **TaskWithVariables _meta injection tests**:
   - `to_wire_task()` with empty variables produces task with no _meta change
   - `to_wire_task()` with variables produces task with variables at top level of _meta
   - Variables do NOT appear as a separate field on the task (only in _meta)
   - Null-value variables are preserved in _meta (deletion happens at store level, not wire level)

Use `pretty_assertions` for readable error output.
Group tests with `mod` blocks for organization (e.g., `mod is_terminal`, `mod valid_transitions`, `mod invalid_transitions`, `mod exhaustive_matrix`).
  </action>
  <verify>
    Run `cargo test --package pmcp-tasks --test state_machine` -- all tests pass.
    Run `cargo test --package pmcp-tasks --test state_machine -- --list 2>&1 | wc -l` -- at least 20 test functions.
    Run `cargo test --package pmcp-tasks` -- ALL tests pass (both test files).
  </verify>
  <done>
    State machine tests pass. All 25 cells of the 5x5 transition matrix are verified. Valid transitions (8) accepted. Self-transitions (5) rejected. Terminal-state transitions (12) rejected. Error messages contain rich context. TaskRecord constructor and TaskWithVariables _meta injection also verified. At least 20 test cases.
  </done>
</task>

<task type="auto">
  <name>Task 3: Property tests and fuzz deserialization tests (CLAUDE.md compliance)</name>
  <files>
    crates/pmcp-tasks/tests/property_tests.rs
  </files>
  <action>
Create `crates/pmcp-tasks/tests/property_tests.rs` with property-based tests using proptest (already in dev-dependencies). This task satisfies the CLAUDE.md ALWAYS requirements for property testing AND fuzz testing for every new feature. Using proptest for fuzz-style testing per CLAUDE.md which states "cargo fuzz OR proptest" as acceptable approaches. proptest is preferred here because it does not require nightly Rust, integrates with the standard test harness, and provides shrinking for minimal failure cases.

**Property test categories (using `proptest!` macro):**

1. **Terminal state invariant** -- For ALL terminal states (Completed, Failed, Cancelled), `can_transition_to(target)` is ALWAYS false for ANY target status:
   ```rust
   proptest! {
       #[test]
       fn terminal_states_reject_all_transitions(
           from in prop::sample::select(vec![TaskStatus::Completed, TaskStatus::Failed, TaskStatus::Cancelled]),
           to in prop::sample::select(vec![TaskStatus::Working, TaskStatus::InputRequired, TaskStatus::Completed, TaskStatus::Failed, TaskStatus::Cancelled]),
       ) {
           prop_assert!(!from.can_transition_to(&to));
       }
   }
   ```

2. **Self-transition invariant** -- For ALL statuses, `can_transition_to(self)` is ALWAYS false:
   ```rust
   proptest! {
       #[test]
       fn no_self_transitions(
           status in arb_task_status(),
       ) {
           prop_assert!(!status.can_transition_to(&status));
       }
   }
   ```

3. **is_terminal consistency** -- `is_terminal()` returns true if and only if `can_transition_to` returns false for ALL possible targets:
   ```rust
   proptest! {
       #[test]
       fn is_terminal_iff_no_valid_transitions(
           status in arb_task_status(),
       ) {
           let all_statuses = [TaskStatus::Working, TaskStatus::InputRequired, TaskStatus::Completed, TaskStatus::Failed, TaskStatus::Cancelled];
           let has_any_transition = all_statuses.iter().any(|t| status.can_transition_to(t));
           prop_assert_eq!(status.is_terminal(), !has_any_transition);
       }
   }
   ```

4. **Serde round-trip for TaskStatus** -- Arbitrary TaskStatus serializes and deserializes without panic or data loss:
   ```rust
   proptest! {
       #[test]
       fn task_status_serde_round_trip(status in arb_task_status()) {
           let json = serde_json::to_value(&status).unwrap();
           let back: TaskStatus = serde_json::from_value(json).unwrap();
           prop_assert_eq!(status, back);
       }
   }
   ```

5. **Serde round-trip for Task** -- Arbitrary Task with various field combinations serializes and deserializes without panic. Generate arbitrary Tasks with:
   - Random task_id strings
   - Random TaskStatus
   - Optional status_message (Some/None)
   - Random timestamp strings
   - Optional ttl (Some(u64)/None) -- verify None round-trips through null correctly
   - Optional poll_interval (Some/None)

6. **New TaskRecord is never expired** -- A freshly created TaskRecord with any valid TTL (including None) has `is_expired() == false`:
   ```rust
   proptest! {
       #[test]
       fn fresh_task_record_is_not_expired(ttl in proptest::option::of(1u64..=u64::MAX)) {
           let record = TaskRecord::new("owner".to_string(), "tools/call".to_string(), ttl);
           prop_assert!(!record.is_expired());
       }
   }
   ```

7. **Fuzz deserialization: TaskStatus from arbitrary strings** -- Deserializing arbitrary strings as TaskStatus either succeeds with a valid variant or fails without panicking (no unwinding panics):
   ```rust
   proptest! {
       #[test]
       fn fuzz_task_status_deserialization(s in "\\PC*") {
           let json_str = format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""));
           // Must not panic -- Ok or Err are both fine
           let _ = serde_json::from_str::<TaskStatus>(&json_str);
       }
   }
   ```

8. **Fuzz deserialization: Task from arbitrary JSON** -- Deserializing arbitrary JSON bytes as Task either succeeds or fails without panicking:
   ```rust
   proptest! {
       #[test]
       fn fuzz_task_deserialization_from_bytes(bytes in proptest::collection::vec(any::<u8>(), 0..1024)) {
           // Must not panic -- Ok or Err are both fine
           let _ = serde_json::from_slice::<Task>(&bytes);
       }
   }
   ```

9. **Fuzz deserialization: Task from arbitrary JSON values** -- Generate arbitrary JSON objects and attempt to deserialize as Task:
   ```rust
   proptest! {
       #[test]
       fn fuzz_task_deserialization_from_json_string(s in "\\PC{0,512}") {
           // Must not panic
           let _ = serde_json::from_str::<Task>(&s);
       }
   }
   ```

**Helper: Arbitrary TaskStatus strategy:**
```rust
fn arb_task_status() -> impl Strategy<Value = TaskStatus> {
    prop::sample::select(vec![
        TaskStatus::Working,
        TaskStatus::InputRequired,
        TaskStatus::Completed,
        TaskStatus::Failed,
        TaskStatus::Cancelled,
    ])
}
```

**Helper: Arbitrary Task strategy:**
```rust
fn arb_task() -> impl Strategy<Value = Task> {
    (
        "[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}",  // uuid-like
        arb_task_status(),
        proptest::option::of("[a-zA-Z0-9 ]{0,100}"),    // status_message
        "2025-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z",  // created_at
        "2025-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z",  // last_updated_at
        proptest::option::of(0u64..=86_400_000u64),      // ttl
        proptest::option::of(1000u64..=60_000u64),        // poll_interval
    ).prop_map(|(task_id, status, status_message, created_at, last_updated_at, ttl, poll_interval)| {
        Task {
            task_id,
            status,
            status_message,
            created_at,
            last_updated_at,
            ttl,
            poll_interval,
            _meta: None,
        }
    })
}
```

Run at least 256 cases per property (proptest default). Document in test file header that proptest satisfies both property testing and fuzz testing CLAUDE.md requirements.
  </action>
  <verify>
    Run `cargo test --package pmcp-tasks --test property_tests` -- all property tests pass.
    Run `cargo test --package pmcp-tasks --test property_tests -- --list 2>&1 | wc -l` -- at least 9 test functions.
    Run `cargo test --package pmcp-tasks` -- ALL tests pass (all three test files).
    Run `cargo clippy --package pmcp-tasks --tests -- -D warnings` -- zero clippy warnings in test code.
  </verify>
  <done>
    Property tests verify: terminal state invariant holds for all combinations, self-transitions rejected for all statuses, is_terminal is consistent with can_transition_to, TaskStatus and Task serde round-trips succeed for arbitrary inputs, fresh TaskRecord is never expired. Fuzz deserialization tests verify Task and TaskStatus handle arbitrary bytes, strings, and JSON without panicking. At least 9 property test cases using proptest. CLAUDE.md property testing and fuzz testing requirements satisfied via proptest.
  </done>
</task>

</tasks>

<verification>
1. `cargo test --package pmcp-tasks` -- ALL tests pass
2. `cargo test --package pmcp-tasks -- --list 2>&1 | wc -l` -- at least 44 test functions total (15 + 20 + 9)
3. `cargo clippy --package pmcp-tasks --tests -- -D warnings` -- zero clippy warnings in test code
4. Critical spec compliance tests exist: ttl null serialization, CreateTaskResult wrapping, GetTaskResult flat
5. Full 5x5 state machine matrix covered
6. TaskWithVariables _meta injection tested
7. Property tests cover state machine invariants with arbitrary inputs (proptest)
8. Fuzz deserialization tests cover Task and TaskStatus from arbitrary bytes/strings (proptest)
</verification>

<success_criteria>
- All protocol type serialization tests pass (TEST-01)
- All state machine transition tests pass (TEST-02)
- All property and fuzz tests pass (CLAUDE.md compliance)
- Zero clippy warnings in test code
- At least 44 test functions across all three files
- Every critical pitfall from RESEARCH.md has a corresponding test
- proptest covers: terminal state invariant, self-transition invariant, serde round-trip, TTL freshness, fuzz deserialization
</success_criteria>

<output>
After completion, create `.planning/phases/01-foundation-types-and-store-contract/01-03-SUMMARY.md`
</output>
