mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 23:18:24 +00:00
test(grasp06): assert relaxation-accepted PR event stays in purgatory
A PR event accepted under the GRASP-06 relaxation (un-announced coord, clone tag names this relay's /prs/ endpoint) must land in purgatory and not be broadcast until the matching refs/nostr/<id> push arrives. Spec: 06.md lines 21–24 in combination with GRASP-01 line 22. The existing acceptance test test_pr_event_accepted_when_clone_tag_names_prs_endpoint asserts the relay returned OK but discards the served-vs-purgatory bit returned by send_event_and_note_purgatory. A future regression that short-circuited the relaxation into WritePolicyResult::Accept would still pass that test while quietly leaking orphan PR events before their git data exists anywhere on this relay. The new test captures the in_purgatory bool and asserts it. Pattern mirrors GRASP-01's test_pr_event_accepted_into_purgatory_and_isnt_served. Shares the same SpecRef (Grasp06RelaxAcceptPrEvent) — the report renderer groups multiple tests per requirement, so this becomes a second check under the same spec line.
This commit is contained in:
@@ -135,6 +135,124 @@ impl EventAcceptanceTests {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Test: an orphan PR event accepted via the GRASP-06 relaxation MUST be
|
||||
/// placed in purgatory and MUST NOT be broadcast until matching git data
|
||||
/// arrives.
|
||||
///
|
||||
/// Spec: 06.md lines 21–24 say "MUST accept" — accept here means "accept
|
||||
/// as the standard PR pipeline would". GRASP-01 line 22 (the purgatory
|
||||
/// rule) then governs visibility: an accepted PR event with no matching
|
||||
/// git data is held in purgatory, not served, until either the matching
|
||||
/// `refs/nostr/<event-id>` push arrives or the 30-minute TTL expires.
|
||||
///
|
||||
/// ## Why this test
|
||||
///
|
||||
/// `test_pr_event_accepted_when_clone_tag_names_prs_endpoint` already
|
||||
/// pins the acceptance contract but discards the purgatory bit returned
|
||||
/// by `send_event_and_note_purgatory`. That leaves a gap: a future bug
|
||||
/// that accidentally short-circuits the relaxation into
|
||||
/// `WritePolicyResult::Accept` (immediate serve) would still pass the
|
||||
/// "accept" test but quietly violate the purgatory contract — leaking
|
||||
/// the orphan PR event before its commit object exists anywhere on
|
||||
/// this relay. This test closes that gap.
|
||||
///
|
||||
/// Mirrors the pattern of GRASP-01's
|
||||
/// `test_pr_event_accepted_into_purgatory_and_isnt_served`: send,
|
||||
/// short wait, query by id and assert empty.
|
||||
///
|
||||
/// ## TDD posture
|
||||
///
|
||||
/// Pre-implementation this FAILS by rejection (test 5's failure path).
|
||||
/// Once the relaxation lands correctly it turns green and stays green
|
||||
/// as long as the relaxation routes through purgatory rather than
|
||||
/// short-circuiting to accept.
|
||||
pub async fn test_pr_event_accepted_via_relaxation_is_held_in_purgatory(
|
||||
client: &AuditClient,
|
||||
) -> TestResult {
|
||||
TestResult::new(
|
||||
"pr_event_accepted_via_relaxation_is_held_in_purgatory",
|
||||
SpecRef::Grasp06RelaxAcceptPrEvent,
|
||||
"PR event accepted under the GRASP-06 relaxation MUST be held in purgatory \
|
||||
until matching git data arrives",
|
||||
)
|
||||
.run(|| async {
|
||||
// Setup mirrors test 5 exactly — same shape of orphan PR event
|
||||
// for the same reasons. The only behavioural difference is that
|
||||
// we additionally assert non-broadcast after acceptance.
|
||||
let ws_url = client
|
||||
.relay_url()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get relay URL: {}", e))?;
|
||||
let http_url = AuditClient::ws_to_http_url(&ws_url)
|
||||
.map_err(|e| format!("Failed to convert WebSocket URL to HTTP: {}", e))?;
|
||||
|
||||
let target_pubkey_hex = Keys::generate().public_key().to_hex();
|
||||
let identifier = format!("audit-grasp06-{}", uuid::Uuid::new_v4());
|
||||
let a_tag_value = format!("30617:{}:{}", target_pubkey_hex, identifier);
|
||||
|
||||
let pr_author_npub = client
|
||||
.pr_author_keys()
|
||||
.public_key()
|
||||
.to_bech32()
|
||||
.map_err(|e| format!("Failed to bech32-encode pr_author npub: {}", e))?;
|
||||
let clone_url = format!(
|
||||
"{}/prs/{}/{}.git",
|
||||
http_url.trim_end_matches('/'),
|
||||
pr_author_npub,
|
||||
identifier
|
||||
);
|
||||
|
||||
let commit_hex = Keys::generate().public_key().to_hex();
|
||||
|
||||
let event = client
|
||||
.event_builder(
|
||||
Kind::GitPullRequest,
|
||||
"grasp-06 audit: orphan PR must land in purgatory, not be served",
|
||||
)
|
||||
.tag(Tag::custom(TagKind::custom("a"), vec![a_tag_value]))
|
||||
.tag(Tag::custom(TagKind::custom("c"), vec![commit_hex]))
|
||||
.tag(Tag::custom(TagKind::custom("clone"), vec![clone_url]))
|
||||
.build(client.pr_author_keys())
|
||||
.map_err(|e| format!("Failed to build PR event: {}", e))?;
|
||||
let event_id = event.id;
|
||||
|
||||
// Acceptance is a precondition for this test, not the thing
|
||||
// being asserted — that's test 5's job. If the relay rejects
|
||||
// here, this test cannot make its assertion; surface the cause
|
||||
// rather than silently misreporting.
|
||||
let (_, in_purgatory) =
|
||||
client
|
||||
.send_event_and_note_purgatory(event)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"Relay rejected the PR event during test setup (test 5 \
|
||||
covers the acceptance contract; this test assumes it \
|
||||
passes). Relay error: {}",
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
// `send_event_and_note_purgatory` already does a 300ms wait and a
|
||||
// single `is_event_on_relay` probe; trust its result rather than
|
||||
// re-probing.
|
||||
if !in_purgatory {
|
||||
return Err(format!(
|
||||
"Orphan PR event {} accepted via GRASP-06 relaxation was \
|
||||
served immediately. The relaxation must route un-announced \
|
||||
PRs through purgatory (06.md lines 21–24 + GRASP-01 line 22), \
|
||||
not directly to accept. A common cause is a \
|
||||
`WritePolicyResult::Accept` short-circuit in the relaxation \
|
||||
branch.",
|
||||
event_id
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Test: a PR event for a coord this relay has no announcement for, with
|
||||
/// a `clone` tag pointing somewhere OTHER than this relay's `/prs/`
|
||||
/// endpoint, MUST remain rejected.
|
||||
|
||||
@@ -107,6 +107,12 @@ impl Grasp06Tests {
|
||||
)
|
||||
.await,
|
||||
);
|
||||
results.add(
|
||||
EventAcceptanceTests::test_pr_event_accepted_via_relaxation_is_held_in_purgatory(
|
||||
client,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
results.add(
|
||||
EventAcceptanceTests::test_pr_event_rejected_when_clone_tag_does_not_name_prs_endpoint(
|
||||
client,
|
||||
@@ -174,6 +180,15 @@ impl Grasp06Tests {
|
||||
)
|
||||
.skip(reason),
|
||||
);
|
||||
results.add(
|
||||
TestResult::new(
|
||||
"pr_event_accepted_via_relaxation_is_held_in_purgatory",
|
||||
SpecRef::Grasp06RelaxAcceptPrEvent,
|
||||
"PR event accepted under the GRASP-06 relaxation MUST be held in purgatory \
|
||||
until matching git data arrives",
|
||||
)
|
||||
.skip(reason),
|
||||
);
|
||||
results.add(
|
||||
TestResult::new(
|
||||
"pr_event_rejected_when_clone_tag_does_not_name_prs_endpoint",
|
||||
|
||||
@@ -211,6 +211,33 @@ isolated_test_with_grasp_06!(
|
||||
EventAcceptanceTests::test_pr_event_accepted_when_clone_tag_names_prs_endpoint
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
// Test 6b: Relaxation-accepted PR MUST stay in purgatory until git data arrives
|
||||
// =============================================================================
|
||||
//
|
||||
// Spec: GRASP-06 06.md lines 21–24, in combination with GRASP-01 line 22
|
||||
// (the purgatory rule).
|
||||
//
|
||||
// Contract: a PR event accepted under the GRASP-06 relaxation (un-announced
|
||||
// coord, clone tag names this relay's /prs/ endpoint) MUST be held in
|
||||
// purgatory and MUST NOT be broadcast until the matching `refs/nostr/<id>`
|
||||
// push arrives. The acceptance message must be `OK true "purgatory: ..."`,
|
||||
// not `OK true` for an immediately served event.
|
||||
//
|
||||
// Why a separate test from 6: test 6 only asserts the relay returned OK,
|
||||
// discarding the served-vs-purgatory bit. A future regression that
|
||||
// short-circuited the relaxation to `WritePolicyResult::Accept` would still
|
||||
// pass test 6 but quietly leak orphan PR events without their git data.
|
||||
// This test guards that boundary.
|
||||
//
|
||||
// Wired only as `with_grasp_06`. Once the relaxation is in place this is
|
||||
// green and stays green as long as the relaxation routes through purgatory.
|
||||
|
||||
isolated_test_with_grasp_06!(
|
||||
test_pr_event_accepted_via_relaxation_is_held_in_purgatory_with_grasp_06,
|
||||
EventAcceptanceTests::test_pr_event_accepted_via_relaxation_is_held_in_purgatory
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
// Test 7: Relaxation MUST NOT apply when clone tag does not name this relay
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user