From 1335ebfe91ae3348132fae7894a914d5c8a7f417 Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Wed, 17 Jun 2026 17:44:14 +0900 Subject: [PATCH 1/5] Add mocked core flow test Assisted-by: Codex:gpt-5.5 --- crates/feder-core/src/lib.rs | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/crates/feder-core/src/lib.rs b/crates/feder-core/src/lib.rs index 3e09a9c..6231ea0 100644 --- a/crates/feder-core/src/lib.rs +++ b/crates/feder-core/src/lib.rs @@ -680,6 +680,56 @@ mod tests { } } + #[test] + fn mocked_core_flow_accepts_follow_then_delivers_created_note() { + let mut core = core(); + let follow = vocab::Follow::new( + iri("https://remote.example/activities/follow/1"), + vocab::Reference::object(actor("https://remote.example/users/bob")), + vocab::Reference::id(iri("https://example.com/users/alice")), + ); + + let follow_result = core.handle(received_follow( + follow, + "https://example.com/activities/accept/1", + )); + + assert_eq!(follow_result.actions.len(), 2); + assert!(matches!(follow_result.actions[0], Action::StoreFollower(_))); + let Action::SendActivity(accept_delivery) = &follow_result.actions[1] else { + panic!("expected Accept delivery action"); + }; + assert_eq!( + accept_delivery.inbox, + iri("https://remote.example/users/bob/inbox") + ); + assert!(matches!(accept_delivery.activity, Activity::Accept(_))); + + let create_result = core.handle(Input::UserCreateNote(UserCreateNote { + note_id: iri("https://example.com/notes/1"), + create_id: iri("https://example.com/activities/create/1"), + actor: vocab::Reference::id(iri("https://example.com/users/alice")), + content: "Hello from Feder.".to_string(), + published: Some("2026-06-10T00:00:00Z".to_string()), + })); + + assert_eq!(create_result.actions.len(), 2); + assert!(matches!(create_result.actions[0], Action::StoreObject(_))); + let Action::SendActivity(create_delivery) = &create_result.actions[1] else { + panic!("expected Create delivery action"); + }; + assert_eq!( + create_delivery.inbox, + iri("https://remote.example/users/bob/inbox") + ); + assert!(matches!(create_delivery.activity, Activity::CreateNote(_))); + + assert_eq!(core.state().followers().len(), 1); + assert_eq!(core.state().delivery_targets().len(), 1); + assert_eq!(core.state().objects().len(), 1); + assert_eq!(core.state().activities().len(), 1); + } + #[test] fn user_create_note_normalizes_embedded_local_actor_to_local_actor_id() { let mut supplied_actor = actor("https://example.com/users/alice"); From 15ab953e145b8fe7b285784cbc36ff06644c0756 Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Wed, 17 Jun 2026 22:30:55 +0900 Subject: [PATCH 2/5] Emit store follower only for new follows Assisted-by: Codex:gpt-5.5 --- crates/feder-core/src/lib.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/crates/feder-core/src/lib.rs b/crates/feder-core/src/lib.rs index 6231ea0..9550884 100644 --- a/crates/feder-core/src/lib.rs +++ b/crates/feder-core/src/lib.rs @@ -131,12 +131,12 @@ impl FederState { if !self.followers.contains(&relation) { self.followers.push(relation.clone()); - } - actions.push(Action::StoreFollower(StoreFollower { - follower: follow.actor.clone(), - following: follow.object.clone(), - })); + actions.push(Action::StoreFollower(StoreFollower { + follower: follow.actor.clone(), + following: follow.object.clone(), + })); + } let mut inbox = self .delivery_targets @@ -470,7 +470,14 @@ mod tests { )); assert_eq!(first_result.actions.len(), 2); - assert_eq!(second_result.actions.len(), 2); + assert_eq!(second_result.actions.len(), 1); + assert!(matches!( + second_result.actions[0], + Action::SendActivity(SendActivity { + activity: Activity::Accept(_), + .. + }) + )); assert_eq!( core.state().followers(), From fa97e1f7a54d8357ddc854ccef59a44b94fd510a Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Wed, 17 Jun 2026 23:31:25 +0900 Subject: [PATCH 3/5] Avoid phase-specific doc wording Assisted-by: Codex:gpt-5.5 --- crates/feder-core/src/lib.rs | 6 +++--- crates/feder-vocab/src/lib.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/feder-core/src/lib.rs b/crates/feder-core/src/lib.rs index 9550884..c520208 100644 --- a/crates/feder-core/src/lib.rs +++ b/crates/feder-core/src/lib.rs @@ -58,7 +58,7 @@ impl FederConfig { } } -/// In-memory state used by Phase 1 core flows. +/// In-memory state used by portable core flows. #[derive(Clone, Debug, Eq, PartialEq)] pub struct FederState { local_actor: vocab::Actor, @@ -277,8 +277,8 @@ pub struct Follower { /// A known actor inbox for future delivery. /// -/// Phase 1 records this only when an incoming object embeds enough actor data -/// to expose an inbox. It does not imply every follower has been resolved. +/// Core records this only when an incoming object embeds enough actor data to +/// expose an inbox. It does not imply every follower has been resolved. #[derive(Clone, Debug, Eq, PartialEq)] pub struct DeliveryTarget { pub actor: vocab::Iri, diff --git a/crates/feder-vocab/src/lib.rs b/crates/feder-vocab/src/lib.rs index 0d7a516..f806167 100644 --- a/crates/feder-vocab/src/lib.rs +++ b/crates/feder-vocab/src/lib.rs @@ -20,7 +20,7 @@ pub type Iri = IriString; /// A non-scalar ActivityStreams property value. /// /// ActivityStreams object slots can contain either an embedded object or the -/// object's IRI. Phase 1 keeps both forms explicit and avoids dereferencing. +/// object's IRI. Feder keeps both forms explicit and avoids dereferencing. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(untagged)] pub enum Reference { @@ -167,7 +167,7 @@ pub enum ActorType { Service, } -/// A minimal ActivityPub actor for Phase 1 core tests. +/// A minimal ActivityPub actor. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Actor { #[serde(rename = "@context", skip_serializing_if = "Option::is_none")] From 10761850470c8fb2fb6b505fb3eb293940122f25 Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Wed, 17 Jun 2026 23:46:11 +0900 Subject: [PATCH 4/5] Expose delivery target storage actions Assisted-by: Codex:gpt-5.5 --- crates/feder-core/src/lib.rs | 68 +++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/crates/feder-core/src/lib.rs b/crates/feder-core/src/lib.rs index c520208..6940679 100644 --- a/crates/feder-core/src/lib.rs +++ b/crates/feder-core/src/lib.rs @@ -149,15 +149,26 @@ impl FederState { actor: follower, inbox: actor.inbox.clone(), }; + let mut should_store_target = false; if let Some(existing) = self .delivery_targets .iter_mut() .find(|existing| existing.actor == target.actor) { - existing.inbox = target.inbox; + if existing.inbox != target.inbox { + existing.inbox = target.inbox.clone(); + should_store_target = true; + } } else { - self.delivery_targets.push(target); + self.delivery_targets.push(target.clone()); + should_store_target = true; + } + + if should_store_target { + actions.push(Action::StoreDeliveryTarget(StoreDeliveryTarget { + target: target.clone(), + })); } inbox = Some(actor.inbox.clone()); @@ -290,6 +301,7 @@ pub struct DeliveryTarget { #[non_exhaustive] pub enum Action { StoreFollower(StoreFollower), + StoreDeliveryTarget(StoreDeliveryTarget), StoreObject(StoreObject), SendActivity(SendActivity), } @@ -300,6 +312,11 @@ pub struct StoreFollower { pub following: vocab::Reference, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StoreDeliveryTarget { + pub target: DeliveryTarget, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct StoreObject { pub object: Object, @@ -398,7 +415,7 @@ mod tests { "https://example.com/activities/accept/1", )); - assert_eq!(result.actions.len(), 2); + assert_eq!(result.actions.len(), 3); assert_eq!( core.state().followers(), &[Follower { @@ -420,8 +437,17 @@ mod tests { following: vocab::Reference::id(iri("https://example.com/users/alice")), }) ); + assert_eq!( + result.actions[1], + Action::StoreDeliveryTarget(StoreDeliveryTarget { + target: DeliveryTarget { + actor: iri("https://remote.example/users/bob"), + inbox: iri("https://remote.example/users/bob/inbox"), + }, + }) + ); - let Action::SendActivity(send) = &result.actions[1] else { + let Action::SendActivity(send) = &result.actions[2] else { panic!("expected SendActivity action"); }; assert_eq!(send.inbox, iri("https://remote.example/users/bob/inbox")); @@ -469,15 +495,27 @@ mod tests { "https://example.com/activities/accept/2", )); - assert_eq!(first_result.actions.len(), 2); - assert_eq!(second_result.actions.len(), 1); - assert!(matches!( + assert_eq!(first_result.actions.len(), 3); + assert_eq!(second_result.actions.len(), 2); + assert_eq!( second_result.actions[0], - Action::SendActivity(SendActivity { - activity: Activity::Accept(_), - .. + Action::StoreDeliveryTarget(StoreDeliveryTarget { + target: DeliveryTarget { + actor: iri("https://remote.example/users/bob"), + inbox: iri("https://remote.example/inboxes/bob"), + }, }) - )); + ); + + let Action::SendActivity(send) = &second_result.actions[1] else { + panic!("expected SendActivity action"); + }; + assert_eq!(send.inbox, iri("https://remote.example/inboxes/bob")); + + let Activity::Accept(accept) = &send.activity else { + panic!("expected Accept activity"); + }; + assert_eq!(accept.id, iri("https://example.com/activities/accept/2")); assert_eq!( core.state().followers(), @@ -701,9 +739,13 @@ mod tests { "https://example.com/activities/accept/1", )); - assert_eq!(follow_result.actions.len(), 2); + assert_eq!(follow_result.actions.len(), 3); assert!(matches!(follow_result.actions[0], Action::StoreFollower(_))); - let Action::SendActivity(accept_delivery) = &follow_result.actions[1] else { + assert!(matches!( + follow_result.actions[1], + Action::StoreDeliveryTarget(_) + )); + let Action::SendActivity(accept_delivery) = &follow_result.actions[2] else { panic!("expected Accept delivery action"); }; assert_eq!( From 9962e5e3b152375951da60fcf691e36ffaa47f71 Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Wed, 17 Jun 2026 23:50:28 +0900 Subject: [PATCH 5/5] Trim follow handling clones Assisted-by: Codex:gpt-5.5 --- crates/feder-core/src/lib.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/feder-core/src/lib.rs b/crates/feder-core/src/lib.rs index 6940679..8254263 100644 --- a/crates/feder-core/src/lib.rs +++ b/crates/feder-core/src/lib.rs @@ -111,11 +111,11 @@ impl FederState { fn record_follow(&mut self, input: ReceivedFollow) -> Vec { let follow = input.follow; - let Some(following) = reference_id(&follow.object).cloned() else { + let Some(following) = reference_id(&follow.object) else { return Vec::new(); }; - if following != self.local_actor.id { + if following != &self.local_actor.id { return Vec::new(); } @@ -125,7 +125,7 @@ impl FederState { let relation = Follower { follower: follower.clone(), - following, + following: following.clone(), }; let mut actions = Vec::new(); @@ -166,9 +166,7 @@ impl FederState { } if should_store_target { - actions.push(Action::StoreDeliveryTarget(StoreDeliveryTarget { - target: target.clone(), - })); + actions.push(Action::StoreDeliveryTarget(StoreDeliveryTarget { target })); } inbox = Some(actor.inbox.clone());