diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-23 15:41:32 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-23 15:41:32 +0000 |
| commit | c54ce061d6d278cce8362d5af085808ca60c239b (patch) | |
| tree | ec967d6195d9f7ec4f061449596611afe3a0950f /grasp-audit/src/result.rs | |
| parent | e0ad39a489b3398f8208713bf728db0cb11475b0 (diff) | |
| parent | 113928aa84894ea8f65c247d9987527e792b32a9 (diff) | |
feat: announcement purgatory
Extends purgatory to hold repository announcements until git data arrives,
preventing empty repositories from being served to clients.
When an announcement is received, a bare repo is created immediately and the
announcement is held in purgatory. It is only promoted and served once a git
push confirms real content exists. If no push arrives before expiry, the bare
repo is deleted and the announcement is silently discarded.
Key behaviours:
- Soft expiry: announcements are hidden from clients but kept alive while git
pushes are in progress, reviving on successful push
- Expiry is extended when a matching state event or git push is observed
- NIP-09 deletion events remove announcements from purgatory
- Purgatory state (announcements, state events, PR events, expired set) is
persisted to disk on graceful shutdown and restored on startup, with elapsed
downtime subtracted from expiry deadlines
- Purgatory announcements drive StateOnly sync in the sync system so state
events are fetched from listed relays before promotion
- SyncLevel added to RepoSyncIndex to distinguish purgatory repos (StateOnly)
from promoted repos (Full L2+L3 sync)
Diffstat (limited to 'grasp-audit/src/result.rs')
| -rw-r--r-- | grasp-audit/src/result.rs | 43 |
1 files changed, 32 insertions, 11 deletions
diff --git a/grasp-audit/src/result.rs b/grasp-audit/src/result.rs index ae3ef26..0c3ec08 100644 --- a/grasp-audit/src/result.rs +++ b/grasp-audit/src/result.rs | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | //! Test result types | 1 | //! Test result types |
| 2 | 2 | ||
| 3 | use crate::specs::grasp01::{get_sections, GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID}; | 3 | use crate::specs::grasp01::{get_sections, SpecRef, GRASP_01_REQUIREMENTS, GRASP_COMMIT_ID}; |
| 4 | use std::collections::BTreeMap; | 4 | use std::collections::BTreeMap; |
| 5 | use std::time::{Duration, Instant}; | 5 | use std::time::{Duration, Instant}; |
| 6 | 6 | ||
| @@ -68,10 +68,16 @@ pub struct TestResult { | |||
| 68 | 68 | ||
| 69 | impl TestResult { | 69 | impl TestResult { |
| 70 | /// Create a new test result | 70 | /// Create a new test result |
| 71 | pub fn new(name: &str, spec_ref: &str, requirement: &str) -> Self { | 71 | /// |
| 72 | /// # Arguments | ||
| 73 | /// * `name` - Test name identifier | ||
| 74 | /// * `spec_ref` - Reference to the spec requirement being tested | ||
| 75 | /// * `requirement` - Human-readable description of what this test validates | ||
| 76 | /// (can be more specific than the general spec text) | ||
| 77 | pub fn new(name: &str, spec_ref: SpecRef, requirement: &str) -> Self { | ||
| 72 | TestResult { | 78 | TestResult { |
| 73 | name: name.to_string(), | 79 | name: name.to_string(), |
| 74 | spec_ref: spec_ref.to_string(), | 80 | spec_ref: spec_ref.spec_ref_string().to_string(), |
| 75 | requirement: requirement.to_string(), | 81 | requirement: requirement.to_string(), |
| 76 | passed: false, | 82 | passed: false, |
| 77 | error: None, | 83 | error: None, |
| @@ -293,9 +299,13 @@ mod tests { | |||
| 293 | 299 | ||
| 294 | #[tokio::test] | 300 | #[tokio::test] |
| 295 | async fn test_result_pass() { | 301 | async fn test_result_pass() { |
| 296 | let result = TestResult::new("test", "SPEC:1", "Must work") | 302 | let result = TestResult::new( |
| 297 | .run(|| async { Ok(()) }) | 303 | "test", |
| 298 | .await; | 304 | SpecRef::NostrRelayNip01Compliant, |
| 305 | "Test requirement", | ||
| 306 | ) | ||
| 307 | .run(|| async { Ok(()) }) | ||
| 308 | .await; | ||
| 299 | 309 | ||
| 300 | assert!(result.passed); | 310 | assert!(result.passed); |
| 301 | assert!(result.error.is_none()); | 311 | assert!(result.error.is_none()); |
| @@ -303,9 +313,13 @@ mod tests { | |||
| 303 | 313 | ||
| 304 | #[tokio::test] | 314 | #[tokio::test] |
| 305 | async fn test_result_fail() { | 315 | async fn test_result_fail() { |
| 306 | let result = TestResult::new("test", "SPEC:1", "Must work") | 316 | let result = TestResult::new( |
| 307 | .run(|| async { Err("Failed".to_string()) }) | 317 | "test", |
| 308 | .await; | 318 | SpecRef::NostrRelayNip01Compliant, |
| 319 | "Test requirement", | ||
| 320 | ) | ||
| 321 | .run(|| async { Err("Failed".to_string()) }) | ||
| 322 | .await; | ||
| 309 | 323 | ||
| 310 | assert!(!result.passed); | 324 | assert!(!result.passed); |
| 311 | assert_eq!(result.error, Some("Failed".to_string())); | 325 | assert_eq!(result.error, Some("Failed".to_string())); |
| @@ -315,8 +329,15 @@ mod tests { | |||
| 315 | fn test_audit_result() { | 329 | fn test_audit_result() { |
| 316 | let mut audit = AuditResult::new("Test Spec"); | 330 | let mut audit = AuditResult::new("Test Spec"); |
| 317 | 331 | ||
| 318 | audit.add(TestResult::new("test1", "SPEC:1", "Req1").pass()); | 332 | audit.add(TestResult::new("test1", SpecRef::NostrRelayNip01Compliant, "Test 1").pass()); |
| 319 | audit.add(TestResult::new("test2", "SPEC:2", "Req2").fail("Error")); | 333 | audit.add( |
| 334 | TestResult::new( | ||
| 335 | "test2", | ||
| 336 | SpecRef::NostrRelayRejectMissingCloneRelays, | ||
| 337 | "Test 2", | ||
| 338 | ) | ||
| 339 | .fail("Error"), | ||
| 340 | ); | ||
| 320 | 341 | ||
| 321 | assert_eq!(audit.total_count(), 2); | 342 | assert_eq!(audit.total_count(), 2); |
| 322 | assert_eq!(audit.passed_count(), 1); | 343 | assert_eq!(audit.passed_count(), 1); |