# QA Plan: Signal Revocation Controls ## Scope Unit tests in `entities/revocation.rs`, integration tests in `tidal/tests/m10_revocation.rs`, and property tests via proptest. --- ## 1. Unit Tests (`--lib`) ### 1.1 `entities/revocation.rs` | ID | Scenario | Pass Condition | |----|----------|----------------| | U1 | SignalType scope, no bounds | Matches correct type; rejects other types | | U2 | SignalType scope, since bound | Rejects signals before since_ns | | U3 | SignalType scope, until bound | Rejects signals after until_ns | | U4 | SignalType scope, both bounds | Window boundary inclusive on both sides | | U5 | TimeRange scope | Inclusive on both ends; any signal type | | U6 | AgentSession scope | Matches by session_id_raw; rejects None or wrong id | | U7 | is_suppressed: no revocations | Returns false (fast path) | | U8 | is_suppressed: matching active | Returns true | | U9 | is_suppressed: inactive revocation | Returns false | | U10 | cancel: unknown id | Returns false | | U11 | cancel: deactivates | is_suppressed returns false after cancel | | U12 | list: includes cancelled | Vec contains both active and cancelled | | U13 | overlapping revocations | Union semantics — either matching returns true | | U14 | upsert replaces existing by id | Vec length stays 1 after replacing | | U15 | RevocationId bytes roundtrip | from_bytes(as_bytes()) == original | ### 1.2 `signals/ledger/core.rs` | ID | Scenario | Pass Condition | |----|----------|----------------| | U16 | entry_last_update_ns: no entry | Returns 0 | | U17 | entry_last_update_ns: after signal | Returns correct timestamp | --- ## 2. Integration Tests (`tidal/tests/m10_revocation.rs`) | ID | Test name | Scenario | |----|-----------|----------| | I1 | basic_signal_type_revocation | Revoke "view"; decay score suppressed; "like" unaffected | | I2 | time_range_revocation | Signals in range suppressed; outside unaffected | | I3 | agent_session_revocation | Session signals suppressed; non-session unaffected | | I4 | cancel_revocation_lifts_suppression | Revoke; verify suppressed; cancel; verify restored | | I5 | revocation_survives_restart | Revoke; close; reopen; verify still suppressed | | I6 | overlapping_revocations | Two overlapping revocations both apply | | I7 | revoke_unknown_signal_type | Returns TidalError::Schema | | I8 | revoke_invalid_time_range | Returns TidalError::InvalidInput | | I9 | cancel_nonexistent_revocation | Returns false (not an error) | | I10 | preference_vector_rebuild_after_revocation | Pref vector excludes revoked items | --- ## 3. Property Tests (proptest) | ID | Property | |----|----------| | P1 | No revocations → is_suppressed always false | | P2 | scope.matches deterministic (same inputs = same output) | | P3 | Union semantics: if any revocation matches, is_suppressed is true | | P4 | RevocationId bytes roundtrip | --- ## 4. Static Analysis - `cargo clippy --manifest-path tidal/Cargo.toml -- -D warnings` — zero warnings - `cargo fmt --manifest-path tidal/Cargo.toml --check` — clean --- ## 5. Performance Sanity - `is_suppressed` with 0 revocations: single DashMap shard miss, ~10ns - `is_suppressed` with 5 active revocations: linear scan of 5 items, ~50ns - No measurable overhead on ranking hot path for users with 0 revocations --- ## Pass Criteria - All lib tests pass (1299+ tests, zero failures) - All 10 m10_revocation integration tests pass - clippy -D warnings: zero warnings - Revocations survive restart (I5 passes)