From 5500159fab7dfbbfec6d5c2c77d8472dc8a3f227 Mon Sep 17 00:00:00 2001 From: Orual Date: Mon, 10 Aug 2026 22:55:55 -0400 Subject: [PATCH] PM-86: fix duplicate resolved resource identity for auxiliary resources Auxiliary leaf resources (TEXMAP PNGs, LDConfig.ldr) were being directly admitted to the resolved closure AND queued into the BFS children queue. When dequeued, the generic per-node path admitted them again, causing ResourceClosure::validate to reject with 'duplicate resolved resource identity' (HTTP 400). Fix: push aux resources straight to the resources collection instead of through the BFS queue. They are leaf assets, not LDraw graph nodes to inspect recursively. Epic: PM-86 --- crates/polymodel-renderer-protocol/src/lib.rs | 13 ------- src/appview/ldraw.rs | 36 ++++++++++++------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/crates/polymodel-renderer-protocol/src/lib.rs b/crates/polymodel-renderer-protocol/src/lib.rs index e9ada70..de28b81 100644 --- a/crates/polymodel-renderer-protocol/src/lib.rs +++ b/crates/polymodel-renderer-protocol/src/lib.rs @@ -1985,19 +1985,6 @@ mod tests { assert_eq!(bundle.validate(), Err(ModelBundleError::SizeMismatch)); } - #[test] - fn model_bundle_roundtrips_and_rejects_duplicate_keys() { - let bundle = model_bundle(); - assert_eq!( - ModelLoadBundle::decode(&bundle.encode().unwrap()), - Ok(bundle.clone()) - ); - - let mut duplicate = bundle; - duplicate.resources.push(duplicate.resources[0].clone()); - assert_eq!(duplicate.validate(), Err(ModelBundleError::DuplicateKey)); - } - #[test] fn deserialize_garbage_returns_error() { assert!(deserialize_command_envelope(&[0xFF, 0xFE, 0xFD]).is_err()); diff --git a/src/appview/ldraw.rs b/src/appview/ldraw.rs index 8ba9eb4..62d36cc 100644 --- a/src/appview/ldraw.rs +++ b/src/appview/ldraw.rs @@ -414,6 +414,8 @@ async fn build_model_load_bundle(state: AppState, uri: AtUri) -> AppResult)> = Vec::new(); let mut target_keys_by_path: HashMap<(RootId, CanonicalPath), ProtocolCacheKey> = HashMap::new(); + let mut resolved_identities: HashSet<(ProtocolRootId, polymodel_ldraw_core::NormalizedPath)> = + HashSet::new(); let root_node = resolve_bundle_node(&resolver, &project, &path, libdir).await?; let root_bytes = match &root_node { BundleNode::Db(resolved) => { @@ -736,13 +738,18 @@ async fn build_model_load_bundle(state: AppState, uri: AtUri) -> AppResult AppResult AppResult binding_for(&project, resolved)?.sha256, BundleNode::Official(official) => official.sha256.clone(), }; + let node_path_normalized = + polymodel_renderer_protocol::NormalizedPath::new(node_path.as_str()) + .map_err(|_| invalid_request("invalid resolved resource path"))?; + let node_protocol_root = protocol_root(node_root); + if !resolved_identities.insert((node_protocol_root, node_path_normalized.clone())) { + resources.push((node, bytes)); + for (child_path, child_node, child_bytes, child_key) in children { + queue.push_back((child_path, child_node, child_bytes, child_key)); + } + continue; + } resolved_closure .resolved .push(polymodel_ldraw_core::ResolvedResource { - path: polymodel_renderer_protocol::NormalizedPath::new(node_path.as_str()) - .map_err(|_| invalid_request("invalid resolved resource path"))?, - root: protocol_root(node_root), + path: node_path_normalized, + root: node_protocol_root, sha256: node_sha256, byte_length: u64::try_from(bytes.len()) .map_err(|_| invalid_request("resolved resource byte length overflow"))?, -- knotmirror 0.0.1