diff options
Diffstat (limited to 'docs/archive/2025-11-05-grasp01-smoke-test-design.md')
| -rw-r--r-- | docs/archive/2025-11-05-grasp01-smoke-test-design.md | 503 |
1 files changed, 503 insertions, 0 deletions
diff --git a/docs/archive/2025-11-05-grasp01-smoke-test-design.md b/docs/archive/2025-11-05-grasp01-smoke-test-design.md new file mode 100644 index 0000000..ffff411 --- /dev/null +++ b/docs/archive/2025-11-05-grasp01-smoke-test-design.md | |||
| @@ -0,0 +1,503 @@ | |||
| 1 | # GRASP-01 Event Relationship Smoke Tests Design | ||
| 2 | |||
| 3 | **Version:** 1.0 | ||
| 4 | **Date:** 2025-11-05 | ||
| 5 | **Status:** Ready for Implementation | ||
| 6 | |||
| 7 | ## Overview | ||
| 8 | |||
| 9 | This document specifies a focused suite of **smoke tests** for GRASP-01 event reference validation (lines 7-9). These tests validate the basic acceptance/rejection behavior based on event tagging relationships, separate from the comprehensive test suite. | ||
| 10 | |||
| 11 | **Key Principle:** Events are accepted if they tag OR are tagged by accepted repositories. | ||
| 12 | |||
| 13 | --- | ||
| 14 | |||
| 15 | ## File Location | ||
| 16 | |||
| 17 | **Proposed Path:** `grasp-audit/src/specs/grasp01/event-acceptance-policy.rs` | ||
| 18 | |||
| 19 | **Rationale:** | ||
| 20 | - Separate from comprehensive suite | ||
| 21 | - Clear naming indicates purpose (smoke tests for event acceptance policy) | ||
| 22 | - Lives in `grasp01/` subdirectory for organization | ||
| 23 | - Can be run independently or as part of full suite | ||
| 24 | |||
| 25 | --- | ||
| 26 | |||
| 27 | ## Test Scenarios | ||
| 28 | |||
| 29 | ### Scenario Group 1: Accept Events Tagging Accepted Repositories | ||
| 30 | |||
| 31 | Events that reference an already-accepted repo should be accepted. | ||
| 32 | |||
| 33 | #### Test 1.1: `test_accept_issue_via_a_tag` | ||
| 34 | **Tags Issue → Repo via `a` tag** | ||
| 35 | |||
| 36 | ```rust | ||
| 37 | Setup: | ||
| 38 | 1. Create and send repo announcement (kind 30617) | ||
| 39 | 2. Create issue (kind 1621) with: | ||
| 40 | - ["a", "30617:{pubkey}:{repo-id}"] | ||
| 41 | 3. Send issue | ||
| 42 | |||
| 43 | Expected: Issue SHOULD be stored (query returns it) | ||
| 44 | ``` | ||
| 45 | |||
| 46 | --- | ||
| 47 | |||
| 48 | #### Test 1.2: `test_accept_comment_via_A_tag` | ||
| 49 | **Tags Comment → Repo via `A` tag (NIP-22 root)** | ||
| 50 | |||
| 51 | ```rust | ||
| 52 | Setup: | ||
| 53 | 1. Create and send repo announcement | ||
| 54 | 2. Create comment (kind 1111) with: | ||
| 55 | - ["A", "30617:{pubkey}:{repo-id}"] // Root | ||
| 56 | - ["K", "30617"] | ||
| 57 | - ["P", "{repo-pubkey}"] | ||
| 58 | 3. Send comment | ||
| 59 | |||
| 60 | Expected: Comment SHOULD be stored | ||
| 61 | ``` | ||
| 62 | |||
| 63 | --- | ||
| 64 | |||
| 65 | #### Test 1.3: `test_accept_kind1_via_q_tag` | ||
| 66 | **Tags Kind 1 → Repo via `q` tag (quote)** | ||
| 67 | |||
| 68 | ```rust | ||
| 69 | Setup: | ||
| 70 | 1. Create and send repo announcement | ||
| 71 | 2. Create kind 1 text note with: | ||
| 72 | - ["q", "30617:{pubkey}:{repo-id}"] | ||
| 73 | - content: "Check out this repo!" | ||
| 74 | 3. Send kind 1 | ||
| 75 | |||
| 76 | Expected: Kind 1 SHOULD be stored | ||
| 77 | ``` | ||
| 78 | |||
| 79 | --- | ||
| 80 | |||
| 81 | ### Scenario Group 2: Accept Events Tagging Accepted Events | ||
| 82 | |||
| 83 | Events that reference other accepted events should be accepted (transitive acceptance). | ||
| 84 | |||
| 85 | #### Test 2.1: `test_accept_issue_quoting_issue_via_q` | ||
| 86 | **Issue referencing unaccepted repo but quoting accepted issue** | ||
| 87 | |||
| 88 | ```rust | ||
| 89 | Setup: | ||
| 90 | 1. Create and send repo A announcement | ||
| 91 | 2. Create and send issue A (for repo A) | ||
| 92 | 3. Create repo B announcement (DO NOT send - not accepted) | ||
| 93 | 4. Create issue B (for repo B) with: | ||
| 94 | - ["a", "30617:{pubkey}:{repo-b-id}"] // References unaccepted repo B | ||
| 95 | - ["q", "{issue-a-id}"] // Quote accepted issue A | ||
| 96 | 5. Send issue B | ||
| 97 | |||
| 98 | Expected: Issue B SHOULD be stored (related via quote to accepted issue A, | ||
| 99 | even though its own repo reference is not accepted) | ||
| 100 | ``` | ||
| 101 | |||
| 102 | --- | ||
| 103 | |||
| 104 | #### Test 2.2: `test_accept_comment_via_E_tag` | ||
| 105 | **Comment on issue via `E` tag (NIP-22)** | ||
| 106 | |||
| 107 | ```rust | ||
| 108 | Setup: | ||
| 109 | 1. Create and send repo announcement | ||
| 110 | 2. Create and send issue (kind 1621) | ||
| 111 | 3. Create comment (kind 1111) with: | ||
| 112 | - ["E", "{issue-id}"] // Root | ||
| 113 | - ["K", "1621"] | ||
| 114 | - ["P", "{issue-author}"] | ||
| 115 | - ["e", "{issue-id}"] // Parent (same as root for top-level) | ||
| 116 | - ["k", "1621"] | ||
| 117 | - ["p", "{issue-author}"] | ||
| 118 | 4. Send comment | ||
| 119 | |||
| 120 | Expected: Comment SHOULD be stored (related to accepted issue) | ||
| 121 | ``` | ||
| 122 | |||
| 123 | --- | ||
| 124 | |||
| 125 | #### Test 2.3: `test_accept_kind1_via_e_tag` | ||
| 126 | **Kind 1 referencing another kind 1 via `e` tag** | ||
| 127 | |||
| 128 | ```rust | ||
| 129 | Setup: | ||
| 130 | 1. Create and send repo announcement | ||
| 131 | 2. Create kind 1 note A with ["q", "30617:{pubkey}:{repo-id}"] | ||
| 132 | 3. Send kind 1 A | ||
| 133 | 4. Create kind 1 note B with: | ||
| 134 | - ["e", "{kind1-a-id}", "", "reply"] | ||
| 135 | - content: "Great point!" | ||
| 136 | 5. Send kind 1 B | ||
| 137 | |||
| 138 | Expected: Kind 1 B SHOULD be stored (related via e tag to accepted kind 1 A) | ||
| 139 | ``` | ||
| 140 | |||
| 141 | --- | ||
| 142 | |||
| 143 | ### Scenario Group 3: Accept Events Tagged by Accepted Events | ||
| 144 | |||
| 145 | Events that are referenced BY accepted events should be accepted (forward references). | ||
| 146 | |||
| 147 | #### Test 3.1: `test_accept_kind1_referenced_in_issue` | ||
| 148 | **Kind 1 referenced in issue via `q` tag** | ||
| 149 | |||
| 150 | ```rust | ||
| 151 | Setup: | ||
| 152 | 1. Create kind 1 note (NOT sent yet) | ||
| 153 | 2. Create and send repo announcement | ||
| 154 | 3. Create issue with: | ||
| 155 | - ["a", "30617:{pubkey}:{repo-id}"] | ||
| 156 | - ["q", "{kind1-id}"] // Reference the not-yet-sent kind 1 | ||
| 157 | 4. Send issue | ||
| 158 | 5. Send kind 1 note | ||
| 159 | |||
| 160 | Expected: Kind 1 SHOULD be stored (referenced by accepted issue) | ||
| 161 | ``` | ||
| 162 | |||
| 163 | --- | ||
| 164 | |||
| 165 | #### Test 3.2: `test_accept_comment_referenced_in_comment` | ||
| 166 | **Comment referenced in another comment via `q` tag** | ||
| 167 | |||
| 168 | ```rust | ||
| 169 | Setup: | ||
| 170 | 1. Create and send repo announcement | ||
| 171 | 2. Create and send issue | ||
| 172 | 3. Create comment A (NOT sent yet) | ||
| 173 | 4. Create comment B with: | ||
| 174 | - ["E", "{issue-id}"] // Root | ||
| 175 | - ["e", "{issue-id}"] // Parent | ||
| 176 | - ["q", "{comment-a-id}"] // Quote comment A | ||
| 177 | 5. Send comment B | ||
| 178 | 6. Send comment A | ||
| 179 | |||
| 180 | Expected: Comment A SHOULD be stored (referenced by accepted comment B) | ||
| 181 | ``` | ||
| 182 | |||
| 183 | --- | ||
| 184 | |||
| 185 | #### Test 3.3: `test_accept_kind1_referenced_in_kind1` | ||
| 186 | **Kind 1 referenced in accepted kind 1 via `e` tag** | ||
| 187 | |||
| 188 | ```rust | ||
| 189 | Setup: | ||
| 190 | 1. Create and send repo announcement | ||
| 191 | 2. Create kind 1 A (NOT sent yet) | ||
| 192 | 3. Create kind 1 B with: | ||
| 193 | - ["q", "30617:{pubkey}:{repo-id}"] | ||
| 194 | - ["e", "{kind1-a-id}", "", "mention"] | ||
| 195 | 4. Send kind 1 B | ||
| 196 | 5. Send kind 1 A | ||
| 197 | |||
| 198 | Expected: Kind 1 A SHOULD be stored (referenced by accepted kind 1 B) | ||
| 199 | ``` | ||
| 200 | |||
| 201 | --- | ||
| 202 | |||
| 203 | ### Scenario Group 4: Reject Unrelated Events | ||
| 204 | |||
| 205 | Events with no relationship to accepted repositories should be rejected. | ||
| 206 | |||
| 207 | #### Test 4.1: `test_reject_orphan_issue` | ||
| 208 | **Issue from unrelated repository** | ||
| 209 | |||
| 210 | ```rust | ||
| 211 | Setup: | ||
| 212 | 1. Create issue (kind 1621) with: | ||
| 213 | - ["a", "30617:{other-pubkey}:{other-repo-id}"] // Different repo | ||
| 214 | 2. Send issue | ||
| 215 | |||
| 216 | Expected: Issue SHOULD NOT be stored (no accepted repo) | ||
| 217 | ``` | ||
| 218 | |||
| 219 | --- | ||
| 220 | |||
| 221 | #### Test 4.2: `test_reject_orphan_kind1` | ||
| 222 | **Kind 1 from unrelated context** | ||
| 223 | |||
| 224 | ```rust | ||
| 225 | Setup: | ||
| 226 | 1. Create kind 1 note with generic content (no tags) | ||
| 227 | 2. Send kind 1 | ||
| 228 | |||
| 229 | Expected: Kind 1 SHOULD NOT be stored (no relationship to any repo) | ||
| 230 | ``` | ||
| 231 | |||
| 232 | --- | ||
| 233 | |||
| 234 | #### Test 4.3: `test_reject_comment_quoting_other_repo` | ||
| 235 | **Comment quoting announcement from different repository** | ||
| 236 | |||
| 237 | ```rust | ||
| 238 | Setup: | ||
| 239 | 1. Create repo A announcement (sent) | ||
| 240 | 2. Create repo B announcement (NOT sent - different owner) | ||
| 241 | 3. Create comment with: | ||
| 242 | - ["A", "30617:{other-pubkey}:{repo-b-id}"] // Root | ||
| 243 | - ["q", "30617:{other-pubkey}:{repo-b-id}"] // Quote unaccepted repo | ||
| 244 | 4. Send comment | ||
| 245 | |||
| 246 | Expected: Comment SHOULD NOT be stored (references unaccepted repo) | ||
| 247 | ``` | ||
| 248 | |||
| 249 | --- | ||
| 250 | |||
| 251 | ## Helper Functions | ||
| 252 | |||
| 253 | Keep helpers minimal and focused on smoke test needs. | ||
| 254 | |||
| 255 | **Implementation Note:** Reference [`nostr-sdk`](https://docs.rs/nostr-sdk) (rust-nostr) for event generation patterns. The SDK provides robust helpers for creating events with proper signatures and tags. Use these patterns rather than building everything from scratch. | ||
| 256 | |||
| 257 | ### `create_test_repo(client, repo_id) -> Event` | ||
| 258 | Creates a basic repo announcement with required tags. | ||
| 259 | |||
| 260 | ```rust | ||
| 261 | async fn create_test_repo(client: &AuditClient, repo_id: &str) -> Result<Event> { | ||
| 262 | client.create_repo_announcement(repo_id).await | ||
| 263 | } | ||
| 264 | ``` | ||
| 265 | |||
| 266 | --- | ||
| 267 | |||
| 268 | ### `create_issue_for_repo(client, repo_event, subject) -> Event` | ||
| 269 | Creates issue referencing repo via `a` tag. | ||
| 270 | |||
| 271 | ```rust | ||
| 272 | async fn create_issue_for_repo( | ||
| 273 | client: &AuditClient, | ||
| 274 | repo_event: &Event, | ||
| 275 | subject: &str, | ||
| 276 | ) -> Result<Event> { | ||
| 277 | let repo_id = extract_d_tag(repo_event)?; | ||
| 278 | let a_tag = Tag::parse(&["a", &format!("30617:{}:{}", repo_event.pubkey, repo_id)])?; | ||
| 279 | |||
| 280 | client.event_builder() | ||
| 281 | .kind(Kind::Custom(1621)) | ||
| 282 | .content(format!("Issue: {}", subject)) | ||
| 283 | .tag(a_tag) | ||
| 284 | .build() | ||
| 285 | .await | ||
| 286 | } | ||
| 287 | ``` | ||
| 288 | |||
| 289 | --- | ||
| 290 | |||
| 291 | ### `create_comment_for_event(client, root_event, content) -> Event` | ||
| 292 | Creates NIP-22 comment for an event. | ||
| 293 | |||
| 294 | ```rust | ||
| 295 | async fn create_comment_for_event( | ||
| 296 | client: &AuditClient, | ||
| 297 | root_event: &Event, | ||
| 298 | content: &str, | ||
| 299 | ) -> Result<Event> { | ||
| 300 | client.event_builder() | ||
| 301 | .kind(Kind::Custom(1111)) | ||
| 302 | .content(content) | ||
| 303 | .tag(Tag::parse(&["E", &root_event.id.to_string()])?) | ||
| 304 | .tag(Tag::parse(&["K", &root_event.kind.to_string()])?) | ||
| 305 | .tag(Tag::parse(&["P", &root_event.pubkey.to_string()])?) | ||
| 306 | .tag(Tag::parse(&["e", &root_event.id.to_string()])?) | ||
| 307 | .tag(Tag::parse(&["k", &root_event.kind.to_string()])?) | ||
| 308 | .tag(Tag::parse(&["p", &root_event.pubkey.to_string()])?) | ||
| 309 | .build() | ||
| 310 | .await | ||
| 311 | } | ||
| 312 | ``` | ||
| 313 | |||
| 314 | --- | ||
| 315 | |||
| 316 | ### `send_and_verify_accepted(client, event) -> Result<()>` | ||
| 317 | Sends event and verifies it was stored. | ||
| 318 | |||
| 319 | ```rust | ||
| 320 | async fn send_and_verify_accepted(client: &AuditClient, event: Event) -> Result<()> { | ||
| 321 | let event_id = client.send_event(event.clone()).await?; | ||
| 322 | |||
| 323 | // Small delay for propagation | ||
| 324 | tokio::time::sleep(Duration::from_millis(100)).await; | ||
| 325 | |||
| 326 | let filter = Filter::new() | ||
| 327 | .id(event_id) | ||
| 328 | .limit(1); | ||
| 329 | |||
| 330 | let results = client.query(filter).await?; | ||
| 331 | |||
| 332 | if results.is_empty() { | ||
| 333 | return Err("Event was not stored".into()); | ||
| 334 | } | ||
| 335 | |||
| 336 | Ok(()) | ||
| 337 | } | ||
| 338 | ``` | ||
| 339 | |||
| 340 | --- | ||
| 341 | |||
| 342 | ### `send_and_verify_rejected(client, event) -> Result<()>` | ||
| 343 | Sends event and verifies it was NOT stored. | ||
| 344 | |||
| 345 | ```rust | ||
| 346 | async fn send_and_verify_rejected(client: &AuditClient, event: Event) -> Result<()> { | ||
| 347 | let event_id = event.id; | ||
| 348 | |||
| 349 | // Attempt to send | ||
| 350 | let _ = client.send_event(event).await; | ||
| 351 | |||
| 352 | // Small delay for propagation | ||
| 353 | tokio::time::sleep(Duration::from_millis(100)).await; | ||
| 354 | |||
| 355 | let filter = Filter::new() | ||
| 356 | .id(event_id) | ||
| 357 | .limit(1); | ||
| 358 | |||
| 359 | let results = client.query(filter).await?; | ||
| 360 | |||
| 361 | if !results.is_empty() { | ||
| 362 | return Err("Event was stored but should have been rejected".into()); | ||
| 363 | } | ||
| 364 | |||
| 365 | Ok(()) | ||
| 366 | } | ||
| 367 | ``` | ||
| 368 | |||
| 369 | --- | ||
| 370 | |||
| 371 | ### `extract_d_tag(event) -> Result<String>` | ||
| 372 | Extracts `d` tag value from event. | ||
| 373 | |||
| 374 | ```rust | ||
| 375 | fn extract_d_tag(event: &Event) -> Result<String> { | ||
| 376 | event.tags | ||
| 377 | .iter() | ||
| 378 | .find(|t| t.kind() == TagKind::d()) | ||
| 379 | .and_then(|t| t.content()) | ||
| 380 | .ok_or("Missing d tag")? | ||
| 381 | .to_string() | ||
| 382 | } | ||
| 383 | ``` | ||
| 384 | |||
| 385 | --- | ||
| 386 | |||
| 387 | ## Module Structure | ||
| 388 | |||
| 389 | ```rust | ||
| 390 | //! GRASP-01 Event Relationship Smoke Tests | ||
| 391 | //! | ||
| 392 | //! Focused smoke tests validating basic event acceptance/rejection | ||
| 393 | //! based on tagging relationships with accepted repositories. | ||
| 394 | |||
| 395 | use crate::{AuditClient, AuditResult, TestResult}; | ||
| 396 | use nostr_sdk::prelude::*; | ||
| 397 | use std::time::Duration; | ||
| 398 | |||
| 399 | pub struct EventAcceptancePolicyTests; | ||
| 400 | |||
| 401 | impl EventAcceptancePolicyTests { | ||
| 402 | pub async fn run_all(client: &AuditClient) -> AuditResult { | ||
| 403 | let mut results = AuditResult::new("GRASP-01 Event Acceptance Policy Tests"); | ||
| 404 | |||
| 405 | // Group 1: Events tagging repos | ||
| 406 | results.add(Self::test_accept_issue_via_a_tag(client).await); | ||
| 407 | results.add(Self::test_accept_comment_via_A_tag(client).await); | ||
| 408 | results.add(Self::test_accept_kind1_via_q_tag(client).await); | ||
| 409 | |||
| 410 | // Group 2: Events tagging accepted events | ||
| 411 | results.add(Self::test_accept_issue_quoting_issue_via_q(client).await); | ||
| 412 | results.add(Self::test_accept_comment_via_E_tag(client).await); | ||
| 413 | results.add(Self::test_accept_kind1_via_e_tag(client).await); | ||
| 414 | |||
| 415 | // Group 3: Events tagged by accepted events | ||
| 416 | results.add(Self::test_accept_kind1_referenced_in_issue(client).await); | ||
| 417 | results.add(Self::test_accept_comment_referenced_in_comment(client).await); | ||
| 418 | results.add(Self::test_accept_kind1_referenced_in_kind1(client).await); | ||
| 419 | |||
| 420 | // Group 4: Reject unrelated events | ||
| 421 | results.add(Self::test_reject_orphan_issue(client).await); | ||
| 422 | results.add(Self::test_reject_orphan_kind1(client).await); | ||
| 423 | results.add(Self::test_reject_comment_quoting_other_repo(client).await); | ||
| 424 | |||
| 425 | results | ||
| 426 | } | ||
| 427 | |||
| 428 | // Test implementations follow... | ||
| 429 | } | ||
| 430 | |||
| 431 | // Helper functions follow... | ||
| 432 | ``` | ||
| 433 | |||
| 434 | --- | ||
| 435 | |||
| 436 | ## Integration with Test Suite | ||
| 437 | |||
| 438 | Add to `grasp-audit/src/specs/grasp01/mod.rs`: | ||
| 439 | |||
| 440 | ```rust | ||
| 441 | pub mod event_acceptance_policy; | ||
| 442 | |||
| 443 | pub use event_acceptance_policy::EventAcceptancePolicyTests; | ||
| 444 | ``` | ||
| 445 | |||
| 446 | Add to main test runner if desired, or run independently: | ||
| 447 | |||
| 448 | ```rust | ||
| 449 | // In grasp01_nostr_relay.rs or separate test file | ||
| 450 | #[tokio::test] | ||
| 451 | #[ignore] | ||
| 452 | async fn test_event_acceptance_policy_suite() { | ||
| 453 | let client = AuditClient::new_for_relay(&relay_url()).await.unwrap(); | ||
| 454 | let results = EventAcceptancePolicyTests::run_all(&client).await; | ||
| 455 | |||
| 456 | // Assert all tests passed | ||
| 457 | assert!(results.all_passed(), "Some tests failed:\n{}", results); | ||
| 458 | } | ||
| 459 | ``` | ||
| 460 | |||
| 461 | --- | ||
| 462 | |||
| 463 | ## Implementation Notes | ||
| 464 | |||
| 465 | 1. **Simplicity First:** Keep test logic straightforward - setup, send, verify | ||
| 466 | 2. **Independent Tests:** Each test should be runnable standalone | ||
| 467 | 3. **Clear Failures:** Use descriptive error messages for debugging | ||
| 468 | 4. **Minimal Helpers:** Only create helpers that reduce significant duplication | ||
| 469 | 5. **Fast Execution:** Smoke tests should run quickly (use minimal delays) | ||
| 470 | |||
| 471 | --- | ||
| 472 | |||
| 473 | ## Expected Outcomes | ||
| 474 | |||
| 475 | When implemented, this suite should: | ||
| 476 | |||
| 477 | - ✅ Run in under 5 seconds total | ||
| 478 | - ✅ Clearly show which relationship types work/fail | ||
| 479 | - ✅ Provide quick validation during development | ||
| 480 | - ✅ Act as regression tests for basic GRASP-01 compliance | ||
| 481 | - ✅ Be easy to understand and modify | ||
| 482 | |||
| 483 | --- | ||
| 484 | |||
| 485 | ## Next Steps | ||
| 486 | |||
| 487 | 1. Create `grasp-audit/src/specs/grasp01/event-acceptance-policy.rs` | ||
| 488 | 2. Implement helper functions (referencing nostr-sdk patterns) | ||
| 489 | 3. Implement each test function following the specifications above | ||
| 490 | 4. Add module declaration to `grasp01/mod.rs` | ||
| 491 | 5. Run tests: `cd grasp-audit && nix develop -c bash test-ngit-relay.sh --mode test` | ||
| 492 | 6. Verify all tests pass or show expected "Not implemented yet" status | ||
| 493 | |||
| 494 | --- | ||
| 495 | |||
| 496 | ## Success Criteria | ||
| 497 | |||
| 498 | - [ ] All 12 tests compile without errors | ||
| 499 | - [ ] Tests run independently and as a suite | ||
| 500 | - [ ] Accept tests verify events ARE stored | ||
| 501 | - [ ] Reject tests verify events are NOT stored | ||
| 502 | - [ ] Helper functions eliminate code duplication | ||
| 503 | - [ ] Test output clearly indicates pass/fail/not-implemented \ No newline at end of file | ||