diff --git a/bobbin/crates/resolver/src/legacy_upgrade.rs b/bobbin/crates/resolver/src/legacy_upgrade.rs index 5f3ed6298..dc297c0fc 100644 --- a/bobbin/crates/resolver/src/legacy_upgrade.rs +++ b/bobbin/crates/resolver/src/legacy_upgrade.rs @@ -91,13 +91,11 @@ pub fn synthesize_created_at(bytes: &[u8], fallback_rfc3339: &str) -> Option &'static [(&'static str, FieldRule)] { match nsid { - "sh.tangled.actor.profile" => &[("preferredHandle", FieldRule::DropIfEmptyString)], "sh.tangled.label.op" => &[ ("add", FieldRule::EnsureArray), ("delete", FieldRule::EnsureArray), @@ -123,9 +121,6 @@ pub fn scrub_record_bytes>( let touched: alloc::vec::Vec<(&str, FieldRule)> = rules .iter() .filter_map(|(field, rule)| match (rule, obj.get(*field)) { - (FieldRule::DropIfEmptyString, Some(serde_json::Value::String(s))) if s.is_empty() => { - Some((*field, *rule)) - } (FieldRule::EnsureArray, Some(serde_json::Value::Null) | None) => Some((*field, *rule)), _ => None, }) @@ -134,9 +129,6 @@ pub fn scrub_record_bytes>( return None; } touched.iter().for_each(|(field, rule)| match rule { - FieldRule::DropIfEmptyString => { - obj.remove(*field); - } FieldRule::EnsureArray => { obj.insert( (*field).to_owned(), @@ -535,51 +527,16 @@ mod tests { assert!(scrub_record_bytes(&nsid("sh.tangled.repo.issue"), json).is_none()); } - #[test] - fn scrub_returns_none_when_target_field_is_non_empty() { - let json = - br#"{"$type":"sh.tangled.actor.profile","bluesky":true,"preferredHandle":"nel.pet"}"#; - assert!(scrub_record_bytes(&nsid("sh.tangled.actor.profile"), json).is_none()); - } - - #[test] - fn scrub_returns_none_when_target_field_is_absent() { - let json = br#"{"$type":"sh.tangled.actor.profile","bluesky":true}"#; - assert!(scrub_record_bytes(&nsid("sh.tangled.actor.profile"), json).is_none()); - } - - #[test] - fn scrub_returns_none_for_non_string_value() { - let json = br#"{"$type":"sh.tangled.actor.profile","bluesky":true,"preferredHandle":42}"#; - assert!(scrub_record_bytes(&nsid("sh.tangled.actor.profile"), json).is_none()); - } - #[test] fn scrub_returns_none_for_non_object_json() { - assert!(scrub_record_bytes(&nsid("sh.tangled.actor.profile"), b"[]").is_none()); - assert!(scrub_record_bytes(&nsid("sh.tangled.actor.profile"), b"null").is_none()); - assert!(scrub_record_bytes(&nsid("sh.tangled.actor.profile"), b"123").is_none()); + assert!(scrub_record_bytes(&nsid("sh.tangled.repo.pull"), b"[]").is_none()); + assert!(scrub_record_bytes(&nsid("sh.tangled.repo.pull"), b"null").is_none()); + assert!(scrub_record_bytes(&nsid("sh.tangled.repo.pull"), b"123").is_none()); } #[test] fn scrub_returns_none_for_invalid_json() { - assert!(scrub_record_bytes(&nsid("sh.tangled.actor.profile"), b"{not json").is_none()); - } - - #[test] - fn scrub_drops_empty_preferred_handle_and_preserves_other_fields() { - let json = br#"{"$type":"sh.tangled.actor.profile","bluesky":true,"preferredHandle":"","description":"hi"}"#; - let scrubbed = scrub_record_bytes(&nsid("sh.tangled.actor.profile"), json) - .expect("empty preferredHandle must trigger scrub"); - let value: serde_json::Value = serde_json::from_slice(&scrubbed).expect("valid json"); - let obj = value.as_object().expect("object"); - assert!(!obj.contains_key("preferredHandle")); - assert_eq!(obj.get("bluesky"), Some(&serde_json::json!(true))); - assert_eq!(obj.get("description"), Some(&serde_json::json!("hi"))); - assert_eq!( - obj.get("$type"), - Some(&serde_json::json!("sh.tangled.actor.profile")) - ); + assert!(scrub_record_bytes(&nsid("sh.tangled.repo.pull"), b"{not json").is_none()); } #[test] @@ -636,23 +593,6 @@ mod tests { )); } - #[test] - fn try_decode_recovers_profile_with_empty_preferred_handle() { - let json = br#"{"$type":"sh.tangled.actor.profile","bluesky":true,"preferredHandle":"","description":"hi"}"#; - let decoded = DecodedRecord::try_decode(&nsid("sh.tangled.actor.profile"), json) - .expect("profile with empty preferredHandle must scrub-recover"); - match decoded { - DecodedRecord::Canon(record) => match *record { - Record::Profile(p) => { - assert!(p.preferred_handle.is_none()); - assert_eq!(p.description.as_deref(), Some("hi")); - } - other => panic!("expected canon profile, got {other:?}"), - }, - other => panic!("expected canon profile, got {other:?}"), - } - } - #[test] fn legacy_decode_passes_through_for_unaffected_nsids() { let json = br#"{"$type":"org.tangled.graph.follow","createdAt":"2026-05-01T00:00:00Z"}"#; diff --git a/bobbin/crates/resolver/src/normalize.rs b/bobbin/crates/resolver/src/normalize.rs index 1abc6f618..f33061044 100644 --- a/bobbin/crates/resolver/src/normalize.rs +++ b/bobbin/crates/resolver/src/normalize.rs @@ -104,7 +104,7 @@ macro_rules! identity_normalize { }; } -use bobbin_types::sh_tangled::actor::profile::Profile; +use bobbin_types::org_tangled::actor::profile::Profile; use bobbin_types::org_tangled::graph::follow::Follow; use bobbin_types::org_tangled::graph::vouch::Vouch; use bobbin_types::sh_tangled::feed::comment::Comment as FeedComment; diff --git a/bobbin/crates/types/src/edges.rs b/bobbin/crates/types/src/edges.rs index ebad7468e..355da1af5 100644 --- a/bobbin/crates/types/src/edges.rs +++ b/bobbin/crates/types/src/edges.rs @@ -8,8 +8,8 @@ use jacquard_common::types::tid::Tid; use jacquard_common::{BosStr, DefaultStr}; use crate::ids::{SubjectRef, nsid_static}; +use crate::org_tangled::actor::profile::Profile; use crate::org_tangled::feed::subscription::Subscription; -use crate::sh_tangled::actor::profile::Profile; use crate::org_tangled::graph::follow::Follow; use crate::org_tangled::graph::vouch::Vouch; use crate::sh_tangled::feed::comment::Comment as FeedCommentRecord; @@ -98,7 +98,7 @@ impl Record { }; } match nsid.as_ref() { - "sh.tangled.actor.profile" => parse!(Profile), + "org.tangled.actor.profile" => parse!(Profile), "sh.tangled.feed.comment" => parse!(FeedComment), "sh.tangled.feed.reaction" => parse!(Reaction), "sh.tangled.feed.star" => parse!(Star), @@ -127,7 +127,7 @@ impl Record { pub fn collection(&self) -> Nsid { let s: &'static str = match self { - Self::Profile(_) => "sh.tangled.actor.profile", + Self::Profile(_) => "org.tangled.actor.profile", Self::FeedComment(_) => "sh.tangled.feed.comment", Self::Reaction(_) => "sh.tangled.feed.reaction", Self::Star(_) => "sh.tangled.feed.star", @@ -1233,9 +1233,9 @@ mod tests { #[test] fn profile_and_vouch_emit_no_edges() { let profile = extract( - "sh.tangled.actor.profile", - "at://did:plc:teq/sh.tangled.actor.profile/self", - json!({"$type": "sh.tangled.actor.profile", "bluesky": false}), + "org.tangled.actor.profile", + "at://did:plc:teq/org.tangled.actor.profile/self", + json!({"$type": "org.tangled.actor.profile"}), ); assert!(profile.is_empty()); let vouch = extract( diff --git a/bobbin/crates/types/src/search.rs b/bobbin/crates/types/src/search.rs index 40b132237..0b6f9622f 100644 --- a/bobbin/crates/types/src/search.rs +++ b/bobbin/crates/types/src/search.rs @@ -10,7 +10,7 @@ use serde::Serialize; use crate::edges::{ExtractError, Record}; use crate::ids::nsid_static; -use crate::sh_tangled::actor::profile::Profile; +use crate::org_tangled::actor::profile::Profile; use crate::sh_tangled::feed::comment::Comment as FeedCommentRecord; use crate::sh_tangled::label::definition::Definition as LabelDefinitionRecord; use crate::sh_tangled::repo::Repo as RepoRecord; @@ -93,7 +93,7 @@ impl SearchableRecord { pub fn nsid(&self) -> Nsid { let s: &'static str = match self { - Self::Profile(_) => "sh.tangled.actor.profile", + Self::Profile(_) => "org.tangled.actor.profile", Self::Repo(_) => "sh.tangled.repo", Self::Issue(_) => "sh.tangled.repo.issue", Self::Pull(_) => "sh.tangled.repo.pull", @@ -161,7 +161,7 @@ fn profile_doc(source: &AtUri, r: &Profile) -> SearchDoc } doc( source, - "sh.tangled.actor.profile", + "org.tangled.actor.profile", &title, parts, None, diff --git a/bobbin/crates/xrpc/src/lib.rs b/bobbin/crates/xrpc/src/lib.rs index 4a4585a9b..74069fe39 100644 --- a/bobbin/crates/xrpc/src/lib.rs +++ b/bobbin/crates/xrpc/src/lib.rs @@ -42,12 +42,12 @@ use bobbin_slingshot_client::{SlingshotClient, SlingshotError}; use bobbin_types::edges::REPO_SOURCE_EDGE_KIND; use bobbin_types::ids::{EdgeKey, SubjectRef, nsid_static, owner_did_from_aturi}; use bobbin_types::knot_acl::{KnotOwnedSource, decode_knot_owned_source}; +use bobbin_types::org_tangled::actor::profile::{Profile, ProfileGetRecordOutput, ProfileRecord}; use bobbin_types::org_tangled::feed::subscription::SubscriptionRecord; use bobbin_types::org_tangled::graph::follow::{Follow, FollowRecord}; use bobbin_types::org_tangled::graph::vouch::{Vouch, VouchRecord}; use bobbin_types::record::RecordBody; use bobbin_types::search::SearchableRecord; -use bobbin_types::sh_tangled::actor::profile::{Profile, ProfileGetRecordOutput, ProfileRecord}; use bobbin_types::sh_tangled::feed::comment::{ Comment as FeedComment, CommentRecord as FeedCommentRecord, }; @@ -3935,7 +3935,7 @@ async fn search_actors_typeahead( .into_iter() .map(|suggestion| { let uri = AtUri::new_owned(format!( - "at://{}/sh.tangled.actor.profile/self", + "at://{}/org.tangled.actor.profile/self", suggestion.did )) .map_err(|error| { diff --git a/bobbin/crates/xrpc/src/recordpath.rs b/bobbin/crates/xrpc/src/recordpath.rs index d69cf71df..5863d4c80 100644 --- a/bobbin/crates/xrpc/src/recordpath.rs +++ b/bobbin/crates/xrpc/src/recordpath.rs @@ -300,7 +300,7 @@ mod tests { let doc = json!({ "items": [ {"$type": "sh.tangled.repo", "uri": "at://did:plc:a/sh.tangled.repo/x"}, - {"$type": "sh.tangled.actor.profile", "did": "did:plc:b"}, + {"$type": "org.tangled.actor.profile", "did": "did:plc:b"}, ] }); let segs = parse_ok("items[sh.tangled.repo].uri"); diff --git a/bobbin/crates/xrpc/tests/bulk.rs b/bobbin/crates/xrpc/tests/bulk.rs index 496639f03..9c0b2a883 100644 --- a/bobbin/crates/xrpc/tests/bulk.rs +++ b/bobbin/crates/xrpc/tests/bulk.rs @@ -180,8 +180,7 @@ fn repo_body(name: &str) -> Value { fn profile_body(handle: &Handle) -> Value { json!({ - "$type": "sh.tangled.actor.profile", - "bluesky": false, + "$type": "org.tangled.actor.profile", "preferredHandle": handle.as_ref() }) } @@ -233,14 +232,14 @@ async fn get_profiles_returns_all_resolved_profiles() { let h = Harness::new().await; h.mount( &did("did:plc:nel"), - &nsid("sh.tangled.actor.profile"), + &nsid("org.tangled.actor.profile"), &rkey("self"), profile_body(&handle("witchcraft.systems")), ) .await; h.mount( &did("did:plc:teq"), - &nsid("sh.tangled.actor.profile"), + &nsid("org.tangled.actor.profile"), &rkey("self"), profile_body(&handle("olaren.dev")), ) @@ -251,8 +250,8 @@ async fn get_profiles_returns_all_resolved_profiles() { "sh.tangled.actor.getProfiles", "actors", &[ - "at://did:plc:nel/sh.tangled.actor.profile/self", - "at://did:plc:teq/sh.tangled.actor.profile/self", + "at://did:plc:nel/org.tangled.actor.profile/self", + "at://did:plc:teq/org.tangled.actor.profile/self", ], )) .await @@ -269,7 +268,7 @@ async fn get_profiles_accepts_percent_escaped_at_uris() { let h = Harness::new().await; h.mount( &did("did:plc:nel"), - &nsid("sh.tangled.actor.profile"), + &nsid("org.tangled.actor.profile"), &rkey("self"), profile_body(&handle("witchcraft.systems")), ) @@ -279,7 +278,7 @@ async fn get_profiles_accepts_percent_escaped_at_uris() { app.oneshot(bulk_request_escaped( "sh.tangled.actor.getProfiles", "actors", - &["at://did:plc:nel/sh.tangled.actor.profile/self"], + &["at://did:plc:nel/org.tangled.actor.profile/self"], )) .await .unwrap(), diff --git a/bobbin/crates/xrpc/tests/cold_start.rs b/bobbin/crates/xrpc/tests/cold_start.rs index aeb492d0b..ea89129f9 100644 --- a/bobbin/crates/xrpc/tests/cold_start.rs +++ b/bobbin/crates/xrpc/tests/cold_start.rs @@ -126,11 +126,10 @@ async fn cold_start_serves_all_four_point_lookups() { mount_record( &server, &clam, - &nsid("sh.tangled.actor.profile"), + &nsid("org.tangled.actor.profile"), &rkey("self"), json!({ - "$type": "sh.tangled.actor.profile", - "bluesky": false, + "$type": "org.tangled.actor.profile", "description": "clam shell" }), ) @@ -179,7 +178,7 @@ async fn cold_start_serves_all_four_point_lookups() { ( "sh.tangled.actor.getProfile", "actor", - format!("at://{}/sh.tangled.actor.profile/self", clam.as_ref()), + format!("at://{}/org.tangled.actor.profile/self", clam.as_ref()), "description", json!("clam shell"), ), @@ -227,11 +226,10 @@ async fn percent_escaped_at_uri_resolves_identically_to_raw() { mount_record( &server, &clam, - &nsid("sh.tangled.actor.profile"), + &nsid("org.tangled.actor.profile"), &rkey("self"), json!({ - "$type": "sh.tangled.actor.profile", - "bluesky": false, + "$type": "org.tangled.actor.profile", "description": "clam shell" }), ) @@ -240,7 +238,7 @@ async fn percent_escaped_at_uri_resolves_identically_to_raw() { let state = fresh_app(&Url::parse(&server.uri()).unwrap()).await; let app = router(state); - let at_uri = format!("at://{}/sh.tangled.actor.profile/self", clam.as_ref()); + let at_uri = format!("at://{}/org.tangled.actor.profile/self", clam.as_ref()); let (raw_status, raw_body) = json_response( app.clone() @@ -324,7 +322,7 @@ async fn collection_mismatch_is_400() { .oneshot(xrpc_request( "sh.tangled.repo.getRepo", "repo", - "at://did:plc:clam/sh.tangled.actor.profile/self", + "at://did:plc:clam/org.tangled.actor.profile/self", )) .await .unwrap(); @@ -432,41 +430,6 @@ async fn wrong_type_does_not_poison_cache() { drop(mock); } -#[tokio::test] -async fn profile_with_empty_preferred_handle_is_tolerated() { - let server = MockServer::start().await; - let nel = did("did:plc:nel"); - mount_record( - &server, - &nel, - &nsid("sh.tangled.actor.profile"), - &rkey("self"), - json!({ - "$type": "sh.tangled.actor.profile", - "bluesky": true, - "preferredHandle": "", - "description": "empty handle, valid profile" - }), - ) - .await; - let state = fresh_app(&Url::parse(&server.uri()).unwrap()).await; - let app = router(state); - let at_uri = format!("at://{}/sh.tangled.actor.profile/self", nel.as_ref()); - let resp = app - .oneshot(xrpc_request( - "sh.tangled.actor.getProfile", - "actor", - &at_uri, - )) - .await - .unwrap(); - let (status, body) = json_response(resp).await; - assert_eq!(status, StatusCode::OK, "status: {body}"); - assert_eq!(body["uri"], at_uri); - assert_eq!(body["value"]["description"], "empty handle, valid profile"); - assert!(body["value"]["preferredHandle"].is_null()); -} - #[tokio::test] async fn missing_uri_param_returns_json_envelope() { let server = MockServer::start().await; diff --git a/bobbin/crates/xrpc/tests/search.rs b/bobbin/crates/xrpc/tests/search.rs index 95fc53895..8e4bcdbfa 100644 --- a/bobbin/crates/xrpc/tests/search.rs +++ b/bobbin/crates/xrpc/tests/search.rs @@ -946,7 +946,7 @@ async fn actor_typeahead_matches_handles_without_hydration() { assert_eq!( actors[0], json!({ - "uri": "at://did:plc:dawn/sh.tangled.actor.profile/self", + "uri": "at://did:plc:dawn/org.tangled.actor.profile/self", "did": "did:plc:dawn", "handle": "dawn.example.com", }), @@ -955,7 +955,7 @@ async fn actor_typeahead_matches_handles_without_hydration() { assert_eq!(actors[1]["did"], json!("did:plc:fawn")); assert_eq!( actors[1]["uri"], - json!("at://did:plc:fawn/sh.tangled.actor.profile/self") + json!("at://did:plc:fawn/org.tangled.actor.profile/self") ); assert_eq!(actors[1]["handle"], json!("fawn.example.com")); } @@ -1023,7 +1023,7 @@ async fn actor_typeahead_handles_rename_and_removal() { assert_eq!(actors[0]["did"], json!("did:plc:dawn")); assert_eq!( actors[0]["uri"], - json!("at://did:plc:dawn/sh.tangled.actor.profile/self") + json!("at://did:plc:dawn/org.tangled.actor.profile/self") ); let response = app