Complete milestone 09 world integration gate (#75)
Some checks failed
Native code generation / deterministic (push) Failing after 8m37s
Imaging and meshing gate / native (push) Successful in 5m24s
Native Rust workspace compile / compile (push) Failing after 6m15s

This commit is contained in:
2026-08-10 21:19:43 +00:00
parent 55424dbd7f
commit fdda05b53f
18 changed files with 767 additions and 82 deletions

View File

@@ -19,6 +19,11 @@ on:
- "tools/check_milestone_09_issue_61.py" - "tools/check_milestone_09_issue_61.py"
- "tools/check_milestone_09_issue_62.py" - "tools/check_milestone_09_issue_62.py"
- "tools/check_milestone_09_issue_63.py" - "tools/check_milestone_09_issue_63.py"
- "tools/check_milestone_09_issue_64.py"
- "tools/check_milestone_09_issue_65.py"
- "tools/check_milestone_09_issue_66.py"
- "tools/check_milestone_09.py"
- "tools/test_milestone_09.py"
- "crates/libremetaverse/src/agent_movement.rs" - "crates/libremetaverse/src/agent_movement.rs"
- "crates/libremetaverse/src/inventory.rs" - "crates/libremetaverse/src/inventory.rs"
- "crates/libremetaverse/src/inventory_manager.rs" - "crates/libremetaverse/src/inventory_manager.rs"
@@ -61,6 +66,11 @@ on:
- "tools/check_milestone_09_issue_61.py" - "tools/check_milestone_09_issue_61.py"
- "tools/check_milestone_09_issue_62.py" - "tools/check_milestone_09_issue_62.py"
- "tools/check_milestone_09_issue_63.py" - "tools/check_milestone_09_issue_63.py"
- "tools/check_milestone_09_issue_64.py"
- "tools/check_milestone_09_issue_65.py"
- "tools/check_milestone_09_issue_66.py"
- "tools/check_milestone_09.py"
- "tools/test_milestone_09.py"
- "crates/libremetaverse/src/agent_movement.rs" - "crates/libremetaverse/src/agent_movement.rs"
- "crates/libremetaverse/src/inventory.rs" - "crates/libremetaverse/src/inventory.rs"
- "crates/libremetaverse/src/inventory_manager.rs" - "crates/libremetaverse/src/inventory_manager.rs"
@@ -119,7 +129,10 @@ jobs:
python3 tools/check_milestone_09_issue_61.py python3 tools/check_milestone_09_issue_61.py
python3 tools/check_milestone_09_issue_62.py python3 tools/check_milestone_09_issue_62.py
python3 tools/check_milestone_09_issue_63.py python3 tools/check_milestone_09_issue_63.py
python3 tools/check_milestone_09_issue_64.py
python3 tools/check_milestone_09_issue_65.py
python3 tools/check_milestone_09_issue_66.py python3 tools/check_milestone_09_issue_66.py
python3 tools/check_milestone_09.py
cargo check --manifest-path tests/api-compile/Cargo.toml --locked cargo check --manifest-path tests/api-compile/Cargo.toml --locked
- name: Test generated wire codecs and translated parity cases - name: Test generated wire codecs and translated parity cases
run: | run: |

View File

@@ -5,6 +5,8 @@ on:
paths: paths:
- ".gitea/workflows/rust-workspace.yml" - ".gitea/workflows/rust-workspace.yml"
- "tools/install_openjpeg_2_5_4.sh" - "tools/install_openjpeg_2_5_4.sh"
- "tools/check_milestone_09.py"
- "tools/test_milestone_09.py"
- "**/*.rs" - "**/*.rs"
- "**/Cargo.toml" - "**/Cargo.toml"
- "Cargo.lock" - "Cargo.lock"
@@ -12,6 +14,8 @@ on:
paths: paths:
- ".gitea/workflows/rust-workspace.yml" - ".gitea/workflows/rust-workspace.yml"
- "tools/install_openjpeg_2_5_4.sh" - "tools/install_openjpeg_2_5_4.sh"
- "tools/check_milestone_09.py"
- "tools/test_milestone_09.py"
- "**/*.rs" - "**/*.rs"
- "**/Cargo.toml" - "**/Cargo.toml"
- "Cargo.lock" - "Cargo.lock"
@@ -43,5 +47,11 @@ jobs:
python3 tools/check_milestone_09_issue_61.py python3 tools/check_milestone_09_issue_61.py
python3 tools/check_milestone_09_issue_62.py python3 tools/check_milestone_09_issue_62.py
python3 tools/check_milestone_09_issue_63.py python3 tools/check_milestone_09_issue_63.py
python3 tools/check_milestone_09_issue_64.py
python3 tools/check_milestone_09_issue_65.py
python3 tools/check_milestone_09_issue_66.py
python3 tools/check_milestone_09.py
- name: Test the complete native world milestone
run: python3 tools/test_milestone_09.py
- name: Compile every workspace target with bounded memory - name: Compile every workspace target with bounded memory
run: cargo check --workspace --all-targets --locked -j 1 run: cargo check --workspace --all-targets --locked -j 1

View File

@@ -388,6 +388,15 @@ one atomic store transaction; cancellation and malformed responses cannot
partially mutate the cache. Focused compatibility and injected-HTTP tests cover partially mutate the cache. Focused compatibility and injected-HTTP tests cover
link correction, endpoint recording, error isolation, and metadata updates. link correction, endpoint recording, error isolation, and metadata updates.
The milestone closes with one deterministic cross-manager fake-grid scenario
and a reproducible owned-test runner. The scenario covers authenticated agent
state, movement and handoff, shared inventory/AIS state, assets and appearance,
avatar/animesh and object entry points, land and discovery caches, social,
estate, experience and marketplace construction, ordered events, redacted
diagnostics, clean logout, and root cancellation. The complete audit and test
boundary is documented in
[`docs/world-milestone-gate.md`](docs/world-milestone-gate.md).
Native appearance state now tracks layered wearables and attachment points, Native appearance state now tracks layered wearables and attachment points,
sends the inventory-backed wearable and attachment LLUDP messages, and exposes sends the inventory-backed wearable and attachment LLUDP messages, and exposes
worn queries by slot, item, and point. The client-owned Current Outfit Folder worn queries by slot, item, and point. The client-owned Current Outfit Folder

View File

@@ -4,7 +4,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.
| Assembly | Types | Members | Status | | Assembly | Types | Members | Status |
|---|---:|---:|---| |---|---:|---:|---|
| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 401 types / 17,023 members; remaining surface is callable failure-only shims | | `LibreMetaverse` | 2,711 | 27,281 | native implementation: 401 types / 17,025 members; remaining surface is callable failure-only shims |
| `LibreMetaverse.Imaging.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain | | `LibreMetaverse.Imaging.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain |
| `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain | | `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain |
| `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim | | `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim |

View File

@@ -129,6 +129,7 @@ struct ClientRuntime {
interest_list_manager: Mutex<Option<Arc<crate::interest_list::InterestListManagerInner>>>, interest_list_manager: Mutex<Option<Arc<crate::interest_list::InterestListManagerInner>>>,
friends_manager: Mutex<Option<Arc<crate::friends_manager::FriendsManagerInner>>>, friends_manager: Mutex<Option<Arc<crate::friends_manager::FriendsManagerInner>>>,
group_manager: Mutex<Option<Arc<crate::group_manager::GroupManagerInner>>>, group_manager: Mutex<Option<Arc<crate::group_manager::GroupManagerInner>>>,
marketplace_manager: Mutex<Option<crate::marketplace_runtime::MarketplaceManager>>,
appearance_manager: Mutex<Option<Arc<crate::appearance_manager::AppearanceManagerInner>>>, appearance_manager: Mutex<Option<Arc<crate::appearance_manager::AppearanceManagerInner>>>,
avatar_manager: Mutex<Option<Arc<crate::avatar_manager::AvatarManagerInner>>>, avatar_manager: Mutex<Option<Arc<crate::avatar_manager::AvatarManagerInner>>>,
animesh_manager: Mutex<Option<Arc<crate::animesh_runtime::AnimeshManagerInner>>>, animesh_manager: Mutex<Option<Arc<crate::animesh_runtime::AnimeshManagerInner>>>,
@@ -139,6 +140,12 @@ struct ClientRuntime {
shutdown_wait: Mutex<()>, shutdown_wait: Mutex<()>,
} }
fn clear_cached<T>(slot: &Mutex<Option<T>>) {
slot.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
}
impl ClientRuntime { impl ClientRuntime {
fn new( fn new(
services: Vec<Arc<dyn ClientService>>, services: Vec<Arc<dyn ClientService>>,
@@ -174,6 +181,7 @@ impl ClientRuntime {
interest_list_manager: Mutex::new(None), interest_list_manager: Mutex::new(None),
friends_manager: Mutex::new(None), friends_manager: Mutex::new(None),
group_manager: Mutex::new(None), group_manager: Mutex::new(None),
marketplace_manager: Mutex::new(None),
appearance_manager: Mutex::new(None), appearance_manager: Mutex::new(None),
avatar_manager: Mutex::new(None), avatar_manager: Mutex::new(None),
animesh_manager: Mutex::new(None), animesh_manager: Mutex::new(None),
@@ -248,42 +256,25 @@ impl ClientRuntime {
} }
} }
services.clear(); services.clear();
self.agent_throttle_sender clear_cached(&self.agent_throttle_sender);
.lock() clear_cached(&self.inventory_manager);
.unwrap_or_else(std::sync::PoisonError::into_inner) clear_cached(&self.inventory_ais_client);
.take(); clear_cached(&self.asset_manager);
self.inventory_manager clear_cached(&self.appearance_manager);
.lock() clear_cached(&self.avatar_manager);
.unwrap_or_else(std::sync::PoisonError::into_inner) clear_cached(&self.animesh_manager);
.take(); clear_cached(&self.object_manager);
self.inventory_ais_client clear_cached(&self.environment_manager);
.lock() clear_cached(&self.estate_tools);
.unwrap_or_else(std::sync::PoisonError::into_inner) clear_cached(&self.terrain_manager);
.take(); clear_cached(&self.sound_manager);
self.object_manager clear_cached(&self.parcel_manager);
.lock() clear_cached(&self.grid_manager);
.unwrap_or_else(std::sync::PoisonError::into_inner) clear_cached(&self.directory_manager);
.take(); clear_cached(&self.interest_list_manager);
self.grid_manager clear_cached(&self.friends_manager);
.lock() clear_cached(&self.group_manager);
.unwrap_or_else(std::sync::PoisonError::into_inner) clear_cached(&self.marketplace_manager);
.take();
self.directory_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.interest_list_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.friends_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.group_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.http_caps_client self.http_caps_client
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) .unwrap_or_else(std::sync::PoisonError::into_inner)
@@ -1106,6 +1097,32 @@ impl GridClient {
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner); .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.inner);
} }
pub(crate) fn native_marketplace(&self) -> crate::marketplace_runtime::MarketplaceManager {
let mut cached = self
.runtime
.marketplace_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(manager) = cached.as_ref() {
return manager.clone();
}
let manager = crate::marketplace_runtime::MarketplaceManager::native_new_client_owned(self);
*cached = Some(manager.clone());
manager
}
pub(crate) fn native_set_marketplace(
&self,
value: crate::marketplace_runtime::MarketplaceManager,
) {
value.native_bind_to_client(self);
*self
.runtime
.marketplace_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value);
}
pub(crate) fn native_set_caps_rate_limiter( pub(crate) fn native_set_caps_rate_limiter(
&mut self, &mut self,
value: crate::caps_http::CapsRateLimiter, value: crate::caps_http::CapsRateLimiter,

View File

@@ -6730,11 +6730,13 @@ impl GridClient {
} }
/// C# member: `P:LibreMetaverse.GridClient.Marketplace`. /// C# member: `P:LibreMetaverse.GridClient.Marketplace`.
pub fn marketplace(&self) -> libremetaverse::marketplace::MarketplaceManager { pub fn marketplace(&self) -> libremetaverse::marketplace::MarketplaceManager {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.GridClient.Marketplace") /* native client-core implementation */
self.native_marketplace()
} }
/// Setter for C# member: `P:LibreMetaverse.GridClient.Marketplace`. /// Setter for C# member: `P:LibreMetaverse.GridClient.Marketplace`.
pub fn set_marketplace(&mut self, value: libremetaverse::marketplace::MarketplaceManager) { pub fn set_marketplace(&mut self, value: libremetaverse::marketplace::MarketplaceManager) {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.GridClient.Marketplace") /* native client-core implementation */
self.native_set_marketplace(value)
} }
/// C# member: `P:LibreMetaverse.GridClient.Network`. /// C# member: `P:LibreMetaverse.GridClient.Network`.
pub fn network(&self) -> libremetaverse::NetworkManager { pub fn network(&self) -> libremetaverse::NetworkManager {
@@ -9930,9 +9932,14 @@ impl InventoryManager {
>, >,
>, >,
) -> Result<Option<Option<libremetaverse_types::UUID>>, crate::Error> { ) -> Result<Option<Option<libremetaverse_types::UUID>>, crate::Error> {
libremetaverse_types::not_implemented( self.native_upload_thumbnail(
"M:LibreMetaverse.InventoryManager.UploadThumbnailAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", inventory_id,
task_id,
j2c_image_data,
cancellation_token,
progress,
) )
.await
} }
/// C# member: `M:LibreMetaverse.InventoryManager.WaitForNextInventoryOfferAsync(System.Threading.CancellationToken)`. /// C# member: `M:LibreMetaverse.InventoryManager.WaitForNextInventoryOfferAsync(System.Threading.CancellationToken)`.
pub async fn wait_for_next_inventory_offer( pub async fn wait_for_next_inventory_offer(
@@ -17640,6 +17647,7 @@ pub mod assets {
pub const TERRAINS_PATH: &'static str = "terrains/"; pub const TERRAINS_PATH: &'static str = "terrains/";
/// C# member: `M:LibreMetaverse.Assets.ArchiveConstants.#ctor`. /// C# member: `M:LibreMetaverse.Assets.ArchiveConstants.#ctor`.
pub fn new() -> Result<Self, crate::Error> { pub fn new() -> Result<Self, crate::Error> {
/* native zero-sized constants namespace */
Ok(Self) Ok(Self)
} }
} }

View File

@@ -11,6 +11,7 @@ use libremetaverse_types::compat::{CancellationToken, HttpResponse, Uri};
use libremetaverse_types::{AssetType, UUID}; use libremetaverse_types::{AssetType, UUID};
use std::collections::{BTreeMap, HashMap, HashSet}; use std::collections::{BTreeMap, HashMap, HashSet};
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant};
const MAX_FOLDER_DEPTH_REQUEST: i32 = 50; const MAX_FOLDER_DEPTH_REQUEST: i32 = 50;
const MAX_AIS_OBJECTS: usize = 100_000; const MAX_AIS_OBJECTS: usize = 100_000;
@@ -110,6 +111,31 @@ impl InventoryAISClient {
caps.capability_uri(name.to_owned()) caps.capability_uri(name.to_owned())
} }
fn wait_for_capability(
&self,
name: &str,
cancellation: &CancellationToken,
) -> Result<Option<Uri>, Error> {
let client = self.client()?;
let Some(simulator) = client.native_network()?.current_sim() else {
return Ok(None);
};
let Some(caps) = simulator.native_caps() else {
return Ok(None);
};
let deadline = Instant::now() + Duration::from_secs(2);
loop {
cancellation.throw_if_cancellation_requested()?;
if let Some(uri) = caps.capability_uri(name.to_owned())? {
return Ok(Some(uri));
}
if caps.seed_request_finished() || Instant::now() >= deadline {
return Ok(None);
}
std::thread::sleep(Duration::from_millis(1));
}
}
pub(crate) fn native_is_available(&self) -> bool { pub(crate) fn native_is_available(&self) -> bool {
self.capability(Self::INVENTORY_CAP_NAME) self.capability(Self::INVENTORY_CAP_NAME)
.ok() .ok()
@@ -147,7 +173,7 @@ impl InventoryAISClient {
) -> Result<Option<(HttpResponse, Vec<u8>)>, Error> { ) -> Result<Option<(HttpResponse, Vec<u8>)>, Error> {
let token = Self::token(cancellation_token); let token = Self::token(cancellation_token);
token.throw_if_cancellation_requested()?; token.throw_if_cancellation_requested()?;
let Some(capability) = self.capability(cap_name)? else { let Some(capability) = self.wait_for_capability(cap_name, &token)? else {
return Ok(None); return Ok(None);
}; };
let bytes = match payload { let bytes = match payload {
@@ -359,18 +385,27 @@ impl InventoryAISClient {
new_inventory: OSD, new_inventory: OSD,
cancellation_token: Option<CancellationToken>, cancellation_token: Option<CancellationToken>,
) -> Result<Vec<InventoryItem>, Error> { ) -> Result<Vec<InventoryItem>, Error> {
let path = format!("category/{parent_uuid}?tid={}", Self::tid()?); let Some((response, bytes)) = self
let (_, _, _, links) = self .request(
.mutation(
Self::INVENTORY_CAP_NAME, Self::INVENTORY_CAP_NAME,
"POST", "POST",
path, format!("category/{parent_uuid}"),
BTreeMap::new(), BTreeMap::new(),
Some(new_inventory), Some(new_inventory),
false,
cancellation_token, cancellation_token,
) )
.await?; .await?
else {
return Ok(Vec::new());
};
if !Self::status_ok(&response) {
return Err(Error::HttpRequest);
}
let Some(map) = Self::decode_map(bytes)? else {
return Ok(Vec::new());
};
let (folders, items, links) = self.parse_all_map(&map)?;
self.reconcile(&map, &folders, &items, &links)?;
Ok(links) Ok(links)
} }

View File

@@ -784,9 +784,10 @@ impl InventoryManager {
pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> { pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
let client = client.ok_or(Error::ArgumentNull)?; let client = client.ok_or(Error::ArgumentNull)?;
let initial_store = Inventory::new_with_grid_client(Arc::clone(&client))?;
let inner = Arc::new(InventoryManagerInner { let inner = Arc::new(InventoryManagerInner {
client: client.native_weak_handle(), client: client.native_weak_handle(),
store: RwLock::new(None), store: RwLock::new(Some(initial_store)),
disposed: AtomicBool::new(false), disposed: AtomicBool::new(false),
callback_position: AtomicU32::new(0), callback_position: AtomicU32::new(0),
request_position: AtomicU64::new(0), request_position: AtomicU64::new(0),
@@ -1914,10 +1915,9 @@ impl InventoryManager {
.await? .await?
.ok_or(Error::InvalidOperation)?; .ok_or(Error::InvalidOperation)?;
let uri = Uri(format!( let uri = Uri(format!(
"{}/category/{}?tid={}", "{}/category/{}",
cap.0.trim_end_matches('/'), cap.0.trim_end_matches('/'),
folder_id, folder_id
UUID::random()?
)); ));
let payload = OSD::Map(HashMap::from([( let payload = OSD::Map(HashMap::from([(
"links".to_owned(), "links".to_owned(),
@@ -4397,6 +4397,103 @@ fn parse_task_inventory_values(
} }
impl InventoryManager { impl InventoryManager {
pub(crate) async fn native_upload_thumbnail(
&self,
inventory_id: UUID,
task_id: UUID,
j2c_image_data: Vec<u8>,
cancellation_token: Option<CancellationToken>,
progress: Option<
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
>,
) -> Result<Option<Option<UUID>>, Error> {
if inventory_id == UUID::zero()
|| j2c_image_data.is_empty()
|| j2c_image_data.len() > crate::asset_models::MAX_ASSET_BYTES
{
return Err(Error::Argument);
}
let token = cancellation_token.unwrap_or_default();
token.throw_if_cancellation_requested()?;
let client = self.client()?;
let Some(simulator) = client.native_network()?.current_sim() else {
return Ok(Some(None));
};
let Some(uri) = self
.wait_for_capability(&simulator, "InventoryThumbnailUpload", &token)
.await?
else {
return Ok(Some(None));
};
let inventory_store = self.native_store();
let known_local_folder = task_id == UUID::zero()
&& inventory_store
.as_ref()
.and_then(|store| {
store
.get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(inventory_id)
.ok()
.flatten()
})
.is_some();
let metadata = if known_local_folder {
HashMap::from([("category_id".to_owned(), OSD::UUID(inventory_id))])
} else {
let mut metadata = HashMap::from([("item_id".to_owned(), OSD::UUID(inventory_id))]);
if task_id != UUID::zero() {
metadata.insert("task_id".to_owned(), OSD::UUID(task_id));
}
metadata
};
let payload = OSDParser::serialize_llsd_xml_bytes(OSD::Map(metadata))?;
let (metadata_response, metadata_body) = client
.native_http_caps_client()
.post_with_uri_string_bytes_cancellation_token_i_progress(
uri,
"application/llsd+xml".into(),
payload,
token.clone(),
None,
)
.await?;
if !(200..300).contains(&metadata_response.status_code) {
return Ok(Some(None));
}
let metadata: serde_json::Value =
serde_json::from_slice(&metadata_body).map_err(|_| Error::Argument)?;
let Some(uploader) = metadata.get("uploader").and_then(serde_json::Value::as_str) else {
return Ok(Some(None));
};
let (upload_response, upload_body) = client
.native_http_caps_client()
.post_with_uri_string_bytes_cancellation_token_i_progress(
Uri(uploader.to_owned()),
"image/x-j2c".into(),
j2c_image_data,
token,
progress,
)
.await?;
if !(200..300).contains(&upload_response.status_code) {
return Ok(Some(None));
}
let result: serde_json::Value =
serde_json::from_slice(&upload_body).map_err(|_| Error::Argument)?;
if result.get("state").and_then(serde_json::Value::as_str) != Some("complete") {
return Ok(Some(None));
}
let asset_id = result
.get("new_asset")
.or_else(|| result.get("new_asset_id"))
.and_then(serde_json::Value::as_str)
.map(|id| UUID::new_with_string(id.to_owned()))
.transpose()?;
Ok(Some(asset_id))
}
pub(crate) async fn native_update_material_inventory( pub(crate) async fn native_update_material_inventory(
&self, &self,
material: crate::assets::AssetMaterial, material: crate::assets::AssetMaterial,

View File

@@ -5,6 +5,7 @@
#![allow(clippy::needless_pass_by_value)] #![allow(clippy::needless_pass_by_value)]
use crate::agent_manager::EventRegistry; use crate::agent_manager::EventRegistry;
use crate::client_core::ClientWeakHandle;
use crate::marketplace::{ use crate::marketplace::{
MarketplaceFolderRole, MarketplaceListingStatus, MarketplaceValidationFlags, MarketplaceFolderRole, MarketplaceListingStatus, MarketplaceValidationFlags,
}; };
@@ -291,8 +292,13 @@ impl MarketplaceListingsSyncedEventArgs {
} }
} }
enum MarketplaceClient {
Standalone(GridClient),
ClientOwned(ClientWeakHandle),
}
struct MarketplaceState { struct MarketplaceState {
client: GridClient, client: RwLock<MarketplaceClient>,
by_id: RwLock<HashMap<i32, MarketplaceListing>>, by_id: RwLock<HashMap<i32, MarketplaceListing>>,
by_folder: RwLock<HashMap<UUID, MarketplaceListing>>, by_folder: RwLock<HashMap<UUID, MarketplaceListing>>,
errors: EventRegistry<MarketplaceErrorEventArgs>, errors: EventRegistry<MarketplaceErrorEventArgs>,
@@ -306,15 +312,36 @@ pub struct MarketplaceManager(Arc<MarketplaceState>);
impl MarketplaceManager { impl MarketplaceManager {
pub fn new(client: GridClient) -> Result<Self, Error> { pub fn new(client: GridClient) -> Result<Self, Error> {
Ok(Self(Arc::new(MarketplaceState { Ok(Self::with_client(MarketplaceClient::Standalone(client)))
client, }
fn with_client(client: MarketplaceClient) -> Self {
Self(Arc::new(MarketplaceState {
client: RwLock::new(client),
by_id: RwLock::new(HashMap::new()), by_id: RwLock::new(HashMap::new()),
by_folder: RwLock::new(HashMap::new()), by_folder: RwLock::new(HashMap::new()),
errors: EventRegistry::default(), errors: EventRegistry::default(),
changed: EventRegistry::default(), changed: EventRegistry::default(),
synced: EventRegistry::default(), synced: EventRegistry::default(),
live_mutations: AtomicBool::new(false), live_mutations: AtomicBool::new(false),
}))) }))
}
pub(crate) fn native_new_client_owned(client: &GridClient) -> Self {
Self::with_client(MarketplaceClient::ClientOwned(client.native_weak_handle()))
}
pub(crate) fn native_bind_to_client(&self, client: &GridClient) {
*write(&self.0.client) = MarketplaceClient::ClientOwned(client.native_weak_handle());
}
fn client(&self) -> Result<GridClient, Error> {
match &*read(&self.0.client) {
MarketplaceClient::Standalone(client) => Ok(client.clone()),
MarketplaceClient::ClientOwned(client) => {
client.upgrade().ok_or(Error::InvalidOperation)
}
}
} }
/// Explicitly enables mutation requests to non-test capability hosts. /// Explicitly enables mutation requests to non-test capability hosts.
@@ -366,8 +393,7 @@ impl MarketplaceManager {
async fn base_uri(&self, token: &CancellationToken) -> Result<Uri, Error> { async fn base_uri(&self, token: &CancellationToken) -> Result<Uri, Error> {
token.throw_if_cancellation_requested()?; token.throw_if_cancellation_requested()?;
let simulator = self let simulator = self
.0 .client()?
.client
.native_network()? .native_network()?
.native_current_sim() .native_current_sim()
.ok_or(Error::InvalidOperation)?; .ok_or(Error::InvalidOperation)?;
@@ -412,7 +438,7 @@ impl MarketplaceManager {
return Err(Error::InvalidOperation); return Err(Error::InvalidOperation);
} }
let uri = Uri(format!("{}/{}", base.0.trim_end_matches('/'), path)); let uri = Uri(format!("{}/{}", base.0.trim_end_matches('/'), path));
let client = self.0.client.native_http_caps_client(); let client = self.client()?.native_http_caps_client();
match method { match method {
"GET" => client.get(uri, token, None).await, "GET" => client.get(uri, token, None).await,
"POST" => { "POST" => {

View File

@@ -10,15 +10,15 @@ use libremetaverse::packets::{
SimStatsPacket, SimStatsPacketStatBlock, StartPingCheckPacket, UseCircuitCodePacket, SimStatsPacket, SimStatsPacketStatBlock, StartPingCheckPacket, UseCircuitCodePacket,
}; };
use libremetaverse::{ use libremetaverse::{
CapsEventDictionary, CapsEventQueueCallback, GridClient, Helpers, HttpCapsClient, LoginState, CapsEventDictionary, CapsEventQueueCallback, GridClient, Helpers, HttpCapsClient,
LoginStatus, NetworkManager, NetworkManagerDisconnectType, NetworkManagerLoginResponseCallback, InventoryItem, LoginState, LoginStatus, NetworkManager, NetworkManagerDisconnectType,
PacketEventDictionary, Simulator, NetworkManagerLoginResponseCallback, PacketEventDictionary, Simulator,
}; };
use libremetaverse_structured_data::{OSD, OSDParser}; use libremetaverse_structured_data::{OSD, OSDParser};
use libremetaverse_types::compat::{ use libremetaverse_types::compat::{
CancellationTokenSource, EventHandler, HttpMessageHandler, HttpRequest, HttpResponse, Uri, CancellationTokenSource, EventHandler, HttpMessageHandler, HttpRequest, HttpResponse, Uri,
}; };
use libremetaverse_types::{UUID, Utils}; use libremetaverse_types::{AssetType, Error, UUID, Utils};
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}; use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
@@ -1843,12 +1843,17 @@ async fn offline_fake_grid_drives_the_complete_networking_lifecycle_without_task
client.settings().agent_settings_mut().multiple_sims = true; client.settings().agent_settings_mut().multiple_sims = true;
client.set_http_caps_client(HttpCapsClient::new(handler).unwrap()); client.set_http_caps_client(HttpCapsClient::new(handler).unwrap());
let manager = client.network(); let manager = client.network();
let manager_event_order = Arc::new(Mutex::new(Vec::new()));
let (queue_sender, queue_receiver) = channel(); let (queue_sender, queue_receiver) = channel();
let queue_order = Arc::clone(&manager_event_order);
let _queue_guard = manager.subscribe_event_queue_running(Arc::new(move |args| { let _queue_guard = manager.subscribe_event_queue_running(Arc::new(move |args| {
queue_order.lock().unwrap().push("event_queue_running");
queue_sender.send(args.simulator().handle).unwrap(); queue_sender.send(args.simulator().handle).unwrap();
})); }));
let (logout_sender, logout_receiver) = channel(); let (logout_sender, logout_receiver) = channel();
let logout_order = Arc::clone(&manager_event_order);
let _logout_guard = manager.subscribe_logged_out(Arc::new(move |args| { let _logout_guard = manager.subscribe_logged_out(Arc::new(move |args| {
logout_order.lock().unwrap().push("logged_out");
logout_sender.send(args.inventory_items).unwrap(); logout_sender.send(args.inventory_items).unwrap();
})); }));
@@ -1929,6 +1934,77 @@ async fn offline_fake_grid_drives_the_complete_networking_lifecycle_without_task
assert_eq!(asset, b"fake-grid-asset"); assert_eq!(asset, b"fake-grid-asset");
assert_eq!(downloads.active_download_count(), 0); assert_eq!(downloads.active_download_count(), 0);
// Cross-manager construction is deliberately performed only after the
// authenticated simulator and seed capabilities are available. Every
// manager below shares the same client runtime and must preserve its
// local store when accessed again through GridClient.
{
let agent = client.self_();
assert_eq!(
agent.agent_id(),
UUID::new_with_string("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa".into()).unwrap()
);
assert_eq!(
agent.session_id(),
UUID::new_with_string("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb".into()).unwrap()
);
}
let inventory_id =
UUID::new_with_string("44444444-4444-4444-4444-444444444444".into()).unwrap();
let inventory = client.inventory();
let mut store = inventory.store().expect("authenticated inventory store");
let mut item = InventoryItem::new_with_uuid(inventory_id).unwrap();
item.base.set_name("fake-grid inventory marker".into());
store.set_item(inventory_id, item.base);
assert!(
client
.inventory()
.store()
.unwrap()
.contains_with_uuid(inventory_id)
.unwrap()
);
assert!(!client.ais_client().is_available());
assert_eq!(client.appearance().last_update_received_cof_version(), -1);
assert!(
client
.assets()
.create_asset_wrapper(AssetType::Texture)
.is_ok()
);
let _avatars = client.avatars();
assert!(client.animesh().get_player(UUID::zero()).unwrap().is_none());
client.objects().request_object(first.clone(), 1).unwrap();
assert!(client.parcels().current_parcel().is_none());
assert!(client.grid().regions_read_only().is_empty());
assert!(client.friends().friend_list().is_empty());
assert_eq!(client.groups().current_group_count(), -1);
assert!(client.marketplace().listings_by_id().is_empty());
let _terrain = client.terrain();
let _environment = client.environment();
let _sound = client.sound();
let _estate = client.estate();
let cancelled_search = CancellationTokenSource::new();
cancelled_search.cancel();
assert!(matches!(
client
.directory()
.search_people("Ada Lovelace".into(), Some(cancelled_search.token()),),
Err(Error::Cancelled)
));
let (marketplace_error_sender, marketplace_error_receiver) = channel();
let marketplace_order = Arc::clone(&manager_event_order);
let _marketplace_error_guard = client.marketplace().subscribe_error(Arc::new(move |_| {
marketplace_order.lock().unwrap().push("marketplace_error");
marketplace_error_sender.send(()).unwrap();
}));
client.marketplace().fetch_listings(None).await.unwrap();
marketplace_error_receiver
.recv_timeout(Duration::from_secs(1))
.unwrap();
second_server.commands.send(ServerCommand::Handoff).unwrap(); second_server.commands.send(ServerCommand::Handoff).unwrap();
manager manager
.connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32(
@@ -2007,6 +2083,13 @@ async fn offline_fake_grid_drives_the_complete_networking_lifecycle_without_task
assert!(downloads.is_disposed()); assert!(downloads.is_disposed());
assert!(!downloads.dispatcher_running()); assert!(!downloads.dispatcher_running());
assert_eq!(downloads.active_download_count(), 0); assert_eq!(downloads.active_download_count(), 0);
assert_eq!(
*manager_event_order.lock().unwrap(),
["event_queue_running", "marketplace_error", "logged_out"]
);
let client_cancellation = client.cancellation_token();
client.dispose_with_method().unwrap();
assert!(client_cancellation.is_cancellation_requested());
let uris = request_uris.lock().unwrap(); let uris = request_uris.lock().unwrap();
assert!(uris.iter().any(|uri| uri.contains("/login"))); assert!(uris.iter().any(|uri| uri.contains("/login")));
assert!(uris.iter().any(|uri| uri.contains("/seed"))); assert!(uris.iter().any(|uri| uri.contains("/seed")));

View File

@@ -0,0 +1,48 @@
# World milestone integration gate
Milestone 09 closes on native Rust behavior, not on catalog coverage alone. The
reproducible focused suite is `python3 tools/test_milestone_09.py`; its target
list contains the world-owned library tests, the complete fake-grid lifecycle,
and the translated compatibility targets for agent, inventory, assets,
appearance, avatars, objects, land, discovery, social, estate, experience, and
marketplace behavior. Live tests remain catalogued and reviewed but skip unless
their existing credentials and the explicit `RUN_LIVE_TESTS=1` opt-in are
supplied. The offline milestone runner always clears that opt-in.
## Cross-manager fake grid
The loopback scenario logs in through injected HTTP and UDP endpoints, waits
for seed capabilities and the event queue, then performs a simulator handoff.
While authenticated it constructs every client-owned world manager, verifies
agent/session identity, reconciles an inventory marker through the shared
store, checks AIS availability, creates an asset wrapper, observes appearance
and animesh state, requests an object, and inspects parcel, grid, friend, group,
and marketplace caches. A cancelled directory query proves that caller
cancellation wins before wire work. A marketplace capability error emitted
through a second `GridClient.Marketplace` lookup proves that the manager cache
and event registry are shared.
Manager events are asserted in deterministic order: event-queue readiness,
the marketplace error, then logout. Capability downloads are bounded by the
existing HTTP and download-manager policies. Diagnostic formatting is checked
against login and capability secrets.
## Cancellation and teardown
Logout cancels the event queue, disconnects every simulator, leaves no pending
logout task, and stops the download dispatcher. Client disposal then cancels
the root token and releases every cached manager, including assets,
appearance, avatars, animesh, land, discovery, social, estate, and marketplace
state. Inventory has an empty shared store before login so offline fixtures and
programs can stage deterministic data; a successful login atomically replaces
it with the authenticated hierarchy.
## Audit and program boundary
`python3 tools/check_milestone_09.py` rejects stubs in all milestone-owned
sources, missing native type/member mappings, unreviewed related parity cases,
missing fake-grid evidence, and drift in the nine catalogued Rust program
targets. The program ports remain a later milestone, but their required world
APIs and target inventory are now auditable. Live actions remain opt-in:
uploads, marketplace changes, estate/parcel mutations, currency spending, and
operations affecting other users are never part of this offline gate.

View File

@@ -1,6 +1,7 @@
use std::path::Path; use std::path::Path;
const KEYS: [&str; 3] = ["GRID_USER", "GRID_PASSWORD", "GRID_LOGIN_URL"]; const KEYS: [&str; 3] = ["GRID_USER", "GRID_PASSWORD", "GRID_LOGIN_URL"];
const LIVE_OPT_IN: &str = "RUN_LIVE_TESTS";
fn dotenv_has(path: &Path, key: &str) -> bool { fn dotenv_has(path: &Path, key: &str) -> bool {
std::fs::read_to_string(path).is_ok_and(|contents| { std::fs::read_to_string(path).is_ok_and(|contents| {
@@ -22,9 +23,19 @@ fn main() {
for key in KEYS { for key in KEYS {
println!("cargo:rerun-if-env-changed={key}"); println!("cargo:rerun-if-env-changed={key}");
} }
if KEYS.iter().all(|key| { println!("cargo:rerun-if-env-changed={LIVE_OPT_IN}");
std::env::var(key).is_ok_and(|value| !value.trim().is_empty()) || dotenv_has(&dotenv, key) let opted_in = std::env::var(LIVE_OPT_IN).is_ok_and(|value| {
}) { matches!(
value.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes"
)
});
if opted_in
&& KEYS.iter().all(|key| {
std::env::var(key).is_ok_and(|value| !value.trim().is_empty())
|| dotenv_has(&dotenv, key)
})
{
println!("cargo:rustc-cfg=live_grid_credentials"); println!("cargo:rustc-cfg=live_grid_credentials");
} }
} }

View File

@@ -734,8 +734,10 @@ fn thumbnail_known_local_folder_sends_category_id() {
let mut folder = InventoryFolder::new(folder_id).expect("InventoryFolder constructor"); let mut folder = InventoryFolder::new(folder_id).expect("InventoryFolder constructor");
folder.base.set_name("Test Folder".into()); folder.base.set_name("Test Folder".into());
folder.base.set_parent_uuid(UUID::zero()); folder.base.set_parent_uuid(UUID::zero());
let mut store = client.inventory().store().expect("fixture inventory store"); let store = client.inventory().store().expect("fixture inventory store");
store.set_item(folder_id, folder.base); store
.update_node_for(&folder)
.expect("install fixture inventory folder");
let result = block_on(client.inventory().upload_thumbnail( let result = block_on(client.inventory().upload_thumbnail(
folder_id, folder_id,
UUID::zero(), UUID::zero(),

View File

@@ -95,19 +95,22 @@ fn avatar_facing_flows_have_typed_callable_signatures() {
} }
#[test] #[test]
fn avatar_facing_constructors_fail_with_catalog_ids() { fn avatar_facing_constructors_preserve_native_defaults_and_catalog_errors() {
assert_eq!( let download = AssetDownload::new().expect("AssetDownload constructor");
member_id(AssetDownload::new()), assert_eq!(download.asset_id, UUID::zero());
"M:LibreMetaverse.AssetDownload.#ctor" assert_eq!(download.next_packet, 0);
); assert!(download.out_of_order_packets.read().unwrap().is_empty());
assert_eq!(
member_id(AssetUpload::new()), let upload = AssetUpload::new().expect("AssetUpload constructor");
"M:LibreMetaverse.AssetUpload.#ctor" assert_eq!(upload.asset_id, UUID::zero());
); assert_eq!(upload.packet_num, 0);
assert_eq!( assert_eq!(upload.xfer_id, 0);
member_id(AppearanceManagerWearableData::new()),
"M:LibreMetaverse.AppearanceManager.WearableData.#ctor" let wearable = AppearanceManagerWearableData::new().expect("WearableData constructor");
); assert_eq!(wearable.asset_id, UUID::zero());
assert_eq!(wearable.item_id, UUID::zero());
assert!(wearable.asset.is_none());
assert_eq!( assert_eq!(
member_id(InventoryException::new_with_constructor()), member_id(InventoryException::new_with_constructor()),
"M:LibreMetaverse.InventoryException.#ctor" "M:LibreMetaverse.InventoryException.#ctor"

245
tools/check_milestone_09.py Normal file
View File

@@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""Audit the complete native world-services milestone boundary."""
from __future__ import annotations
import json
from pathlib import Path
import re
import generate_api_shims
ROOT = Path(__file__).resolve().parents[1]
SOURCE_ROOT = ROOT / "crates" / "libremetaverse" / "src"
OWNED_SOURCES = (
"agent_manager.rs",
"agent_messages.rs",
"agent_movement.rs",
"animation.rs",
"animesh.rs",
"animesh_skinning.rs",
"appearance_baker.rs",
"appearance_manager.rs",
"asset_archive.rs",
"asset_cache.rs",
"asset_manager.rs",
"asset_material.rs",
"asset_models.rs",
"avatar_manager.rs",
"avatar_rig.rs",
"baking_texture_provider.rs",
"current_outfit.rs",
"directory_manager.rs",
"environment_manager.rs",
"estate_tools.rs",
"friends_manager.rs",
"gesture.rs",
"gltf.rs",
"grid_manager.rs",
"group_manager.rs",
"initial_outfit.rs",
"interest_list.rs",
"inventory.rs",
"inventory_ais.rs",
"inventory_manager.rs",
"marketplace.rs",
"model_workflow.rs",
"object_manager.rs",
"object_material.rs",
"object_model.rs",
"parcel_manager.rs",
"slurl.rs",
"sound_manager.rs",
"terrain_codec.rs",
"terrain_manager.rs",
"transfers.rs",
"user_report.rs",
)
REQUIRED_NATIVE_TYPES = {
"T:LibreMetaverse.AgentManager",
"T:LibreMetaverse.AgentManager.AgentMovement",
"T:LibreMetaverse.Animesh.AnimeshManager",
"T:LibreMetaverse.AppearanceManager",
"T:LibreMetaverse.AssetManager",
"T:LibreMetaverse.AvatarManager",
"T:LibreMetaverse.DirectoryManager",
"T:LibreMetaverse.EnvironmentManager",
"T:LibreMetaverse.EstateTools",
"T:LibreMetaverse.FriendsManager",
"T:LibreMetaverse.GridManager",
"T:LibreMetaverse.GroupManager",
"T:LibreMetaverse.InventoryAISClient",
"T:LibreMetaverse.InventoryManager",
"T:LibreMetaverse.Marketplace.MarketplaceManager",
"T:LibreMetaverse.ObjectManager",
"T:LibreMetaverse.ParcelManager",
"T:LibreMetaverse.SoundManager",
"T:LibreMetaverse.TerrainManager",
}
REQUIRED_NATIVE_MEMBERS = {
"M:LibreMetaverse.InventoryManager.UploadThumbnailAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})",
"P:LibreMetaverse.GridClient.Marketplace",
"P:LibreMetaverse.GridClient.Marketplace#set",
}
RELATED_UPSTREAM_SOURCES = {
"LibreMetaverse.Tests/AnimationTrackTests.cs",
"LibreMetaverse.Tests/AnimeshPlayerTests.cs",
"LibreMetaverse.Tests/AnimeshSkinningTests.cs",
"LibreMetaverse.Tests/AppearanceLiveTests.cs",
"LibreMetaverse.Tests/AppearanceManagerTests.cs",
"LibreMetaverse.Tests/AppearanceTests.cs",
"LibreMetaverse.Tests/AssetMaterialOverrideTests.cs",
"LibreMetaverse.Tests/AvatarManagerTests.cs",
"LibreMetaverse.Tests/BakerTests.cs",
"LibreMetaverse.Tests/BatchLinkTests.cs",
"LibreMetaverse.Tests/CurrentOutfitFolderTests.cs",
"LibreMetaverse.Tests/EnvironmentManagerTests.cs",
"LibreMetaverse.Tests/EstateExperienceReplyTests.cs",
"LibreMetaverse.Tests/EstateToolsTests.cs",
"LibreMetaverse.Tests/ExperienceListAndRegionMessageTests.cs",
"LibreMetaverse.Tests/ExperiencePreferencesMessageTests.cs",
"LibreMetaverse.Tests/GLTFMaterialOverrideCapTests.cs",
"LibreMetaverse.Tests/GLTFMaterialUploadTests.cs",
"LibreMetaverse.Tests/GltfDocumentTests.cs",
"LibreMetaverse.Tests/InventoryAISClientTests.cs",
"LibreMetaverse.Tests/InventoryFetchTests.cs",
"LibreMetaverse.Tests/InventoryManagerTests.cs",
"LibreMetaverse.Tests/InventoryTests.cs",
"LibreMetaverse.Tests/LibraryInventoryTests.cs",
"LibreMetaverse.Tests/MarketplaceFolderClassifierTests.cs",
"LibreMetaverse.Tests/MarketplaceManagerTests.cs",
"LibreMetaverse.Tests/MisclassifiedLinkRegressionTests.cs",
"LibreMetaverse.Tests/OarFileTerrainTests.cs",
"LibreMetaverse.Tests/OfflineMessagesTests.cs",
"LibreMetaverse.Tests/PrimObjectTests.cs",
"LibreMetaverse.Tests/PrimitiveTests.cs",
"LibreMetaverse.Tests/ProductInfoRequestMessageTests.cs",
"LibreMetaverse.Tests/QueryExperiencesOnParcelTests.cs",
"LibreMetaverse.Tests/RegionScheduleTests.cs",
"LibreMetaverse.Tests/RequestTaskInventoryCapTests.cs",
"LibreMetaverse.Tests/SendPostcardTests.cs",
"LibreMetaverse.Tests/SimConsoleTests.cs",
"LibreMetaverse.Tests/SlurlParserTests.cs",
"LibreMetaverse.Tests/StartAuctionTests.cs",
"LibreMetaverse.Tests/TerrainMaterialOverrideTests.cs",
"LibreMetaverse.Tests/TextureEntryTests.cs",
"LibreMetaverse.Tests/UploadThumbnailTests.cs",
}
STUB_RE = re.compile(r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(")
def require_markers(path: Path, markers: tuple[str, ...]) -> None:
text = path.read_text()
missing = [marker for marker in markers if marker not in text]
if missing:
raise SystemExit(f"{path}: audit evidence missing: " + ", ".join(missing))
def check_owned_sources() -> None:
offenders = [
name for name in OWNED_SOURCES if STUB_RE.search((SOURCE_ROOT / name).read_text())
]
if offenders:
raise SystemExit("milestone 09 owned Rust stubs remain: " + ", ".join(offenders))
def check_native_mappings() -> None:
native = set(generate_api_shims.NATIVE_TYPES) | set(generate_api_shims.NATIVE_DECLARATIONS)
missing_types = sorted(REQUIRED_NATIVE_TYPES - native)
if missing_types:
raise SystemExit("milestone 09 native types missing: " + ", ".join(missing_types))
missing_members = sorted(
REQUIRED_NATIVE_MEMBERS - set(generate_api_shims.NATIVE_MEMBER_BODIES)
)
if missing_members:
raise SystemExit("milestone 09 native members missing: " + ", ".join(missing_members))
def check_parity_catalog() -> None:
catalog = json.loads((ROOT / "tests" / "upstream-tests.json").read_text())
related = [test for test in catalog["tests"] if test["source"] in RELATED_UPSTREAM_SOURCES]
present_sources = {test["source"] for test in related}
missing_sources = sorted(RELATED_UPSTREAM_SOURCES - present_sources)
if missing_sources:
raise SystemExit("milestone 09 parity sources missing: " + ", ".join(missing_sources))
bad = [
test["id"]
for test in related
if test["status"] not in {"translated", "ignored-live"}
or test.get("semantic_review") != "reviewed"
]
if bad:
raise SystemExit("milestone 09 parity cases are not reviewed: " + ", ".join(bad))
missing_rust = sorted(
{
test["rust_file"]
for test in related
if test["status"] == "translated"
and not (ROOT / test["rust_file"]).is_file()
}
)
if missing_rust:
raise SystemExit("milestone 09 translated files missing: " + ", ".join(missing_rust))
def check_fake_grid_gate() -> None:
require_markers(
ROOT / "crates" / "libremetaverse" / "tests" / "network_manager.rs",
(
"offline_fake_grid_drives_the_complete_networking_lifecycle_without_task_leaks",
"event_queue_running",
"fake-grid inventory marker",
"create_asset_wrapper",
"get_player",
"request_object",
"current_parcel",
"friend_list",
"current_group_count",
"marketplace_error",
"search_people",
"pending_logout_tasks",
"dispatcher_running",
"is_cancellation_requested",
"seed-secret",
"queue-secret",
"asset-secret",
),
)
def check_docs_and_program_prerequisites() -> None:
require_markers(
ROOT / "docs" / "world-milestone-gate.md",
(
"Cross-manager fake grid",
"Cancellation and teardown",
"Live actions remain opt-in",
"tools/test_milestone_09.py",
"RUN_LIVE_TESTS=1",
),
)
projects = json.loads((ROOT / "programs" / "upstream-programs.json").read_text())[
"projects"
]
if len(projects) != 9:
raise SystemExit("milestone 09 expected all nine program prerequisite inventories")
manifest = (ROOT / "programs" / "Cargo.toml").read_text()
if manifest.count("[[bin]]") != 9:
raise SystemExit("milestone 09 expected all nine Rust program targets")
def main() -> None:
check_owned_sources()
check_native_mappings()
check_parity_catalog()
check_fake_grid_gate()
check_docs_and_program_prerequisites()
print(
"milestone 09 audit: owned world sources contain no stubs; native mappings, "
"reviewed parity cases, fake-grid integration, docs, and program prerequisites are present"
)
if __name__ == "__main__":
main()

View File

@@ -510,7 +510,8 @@ NATIVE_MEMBER_BODIES = {
"crate::asset_archive::asset_type_to_extension()", "crate::asset_archive::asset_type_to_extension()",
"F:LibreMetaverse.Assets.ArchiveConstants.EXTENSION_TO_ASSET_TYPE": "F:LibreMetaverse.Assets.ArchiveConstants.EXTENSION_TO_ASSET_TYPE":
"crate::asset_archive::extension_to_asset_type()", "crate::asset_archive::extension_to_asset_type()",
"M:LibreMetaverse.Assets.ArchiveConstants.#ctor": "Ok(Self)", "M:LibreMetaverse.Assets.ArchiveConstants.#ctor":
"/* native zero-sized constants namespace */ Ok(Self)",
"M:LibreMetaverse.Assets.OarFile.ClearAssetFolder(System.String)": "M:LibreMetaverse.Assets.OarFile.ClearAssetFolder(System.String)":
"crate::asset_archive::clear_asset_folder(assets_path)", "crate::asset_archive::clear_asset_folder(assets_path)",
"M:LibreMetaverse.Assets.OarFile.PackageArchive(System.String,System.String)": "M:LibreMetaverse.Assets.OarFile.PackageArchive(System.String,System.String)":
@@ -525,8 +526,12 @@ NATIVE_MEMBER_BODIES = {
"crate::asset_archive::save_terrain(sim, terrain_path)", "crate::asset_archive::save_terrain(sim, terrain_path)",
"M:LibreMetaverse.Assets.OarFile.UnpackageArchive(System.String,LibreMetaverse.Assets.OarFile.AssetLoadedCallback,LibreMetaverse.Assets.OarFile.TerrainLoadedCallback,LibreMetaverse.Assets.OarFile.SceneObjectLoadedCallback,LibreMetaverse.Assets.OarFile.SettingsLoadedCallback)": "M:LibreMetaverse.Assets.OarFile.UnpackageArchive(System.String,LibreMetaverse.Assets.OarFile.AssetLoadedCallback,LibreMetaverse.Assets.OarFile.TerrainLoadedCallback,LibreMetaverse.Assets.OarFile.SceneObjectLoadedCallback,LibreMetaverse.Assets.OarFile.SettingsLoadedCallback)":
"crate::asset_archive::unpackage_archive(filename, asset_callback, terrain_callback, object_callback, settings_callback)", "crate::asset_archive::unpackage_archive(filename, asset_callback, terrain_callback, object_callback, settings_callback)",
"M:LibreMetaverse.InventoryManager.UploadThumbnailAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})":
"self.native_upload_thumbnail(inventory_id, task_id, j2c_image_data, cancellation_token, progress).await",
"P:LibreMetaverse.GridClient.Estate": "self.native_estate().unwrap_or_else(|_| panic!(\"failed to construct EstateTools\"))", "P:LibreMetaverse.GridClient.Estate": "self.native_estate().unwrap_or_else(|_| panic!(\"failed to construct EstateTools\"))",
"P:LibreMetaverse.GridClient.Estate#set": "self.native_set_estate(value)", "P:LibreMetaverse.GridClient.Estate#set": "self.native_set_estate(value)",
"P:LibreMetaverse.GridClient.Marketplace": "self.native_marketplace()",
"P:LibreMetaverse.GridClient.Marketplace#set": "self.native_set_marketplace(value)",
"P:LibreMetaverse.GridClient.Environment": "P:LibreMetaverse.GridClient.Environment":
"self.native_environment().unwrap_or_else(|_| panic!(\"failed to construct EnvironmentManager\"))", "self.native_environment().unwrap_or_else(|_| panic!(\"failed to construct EnvironmentManager\"))",
"P:LibreMetaverse.GridClient.Environment#set": "self.native_set_environment(value)", "P:LibreMetaverse.GridClient.Environment#set": "self.native_set_environment(value)",

View File

@@ -974,6 +974,7 @@ def validate_generated_shims() -> None:
if ( if (
"native client-core implementation" in body "native client-core implementation" in body
or "native network implementation" in body or "native network implementation" in body
or "native zero-sized constants namespace" in body
): ):
continue continue
uses_standardized_or_native_path = any( uses_standardized_or_native_path = any(
@@ -1008,6 +1009,7 @@ def validate_generated_shims() -> None:
"crate::inventory::", "crate::inventory::",
"crate::inventory_ais::", "crate::inventory_ais::",
"crate::inventory_manager::", "crate::inventory_manager::",
"crate::asset_archive::",
"crate::initial_outfit::", "crate::initial_outfit::",
"crate::current_outfit::", "crate::current_outfit::",
"crate::message_codec::", "crate::message_codec::",

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Run only the Rust tests owned by the world-services milestone."""
from __future__ import annotations
import os
from pathlib import Path
import subprocess
ROOT = Path(__file__).resolve().parents[1]
COMPAT_TARGETS = (
"appearance_live_semantics",
"appearance_semantics",
"appearance_visual_semantics",
"archive_model_semantics",
"asset_capability_semantics",
"asset_document_semantics",
"avatar_manager_shims",
"current_outfit_folder_semantics",
"current_outfit_policy_semantics",
"inventory_ais_semantics",
"inventory_live_semantics",
"inventory_manager_semantics",
"inventory_store_semantics",
"marketplace_classifier_semantics",
"marketplace_manager_semantics",
"misclassified_link_semantics",
"slurl_semantics",
"social_constructor_semantics",
"social_message_semantics",
"task_inventory_semantics",
"user_report_semantics",
"wire_shims",
"world_capability_semantics",
"world_constructor_semantics",
"world_live_semantics",
"world_manager_shims",
"world_message_semantics",
"world_object_semantics",
"world_packet_semantics",
)
def run(command: list[str]) -> None:
print("+", " ".join(command), flush=True)
subprocess.run(command, cwd=ROOT, env=ENV, check=True)
ENV = os.environ.copy()
ENV.setdefault("CARGO_BUILD_JOBS", "1")
ENV.pop("RUN_LIVE_TESTS", None)
run(["cargo", "test", "-p", "libremetaverse", "--lib", "--locked"])
run(
[
"cargo",
"test",
"-p",
"libremetaverse",
"--test",
"network_manager",
"offline_fake_grid_drives_the_complete_networking_lifecycle_without_task_leaks",
"--locked",
]
)
compat = ["cargo", "test", "-p", "libremetaverse-compat-tests"]
for target in COMPAT_TARGETS:
compat.extend(("--test", target))
compat.append("--locked")
run(compat)