Skip to content

Commit d8917e3

Browse files
ScaleLeanChrisdm-builder
authored andcommitted
fix(jobs): admit channel-scoped job events
Signed-off-by: ScaleLeanChris <chris@scalelean.com> Signed-off-by: Brad Groux <bradgroux@hotmail.com> Signed-off-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
1 parent 95a3d41 commit d8917e3

2 files changed

Lines changed: 121 additions & 9 deletions

File tree

crates/buzz-relay/src/handlers/ingest.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,21 @@ use buzz_core::kind::{
2121
KIND_GIT_STATUS_CLOSED, KIND_GIT_STATUS_DRAFT, KIND_GIT_STATUS_MERGED, KIND_GIT_STATUS_OPEN,
2222
KIND_HUDDLE_ENDED, KIND_HUDDLE_GUIDELINES, KIND_HUDDLE_PARTICIPANT_JOINED,
2323
KIND_HUDDLE_PARTICIPANT_LEFT, KIND_HUDDLE_STARTED, KIND_IA_ARCHIVE_REQUEST,
24-
KIND_IA_UNARCHIVE_REQUEST, KIND_LONG_FORM, KIND_MANAGED_AGENT,
24+
KIND_IA_UNARCHIVE_REQUEST, KIND_JOB_ACCEPTED, KIND_JOB_CANCEL, KIND_JOB_ERROR,
25+
KIND_JOB_PROGRESS, KIND_JOB_REQUEST, KIND_JOB_RESULT, KIND_LONG_FORM, KIND_MANAGED_AGENT,
2526
KIND_MEMBER_ADDED_NOTIFICATION, KIND_MEMBER_REMOVED_NOTIFICATION, KIND_MODERATION_BAN,
2627
KIND_MODERATION_RESOLVE_REPORT, KIND_MODERATION_TIMEOUT, KIND_MODERATION_UNBAN,
2728
KIND_MODERATION_UNTIMEOUT, KIND_MUTE_LIST, KIND_NIP29_CREATE_GROUP, KIND_NIP29_DELETE_EVENT,
2829
KIND_NIP29_DELETE_GROUP, KIND_NIP29_EDIT_METADATA, KIND_NIP29_JOIN_REQUEST,
2930
KIND_NIP29_LEAVE_REQUEST, KIND_NIP29_PUT_USER, KIND_NIP29_REMOVE_USER,
3031
KIND_NIP43_LEAVE_REQUEST, KIND_NIP65_RELAY_LIST_METADATA, KIND_PERSONA, KIND_PIN_LIST,
3132
KIND_PRESENCE_UPDATE, KIND_PRIVATE_MANAGED_AGENT, KIND_PRODUCT_FEEDBACK, KIND_PROFILE,
32-
KIND_PROJECT, KIND_REACTION,
33-
KIND_READ_STATE, KIND_REPORT, KIND_STREAM_MESSAGE, KIND_STREAM_MESSAGE_BOOKMARKED,
34-
KIND_STREAM_MESSAGE_DIFF, KIND_STREAM_MESSAGE_EDIT, KIND_STREAM_MESSAGE_PINNED,
35-
KIND_STREAM_MESSAGE_SCHEDULED, KIND_STREAM_MESSAGE_V2, KIND_STREAM_REMINDER, KIND_TEAM,
36-
KIND_TEAM_CATALOG, KIND_TEXT_NOTE, KIND_USER_STATUS, KIND_WORKFLOW_DEF, KIND_WORKFLOW_TRIGGER,
37-
RELAY_ADMIN_ADD_MEMBER, RELAY_ADMIN_CHANGE_ROLE, RELAY_ADMIN_REMOVE_MEMBER,
38-
RELAY_ADMIN_SET_WORKSPACE_PROFILE,
33+
KIND_PROJECT, KIND_REACTION, KIND_READ_STATE, KIND_REPORT, KIND_STREAM_MESSAGE,
34+
KIND_STREAM_MESSAGE_BOOKMARKED, KIND_STREAM_MESSAGE_DIFF, KIND_STREAM_MESSAGE_EDIT,
35+
KIND_STREAM_MESSAGE_PINNED, KIND_STREAM_MESSAGE_SCHEDULED, KIND_STREAM_MESSAGE_V2,
36+
KIND_STREAM_REMINDER, KIND_TEAM, KIND_TEAM_CATALOG, KIND_TEXT_NOTE, KIND_USER_STATUS,
37+
KIND_WORKFLOW_DEF, KIND_WORKFLOW_TRIGGER, RELAY_ADMIN_ADD_MEMBER, RELAY_ADMIN_CHANGE_ROLE,
38+
RELAY_ADMIN_REMOVE_MEMBER, RELAY_ADMIN_SET_WORKSPACE_PROFILE,
3939
};
4040
use buzz_core::tenant::TenantContext;
4141
use buzz_core::verification::verify_event;
@@ -482,7 +482,13 @@ fn required_scope_for_kind(kind: u32, event: &Event) -> Result<Scope, &'static s
482482
| KIND_STREAM_MESSAGE_DIFF
483483
| KIND_FORUM_POST
484484
| KIND_FORUM_VOTE
485-
| KIND_FORUM_COMMENT => Ok(Scope::MessagesWrite),
485+
| KIND_FORUM_COMMENT
486+
| KIND_JOB_REQUEST
487+
| KIND_JOB_ACCEPTED
488+
| KIND_JOB_PROGRESS
489+
| KIND_JOB_RESULT
490+
| KIND_JOB_ERROR
491+
| KIND_JOB_CANCEL => Ok(Scope::MessagesWrite),
486492
KIND_NIP29_PUT_USER | KIND_NIP29_REMOVE_USER | KIND_NIP29_DELETE_GROUP => {
487493
Ok(Scope::AdminChannels)
488494
}
@@ -720,6 +726,12 @@ pub(crate) fn requires_h_channel_scope(kind: u32) -> bool {
720726
| KIND_FORUM_POST
721727
| KIND_FORUM_VOTE
722728
| KIND_FORUM_COMMENT
729+
| KIND_JOB_REQUEST
730+
| KIND_JOB_ACCEPTED
731+
| KIND_JOB_PROGRESS
732+
| KIND_JOB_RESULT
733+
| KIND_JOB_ERROR
734+
| KIND_JOB_CANCEL
723735
// NIP-29 admin kinds (except CREATE_GROUP which creates the channel)
724736
| KIND_NIP29_PUT_USER
725737
| KIND_NIP29_REMOVE_USER

crates/buzz-test-client/tests/e2e_relay.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ use std::time::Duration;
2222

2323
use base64::engine::general_purpose::STANDARD as BASE64;
2424
use base64::Engine;
25+
use buzz_core::job::{
26+
JobDisposition, JobResultPayload, JobVerification, JobVerificationStatus,
27+
JOB_RESULT_SCHEMA_VERSION,
28+
};
29+
use buzz_core::kind::{KIND_JOB_REQUEST, KIND_JOB_RESULT};
2530
use buzz_test_client::{BuzzTestClient, RelayMessage, TestClientError};
2631
use nostr::{Alphabet, EventBuilder, Filter, Keys, Kind, SingleLetterTag, Tag};
2732
use sha2::{Digest, Sha256};
@@ -414,6 +419,101 @@ async fn test_send_event_and_receive_via_subscription() {
414419
client_b.disconnect().await.expect("disconnect B");
415420
}
416421

422+
#[tokio::test]
423+
#[ignore]
424+
async fn test_structured_and_legacy_job_results_round_trip() {
425+
let url = relay_url();
426+
let keys = Keys::generate();
427+
let channel = create_test_channel(&keys).await;
428+
let channel_id = Uuid::parse_str(&channel).expect("channel UUID");
429+
let mut client = BuzzTestClient::connect(&url, &keys).await.expect("connect");
430+
431+
let request = EventBuilder::new(
432+
Kind::Custom(KIND_JOB_REQUEST as u16),
433+
"Prepare the release handoff",
434+
)
435+
.tags([Tag::parse(["h", channel.as_str()]).expect("request h tag")])
436+
.sign_with_keys(&keys)
437+
.expect("sign job request");
438+
let ok = client
439+
.send_event(request.clone())
440+
.await
441+
.expect("send job request");
442+
assert!(ok.accepted, "relay rejected job request: {}", ok.message);
443+
444+
let payload = JobResultPayload {
445+
schema_version: JOB_RESULT_SCHEMA_VERSION,
446+
job_request: request.id.to_hex(),
447+
requested_outcome: "Prepare the release handoff".into(),
448+
outcome: "Release handoff is ready".into(),
449+
last_progress: Some("Verified the release checklist".into()),
450+
disposition: JobDisposition::NoArtifact,
451+
artifacts: Vec::new(),
452+
verification: vec![JobVerification {
453+
label: "Release checklist".into(),
454+
status: JobVerificationStatus::Passed,
455+
evidence: Some("All required items passed".into()),
456+
}],
457+
blocker: None,
458+
};
459+
let structured = buzz_sdk::build_job_result(channel_id, request.id, &payload)
460+
.expect("build structured result")
461+
.sign_with_keys(&keys)
462+
.expect("sign structured result");
463+
let ok = client
464+
.send_event(structured.clone())
465+
.await
466+
.expect("send structured result");
467+
assert!(
468+
ok.accepted,
469+
"relay rejected structured result: {}",
470+
ok.message
471+
);
472+
473+
let legacy_content = "Release handoff completed. Checklist passed.";
474+
let legacy = EventBuilder::new(Kind::Custom(KIND_JOB_RESULT as u16), legacy_content)
475+
.tags([
476+
Tag::parse(["h", channel.as_str()]).expect("legacy h tag"),
477+
Tag::parse(["e", &request.id.to_hex(), "", "reply"]).expect("legacy reply tag"),
478+
])
479+
.sign_with_keys(&keys)
480+
.expect("sign legacy result");
481+
let ok = client
482+
.send_event(legacy.clone())
483+
.await
484+
.expect("send legacy result");
485+
assert!(ok.accepted, "relay rejected legacy result: {}", ok.message);
486+
487+
let sid = sub_id("job-result-round-trip");
488+
let filter = Filter::new()
489+
.kind(Kind::Custom(KIND_JOB_RESULT as u16))
490+
.custom_tags(SingleLetterTag::lowercase(Alphabet::H), [channel.as_str()]);
491+
client
492+
.subscribe(&sid, vec![filter])
493+
.await
494+
.expect("subscribe to job results");
495+
let events = client
496+
.collect_until_eose(&sid, Duration::from_secs(5))
497+
.await
498+
.expect("query job results");
499+
500+
let stored_structured = events
501+
.iter()
502+
.find(|event| event.id == structured.id)
503+
.expect("structured result returned by relay");
504+
assert_eq!(
505+
stored_structured.content,
506+
payload.to_json().expect("payload JSON")
507+
);
508+
let stored_legacy = events
509+
.iter()
510+
.find(|event| event.id == legacy.id)
511+
.expect("legacy result returned by relay");
512+
assert_eq!(stored_legacy.content, legacy_content);
513+
514+
client.disconnect().await.expect("disconnect");
515+
}
516+
417517
#[tokio::test]
418518
#[ignore]
419519
async fn test_large_event_frame_below_configured_limit_is_accepted() {

0 commit comments

Comments
 (0)