tidaldb/.sdlc/features/m10-signal-revocation-controls/qa-plan.md

90 lines
3.4 KiB
Markdown

# 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)