|
| 1 | +# Fix the 2 remaining test failures in test/wf_case_runner_tests.erl: Implementation Plan |
| 2 | + |
| 3 | +## Implementation Plan Title |
| 4 | + |
| 5 | +Fix State Transition and Status Reporting in wf_case_runner gen_statem |
| 6 | + |
| 7 | +## Overview |
| 8 | + |
| 9 | +This plan fixes two bugs in the `wf_case_runner` gen_statem implementation that cause test failures: |
| 10 | + |
| 11 | +1. **State Transition Bug**: The `enter_state/2` function returns a plain `StateData` record instead of a gen_statem tuple, preventing proper state transitions from `running` to `done`. |
| 12 | +2. **Status Map Inconsistency**: The `done/3` state handler returns a status map missing `ip` and `step_count` keys that tests expect. |
| 13 | + |
| 14 | +These bugs prevent workflow cases from completing properly and cause the `status_query_test` to fail when querying the `ip` key. |
| 15 | + |
| 16 | +## Current State |
| 17 | + |
| 18 | +**Test Failures:** |
| 19 | +- `running_to_done_completion_test/1` (line 46-56): Expects case to reach `done` state within 100ms, but it remains in `running` state indefinitely |
| 20 | +- `status_query_test/1` (line 82-88): Expects status map to contain `ip` and `step_count` keys, but `done/3` handler returns map with only `state` and `result` |
| 21 | + |
| 22 | +**Root Causes:** |
| 23 | + |
| 24 | +**Issue 1: `enter_state/2` Returns Wrong Type** (`src/wf_case_runner.erl:391-401`) |
| 25 | +```erlang |
| 26 | +enter_state(TargetState, StateData) -> |
| 27 | + case TargetState of |
| 28 | + done -> StateData; %% Returns StateData instead of {next_state, done, StateData} |
| 29 | + cancelled -> StateData; %% Returns StateData instead of {next_state, cancelled, StateData} |
| 30 | + _Other -> StateData |
| 31 | + end. |
| 32 | +``` |
| 33 | + |
| 34 | +The function is called from `execute_quantum/1` (lines 358, 365) when execution completes. The return value is used directly as the gen_statem return value: |
| 35 | +```erlang |
| 36 | +running(cast, {signal, Signal}, StateData) -> |
| 37 | + ... |
| 38 | + {next_state, running, execute_quantum(StateData)}. |
| 39 | +``` |
| 40 | + |
| 41 | +When `execute_quantum/1` calls `enter_state(done, StateData1)`, it returns `StateData1` (a record), but the caller expects a gen_statem tuple like `{next_state, done, StateData1}`. This causes the gen_statem to remain in the `running` state instead of transitioning to `done`. |
| 42 | + |
| 43 | +**Issue 2: `done/3` Status Handler Missing Fields** (`src/wf_case_runner.erl:295-298`) |
| 44 | +```erlang |
| 45 | +done({call, From}, status, StateData) -> |
| 46 | + StatusInfo = #{state => done, result => StateData#state_data.result}, |
| 47 | + {keep_state_and_data, [{reply, From, {ok, done, StatusInfo}}]}. |
| 48 | +``` |
| 49 | + |
| 50 | +The `running/3` state handler (lines 205-214) returns a richer status map: |
| 51 | +```erlang |
| 52 | +running({call, From}, status, StateData) -> |
| 53 | + ExecState = StateData#state_data.exec_state, |
| 54 | + StatusInfo = #{ |
| 55 | + state => running, |
| 56 | + ip => ExecState#exec_state.ip, |
| 57 | + step_count => ExecState#exec_state.step_count, |
| 58 | + status => ExecState#exec_state.status |
| 59 | + }, |
| 60 | + {keep_state_and_data, [{reply, From, {ok, running, StatusInfo}}]}. |
| 61 | +``` |
| 62 | + |
| 63 | +The test at line 86 asserts `maps:is_key(ip, StatusInfo)`, which fails when the case is in the `done` state because the status map lacks the `ip` key. |
| 64 | + |
| 65 | +## Desired End State |
| 66 | + |
| 67 | +**Functional Requirements:** |
| 68 | +1. Workflow cases executing the simple task bytecode (`[{'TASK_EXEC', mock_task}, {'DONE'}]`) transition from `running` to `done` state upon completion |
| 69 | +2. Status queries return consistent map structure across all states (`running`, `done`, `cancelled`) |
| 70 | +3. All 5 tests in `wf_case_runner_tests` pass: |
| 71 | + - `running_to_done_completion_test` |
| 72 | + - `signal_delivery_test` |
| 73 | + - `cancel_test` |
| 74 | + - `status_query_test` |
| 75 | + - `trace_configuration_test` |
| 76 | + |
| 77 | +**Verification:** |
| 78 | +```bash |
| 79 | +rebar3 compile |
| 80 | +rebar3 eunit --module=wf_case_runner_tests |
| 81 | +``` |
| 82 | + |
| 83 | +Expected output: All tests pass with 0 failures. |
| 84 | + |
| 85 | +### Key Discoveries: |
| 86 | + |
| 87 | +- **`execute_quantum/1` Return Type is Wrong** (`src/wf_case_runner.erl:340`): The spec says it returns `state_data()`, but it actually returns gen_statem tuples like `{next_state, waiting_effect, StateData, Actions}` in the yield case (lines 376, 380). This is actually correct for the callers, but the spec needs updating. |
| 88 | + |
| 89 | +- **`enter_state/2` is a State Transition Helper**: The function is designed to abstract state transitions, but it's currently implemented as a no-op that always returns `StateData`. It should return gen_statem tuples for state changes. |
| 90 | + |
| 91 | +- **Status Map Consistency Pattern**: The `handle_common_event/3` function (lines 308-316) provides a fallback status handler that returns consistent maps with `ip`, `step_count`, and `status` keys. State-specific handlers should follow this pattern. |
| 92 | + |
| 93 | +- **Test Timing is Sufficient**: The mock task completes immediately (no I/O or blocking operations), so 100ms is more than adequate for the case to complete. The failure is due to the state transition bug, not timing. |
| 94 | + |
| 95 | +## What We're NOT Doing |
| 96 | + |
| 97 | +1. **Fixing the function spec for `execute_quantum/1`**: While the spec is technically wrong (it says `state_data()` but returns gen_statem tuples), fixing the spec is not required to make the tests pass. The spec can be updated in a separate cleanup task. |
| 98 | + |
| 99 | +2. **Refactoring status query handling**: Currently there are three places where status queries are handled: |
| 100 | + - `running/3` (lines 205-214) |
| 101 | + - `done/3` (lines 295-298) |
| 102 | + - `cancelled/3` (lines 282-285) |
| 103 | + - `handle_common_event/3` (lines 308-316, fallback) |
| 104 | + |
| 105 | + We're NOT consolidating these into a single handler. We're only fixing the `done/3` handler to return the required fields. |
| 106 | + |
| 107 | +3. **Adding automatic termination for completed cases**: The `done` state currently keeps the process alive (returns `{keep_state_and_data, ...}`). We're NOT changing this behavior to `{stop, normal, ...}` as that's a design decision outside the scope of this bug fix. |
| 108 | + |
| 109 | +4. **Fixing ETS table name collision risk**: The `trace_configuration_test` uses table name `wf_trace_events`. If multiple tests run concurrently, there could be ETS table name conflicts. This is a known issue but not causing test failures currently, so it's out of scope. |
| 110 | + |
| 111 | +5. **Implementing retry logic or explicit synchronization**: The 100ms sleep in tests is sufficient for the mock task. We're NOT adding complex synchronization primitives. |
| 112 | + |
| 113 | +## Implementation Approach |
| 114 | + |
| 115 | +The fix consists of two minimal changes to `src/wf_case_runner.erl`: |
| 116 | + |
| 117 | +**Phase 1: Fix `enter_state/2` to return gen_statem tuples** |
| 118 | +- Change the function to return `{next_state, TargetState, StateData}` for state transitions |
| 119 | +- Keep the function signature the same |
| 120 | +- This fixes the root cause of `running_to_done_completion_test` failure |
| 121 | + |
| 122 | +**Phase 2: Fix `done/3` status handler to include `ip` and `step_count`** |
| 123 | +- Extract fields from `ExecState` in `StateData` |
| 124 | +- Add `ip`, `step_count`, and `status` to the status map |
| 125 | +- Keep the `result` field for backward compatibility |
| 126 | +- This fixes the `status_query_test` failure |
| 127 | + |
| 128 | +The fixes are minimal, targeted, and follow existing patterns in the codebase. Phase 1 must be completed before Phase 2, but both phases can be tested independently. |
| 129 | + |
| 130 | +--- |
| 131 | + |
| 132 | +## Phases |
| 133 | + |
| 134 | +### Phase 1: Fix State Transition in `enter_state/2` |
| 135 | + |
| 136 | +#### Overview |
| 137 | + |
| 138 | +Fix the `enter_state/2` function to return proper gen_statem tuples instead of plain `StateData` records. This allows the gen_statem to transition from `running` to `done` state when execution completes. |
| 139 | + |
| 140 | +#### Changes Required: |
| 141 | + |
| 142 | +##### 1. Update `enter_state/2` function |
| 143 | + |
| 144 | +**File**: `src/wf_case_runner.erl:391-401` |
| 145 | + |
| 146 | +**Current Implementation:** |
| 147 | +```erlang |
| 148 | +%% @doc Transition to new state |
| 149 | +enter_state(TargetState, StateData) -> |
| 150 | + %% Set state timeout if not done/cancelled |
| 151 | + case TargetState of |
| 152 | + done -> |
| 153 | + StateData; |
| 154 | + cancelled -> |
| 155 | + StateData; |
| 156 | + _Other -> |
| 157 | + StateData |
| 158 | + end. |
| 159 | +``` |
| 160 | + |
| 161 | +**New Implementation:** |
| 162 | +```erlang |
| 163 | +%% @doc Transition to new state |
| 164 | +enter_state(done, StateData) -> |
| 165 | + {next_state, done, StateData}; |
| 166 | +enter_state(cancelled, StateData) -> |
| 167 | + {next_state, cancelled, StateData}; |
| 168 | +enter_state(_Other, StateData) -> |
| 169 | + {keep_state, StateData}. |
| 170 | +``` |
| 171 | + |
| 172 | +**Rationale:** |
| 173 | +- The function is called from `execute_quantum/1` when execution completes (lines 358, 365) |
| 174 | +- The return value is used as the gen_statem return value in state handlers |
| 175 | +- Must return `{next_state, TargetState, StateData}` for actual state transitions |
| 176 | +- Must return `{keep_state, StateData}` for staying in current state |
| 177 | +- Pattern matching on `TargetState` is clearer than case statement |
| 178 | + |
| 179 | +#### Success Criteria: |
| 180 | + |
| 181 | +##### Automated Verification: |
| 182 | + |
| 183 | +- [ ] Test passes: `rebar3 eunit --module=wf_case_runner_tests --test=running_to_done_completion_test` |
| 184 | +- [ ] Compilation succeeds: `rebar3 compile` |
| 185 | +- [ ] No dialyzer warnings: `rebar3 dialyzer` |
| 186 | + |
| 187 | +##### Manual Verification: |
| 188 | + |
| 189 | +- [ ] Inspect test output to confirm case reaches `done` state |
| 190 | +- [ ] Verify no other tests are broken by the change |
| 191 | +- [ ] Confirm state transition happens within 100ms timeout |
| 192 | + |
| 193 | +**Note**: After this phase, `running_to_done_completion_test` should pass, but `status_query_test` may still fail (that's Phase 2). |
| 194 | + |
| 195 | +--- |
| 196 | + |
| 197 | +### Phase 2: Fix Status Map Consistency in `done/3` |
| 198 | + |
| 199 | +#### Overview |
| 200 | + |
| 201 | +Update the `done/3` state handler to return a status map with the same structure as other state handlers, including `ip`, `step_count`, and `status` fields. |
| 202 | + |
| 203 | +#### Changes Required: |
| 204 | + |
| 205 | +##### 1. Update `done/3` status query handler |
| 206 | + |
| 207 | +**File**: `src/wf_case_runner.erl:295-298` |
| 208 | + |
| 209 | +**Current Implementation:** |
| 210 | +```erlang |
| 211 | +done({call, From}, status, StateData) -> |
| 212 | + %% Return done status with result |
| 213 | + StatusInfo = #{state => done, result => StateData#state_data.result}, |
| 214 | + {keep_state_and_data, [{reply, From, {ok, done, StatusInfo}}]}. |
| 215 | +``` |
| 216 | + |
| 217 | +**New Implementation:** |
| 218 | +```erlang |
| 219 | +done({call, From}, status, StateData) -> |
| 220 | + %% Return done status with result and execution info |
| 221 | + ExecState = StateData#state_data.exec_state, |
| 222 | + StatusInfo = #{ |
| 223 | + state => done, |
| 224 | + ip => ExecState#exec_state.ip, |
| 225 | + step_count => ExecState#exec_state.step_count, |
| 226 | + status => ExecState#exec_state.status, |
| 227 | + result => StateData#state_data.result |
| 228 | + }, |
| 229 | + {keep_state_and_data, [{reply, From, {ok, done, StatusInfo}}]}. |
| 230 | +``` |
| 231 | + |
| 232 | +**Rationale:** |
| 233 | +- Aligns with `running/3` handler (lines 205-214) which includes `ip`, `step_count`, `status` |
| 234 | +- Maintains backward compatibility by keeping `result` field |
| 235 | +- Test at line 86 checks `maps:is_key(ip, StatusInfo)` |
| 236 | +- Test at line 87 checks `maps:is_key(step_count, StatusInfo)` |
| 237 | +- Pattern matches the fallback `handle_common_event/3` structure (lines 308-316) |
| 238 | + |
| 239 | +#### Success Criteria: |
| 240 | + |
| 241 | +##### Automated Verification: |
| 242 | + |
| 243 | +- [ ] Test passes: `rebar3 eunit --module=wf_case_runner_tests --test=status_query_test` |
| 244 | +- [ ] Full test suite passes: `rebar3 eunit --module=wf_case_runner_tests` |
| 245 | +- [ ] All 5 tests in module pass: |
| 246 | + - `running_to_done_completion_test` |
| 247 | + - `signal_delivery_test` |
| 248 | + - `cancel_test` |
| 249 | + - `status_query_test` |
| 250 | + - `trace_configuration_test` |
| 251 | + |
| 252 | +##### Manual Verification: |
| 253 | + |
| 254 | +- [ ] Verify status query returns map with all expected keys: `state`, `ip`, `step_count`, `status`, `result` |
| 255 | +- [ ] Confirm no regressions in other tests |
| 256 | +- [ ] Check that `ip` value is correct (should be 2 after executing both opcodes) |
| 257 | +- [ ] Check that `step_count` value is correct (should be 2) |
| 258 | + |
| 259 | +**Note**: After this phase, both failing tests should pass and the full test suite should succeed. |
| 260 | + |
| 261 | +--- |
| 262 | + |
| 263 | +## Testing Strategy |
| 264 | + |
| 265 | +### Unit Tests: |
| 266 | + |
| 267 | +The existing EUnit tests in `test/wf_case_runner_tests.erl` provide coverage for the fixes: |
| 268 | + |
| 269 | +1. **`running_to_done_completion_test/1`**: Verifies state transition from `running` to `done` |
| 270 | + - Tests Phase 1 fix |
| 271 | + - Waits 100ms for completion |
| 272 | + - Queries status and expects `{ok, done, StatusInfo}` |
| 273 | + |
| 274 | +2. **`status_query_test/1`**: Verifies status map contains required keys |
| 275 | + - Tests Phase 2 fix |
| 276 | + - Queries status immediately (case may still be in `running` or `done`) |
| 277 | + - Asserts `ip` and `step_count` keys exist |
| 278 | + |
| 279 | +3. **`signal_delivery_test/1`**: Regression test for signal handling |
| 280 | + - Ensures Phase 1 changes don't break cast event handling |
| 281 | + |
| 282 | +4. **`cancel_test/1`**: Regression test for cancellation |
| 283 | + - Ensures Phase 1 changes don't break cancel flow |
| 284 | + - Tests both cancel from `running` and `enter_state(cancelled, ...)` path |
| 285 | + |
| 286 | +5. **`trace_configuration_test/1`**: Regression test for trace configuration |
| 287 | + - Ensures changes don't break trace state updates |
| 288 | + |
| 289 | +### Integration Tests: |
| 290 | + |
| 291 | +Run the full test suite to ensure no regressions: |
| 292 | + |
| 293 | +```bash |
| 294 | +rebar3 eunit |
| 295 | +``` |
| 296 | + |
| 297 | +This will execute all test modules and catch any unintended side effects. |
| 298 | + |
| 299 | +### Manual Testing Steps: |
| 300 | + |
| 301 | +1. **Verify state transition timing:** |
| 302 | + - Start a wf_case_runner with simple task bytecode |
| 303 | + - Query status immediately (should be `running`) |
| 304 | + - Query status after 100ms (should be `done`) |
| 305 | + - Confirm transition happens within timeout |
| 306 | + |
| 307 | +2. **Verify status map consistency:** |
| 308 | + - Query status in `running` state, check keys: `state`, `ip`, `step_count`, `status` |
| 309 | + - Query status in `done` state, check keys: `state`, `ip`, `step_count`, `status`, `result` |
| 310 | + - Query status in `cancelled` state, check keys: `state` |
| 311 | + - Verify `ip` increments correctly (0 → 1 → 2) |
| 312 | + - Verify `step_count` increments correctly (0 → 1 → 2) |
| 313 | + |
| 314 | +3. **Verify error handling:** |
| 315 | + - Start case with invalid bytecode (should fail gracefully) |
| 316 | + - Cancel case during execution (should reach `cancelled` state) |
| 317 | + - Timeout case (should transition to `cancelled` with timeout reason) |
| 318 | + |
| 319 | +## Migration Notes |
| 320 | + |
| 321 | +No data migration or API changes required. The fixes are internal implementation details that don't affect the external API: |
| 322 | + |
| 323 | +- `wf_case_runner:start_link/3` - unchanged |
| 324 | +- `wf_case_runner:signal/2` - unchanged |
| 325 | +- `wf_case_runner:cancel/1` - unchanged |
| 326 | +- `wf_case_runner:set_trace/2` - unchanged |
| 327 | +- `wf_case_runner:status/1` - unchanged (still returns `{ok, StateName, StatusInfo}`) |
| 328 | + |
| 329 | +The only visible change is that status maps now include additional fields (`ip`, `step_count`, `status`) in the `done` state, which is backward compatible (adding fields to a map doesn't break existing code). |
| 330 | + |
| 331 | +## References |
| 332 | + |
| 333 | +- Research: `/Users/speed/wf-substrate/.wreckit/items/031-fix-the-2-remaining-test-failures-in-testwfcaserun/research.md` |
| 334 | +- Test file: `/Users/speed/wf-substrate/test/wf_case_runner_tests.erl` |
| 335 | +- Implementation: `/Users/speed/wf-substrate/src/wf_case_runner.erl` |
| 336 | +- Related: Item 025 (fixed scheduler policy selection in `wf_sched:select_action/2`) |
| 337 | + |
| 338 | +### Key Code Locations: |
| 339 | + |
| 340 | +- **Bug 1**: `src/wf_case_runner.erl:391-401` - `enter_state/2` function |
| 341 | +- **Bug 2**: `src/wf_case_runner.erl:295-298` - `done/3` status handler |
| 342 | +- **Test 1**: `test/wf_case_runner_tests.erl:46-56` - `running_to_done_completion_test/1` |
| 343 | +- **Test 2**: `test/wf_case_runner_tests.erl:82-88` - `status_query_test/1` |
| 344 | +- **Caller**: `src/wf_case_runner.erl:340-389` - `execute_quantum/1` function |
| 345 | +- **Pattern**: `src/wf_case_runner.erl:205-214` - `running/3` status handler (correct pattern) |
0 commit comments