feat: power reference site with in-process WIP WASM

Add the public-document Worldspace, Rust Client/Host bridge, and WIP-backed tree and document rendering while preserving static fallback and navigation.

Pin the Rust source and build tools, verify resource and trap boundaries, and cover dev and preview loading including same-origin generated module URLs.
This commit is contained in:
2026-10-16 05:59:46 +09:00
parent ad08037f4f
commit c480b67301
26 changed files with 2719 additions and 63 deletions
+437
View File
@@ -0,0 +1,437 @@
use std::collections::BTreeMap;
use wip_interface::WipType;
use wip_site_worldspace::wip_protocol::{
CallOperationRequest, FetchInterfaceRequest, InterfaceReference, InterfaceTarget,
ObserveRequest, ProtocolErrorCode, Target, Value,
};
use wip_site_worldspace::{
DocumentContent, DocumentNode, MAX_DEPTH, MAX_NODES, Worldspace, WorldspaceData,
};
fn node(path: &str, parent: Option<&str>, display: &str, href: &str) -> DocumentNode {
DocumentNode {
path: path.into(),
name: if path == "/" {
""
} else {
path.rsplit('/').next().unwrap()
}
.into(),
display_path: display.into(),
parent: parent.map(str::to_owned),
title: format!("Title {display}"),
href: href.into(),
html: format!("<h1>{display}</h1>"),
}
}
fn data() -> WorldspaceData {
WorldspaceData {
nodes: vec![
node("/", None, "/", "/"),
node("/book", Some("/"), "/book", "/book/"),
node(
"/book/1-chapter.md",
Some("/book"),
"/1-chapter.md",
"/book/1-chapter/",
),
node(
"/book/1-chapter.md/1.1-section.md",
Some("/book/1-chapter.md"),
"/1.1-section.md",
"/book/1.1-section/",
),
// This intentional order differs from lexical order.
node(
"/book/10-last.md",
Some("/book"),
"/10-last.md",
"/book/10-last/",
),
node(
"/book/2-second.md",
Some("/book"),
"/2-second.md",
"/book/2-second/",
),
],
}
}
fn read(path: &str) -> CallOperationRequest {
CallOperationRequest {
target: Target {
path: path.into(),
validator: None,
},
interface: InterfaceTarget {
reference: Worldspace::document_interface(),
scope_ref: None,
validator: None,
},
operation: "read".into(),
arguments: BTreeMap::new(),
}
}
#[test]
fn all_three_interactions_use_the_published_document_interface() {
let world = Worldspace::new(data()).unwrap();
let request = ObserveRequest {
path: "/book/1-chapter.md".into(),
depth: 1,
};
let observation = world.observe(request.clone()).unwrap();
request.validate_response(&observation).unwrap();
assert_eq!(
observation.object.interfaces,
vec![Worldspace::document_interface()]
);
assert_eq!(
observation.children.unwrap()[0].object.name,
"1.1-section.md"
);
let fetch = FetchInterfaceRequest {
interface: Worldspace::document_interface(),
};
let interface = world.fetch_interface(fetch.clone()).unwrap();
interface.validate_for(&fetch).unwrap();
assert_eq!(interface.descriptor.operations.len(), 1);
assert_eq!(interface.descriptor.operations[0].name, "read");
assert!(interface.descriptor.operations[0].parameters.is_empty());
assert!(interface.scope_ref.is_none());
for expected in data().nodes {
let call = read(&expected.path);
let response = world.call_operation(call.clone()).unwrap();
response
.validate_for(&interface.descriptor, &call.operation)
.unwrap();
assert_eq!(
DocumentContent::decode(response.result).unwrap(),
DocumentContent {
path: expected.path,
title: expected.title,
href: expected.href,
html: expected.html,
}
);
}
}
#[test]
fn depth_boundaries_empty_children_and_order_are_protocol_observations() {
let world = Worldspace::new(data()).unwrap();
let observe = |path: &str, depth| {
world
.observe(ObserveRequest {
path: path.into(),
depth,
})
.unwrap()
};
assert!(observe("/", 0).children.is_none());
let root = observe("/", 1);
let children = root.children.unwrap();
assert_eq!(children.len(), 1);
assert_eq!(children[0].object.name, "book");
assert!(children[0].children.is_none());
let root = observe("/", 3);
let book = &root.children.as_ref().unwrap()[0];
let chapters = book.children.as_ref().unwrap();
assert_eq!(
chapters
.iter()
.map(|node| node.object.name.as_str())
.collect::<Vec<_>>(),
vec!["1-chapter.md", "10-last.md", "2-second.md"]
);
assert!(chapters[0].children.as_ref().unwrap()[0].children.is_none());
assert_eq!(chapters[1].children, Some(vec![]));
assert_eq!(
observe("/book/1-chapter.md/1.1-section.md", 1).children,
Some(vec![])
);
assert!(observe("/", MAX_DEPTH).children.is_some());
assert_eq!(
world
.observe(ObserveRequest {
path: "/".into(),
depth: MAX_DEPTH + 1
})
.unwrap_err()
.code,
ProtocolErrorCode::ResourceLimitExceeded
);
}
#[test]
fn canonical_host_failures_are_not_replaced_by_direct_body_lookup() {
let world = Worldspace::new(data()).unwrap();
for path in ["relative", "/book/", "/book//x", "/book/../x", "/./book"] {
assert_eq!(
world
.observe(ObserveRequest {
path: path.into(),
depth: 0
})
.unwrap_err()
.code,
ProtocolErrorCode::InvalidRequest
);
assert_eq!(
world.call_operation(read(path)).unwrap_err().code,
ProtocolErrorCode::InvalidRequest
);
}
for path in [
"/missing",
"/1-chapter.md",
"/draft/private.md",
"/book/draft/private.md",
] {
assert_eq!(
world
.observe(ObserveRequest {
path: path.into(),
depth: 1
})
.unwrap_err()
.code,
ProtocolErrorCode::NotFound
);
assert_eq!(
world.call_operation(read(path)).unwrap_err().code,
ProtocolErrorCode::NotFound
);
}
for reference in [
InterfaceReference {
scope: "/".into(),
name: "missing".into(),
},
InterfaceReference {
scope: "/book".into(),
name: "document".into(),
},
InterfaceReference {
scope: "/draft".into(),
name: "document".into(),
},
] {
assert_eq!(
world
.fetch_interface(FetchInterfaceRequest {
interface: reference.clone()
})
.unwrap_err()
.code,
ProtocolErrorCode::InterfaceNotFound
);
let mut call = read("/book");
call.interface.reference = reference;
assert_eq!(
world.call_operation(call).unwrap_err().code,
ProtocolErrorCode::InterfaceMismatch
);
}
assert_eq!(
world
.fetch_interface(FetchInterfaceRequest {
interface: InterfaceReference {
scope: "relative".into(),
name: "document".into()
},
})
.unwrap_err()
.code,
ProtocolErrorCode::InvalidRequest
);
let mut call = read("/book");
call.interface.scope_ref = Some("not-the-published-root".into());
assert_eq!(
world.call_operation(call).unwrap_err().code,
ProtocolErrorCode::InterfaceMismatch
);
let mut call = read("/book");
call.operation = "missing".into();
assert_eq!(
world.call_operation(call).unwrap_err().code,
ProtocolErrorCode::OperationNotFound
);
let mut call = read("/book");
call.arguments
.insert("extra".into(), Value::String("not accepted".into()));
assert_eq!(
world.call_operation(call).unwrap_err().code,
ProtocolErrorCode::InvalidArguments
);
let mut call = read("/book");
call.target.validator = Some(vec![1]);
assert_eq!(
world.call_operation(call).unwrap_err().code,
ProtocolErrorCode::ValidatorMismatch
);
let mut call = read("/book");
call.interface.validator = Some(vec![1]);
assert_eq!(
world.call_operation(call).unwrap_err().code,
ProtocolErrorCode::InterfaceValidatorMismatch
);
}
#[test]
fn rejects_duplicate_invalid_disconnected_and_private_input() {
let reject = |mutate: fn(&mut WorldspaceData)| {
let mut input = data();
mutate(&mut input);
assert!(Worldspace::new(input).is_err());
};
reject(|data| data.nodes.push(data.nodes[0].clone()));
reject(|data| data.nodes[2].display_path = "/book".into());
reject(|data| data.nodes[2].href = "/book/".into());
reject(|data| data.nodes[2].name = "a/b".into());
reject(|data| data.nodes[2].name = "..".into());
reject(|data| data.nodes[2].name.clear());
reject(|data| data.nodes[2].path = "/book//x".into());
reject(|data| data.nodes[2].path = "/elsewhere".into());
reject(|data| data.nodes[2].parent = None);
reject(|data| data.nodes[3].parent = Some("/missing".into()));
reject(|data| data.nodes[0].name = "root".into());
reject(|data| data.nodes[0].parent = Some("/".into()));
reject(|data| data.nodes[0].display_path = "/home".into());
reject(|data| {
data.nodes.remove(0);
});
reject(|data| data.nodes[2].display_path = "/draft/secret.md".into());
reject(|data| {
data.nodes
.push(node("/draft", Some("/"), "/private", "/private/"))
});
reject(|data| data.nodes[0].href = "//external.example/".into());
reject(|data| data.nodes[0].title.clear());
assert!(Worldspace::new(WorldspaceData { nodes: vec![] }).is_err());
assert!(
Worldspace::new(WorldspaceData {
nodes: vec![data().nodes[0].clone(); MAX_NODES + 1]
})
.is_err()
);
let mut wide = WorldspaceData {
nodes: vec![data().nodes[0].clone()],
};
for index in 1..MAX_NODES {
let path = format!("/node-{index}");
wide.nodes
.push(node(&path, Some("/"), &path, &format!("{path}/")));
}
let world = Worldspace::new(wide).unwrap();
assert_eq!(
world
.observe(ObserveRequest {
path: "/".into(),
depth: 1
})
.unwrap()
.children
.unwrap()
.len(),
MAX_NODES - 1
);
let mut deep = WorldspaceData {
nodes: vec![data().nodes[0].clone()],
};
let mut parent = "/".to_string();
for _ in 0..=MAX_DEPTH {
let path = format!("{}/a", parent.trim_end_matches('/'));
deep.nodes
.push(node(&path, Some(&parent), &path, &format!("{path}/")));
parent = path;
if deep.nodes.len() == MAX_DEPTH as usize + 1 {
assert!(Worldspace::new(deep.clone()).is_ok());
}
}
assert!(Worldspace::new(deep).is_err());
}
#[test]
fn json_contract_roundtrips_and_rejects_malformed_or_unknown_fields() {
let json = serde_json::to_string(&data()).unwrap();
assert!(json.contains("displayPath"));
let world = Worldspace::from_json(&json).unwrap();
assert!(world.call_operation(read("/")).is_ok());
assert!(Worldspace::from_json("not json").is_err());
assert!(Worldspace::from_json(r#"{"nodes":[],"draft":true}"#).is_err());
let too_large = " ".repeat(wip_site_worldspace::MAX_JSON_BYTES + 1);
assert!(Worldspace::from_json(&too_large).is_err());
// Input order need not be topological; child order still follows the input.
let mut shuffled = data();
shuffled.nodes.reverse();
assert!(
Worldspace::new(shuffled)
.unwrap()
.observe(ObserveRequest {
path: "/".into(),
depth: 4
})
.is_ok()
);
}
/// This is deliberately explicit, not silently skipped when generated input is absent.
/// Run after `npm run build`: cargo test -p wip-site-worldspace --test worldspace -- --include-ignored
#[test]
#[ignore = "requires the site's generated dist/worldspace.json (npm run build)"]
fn generated_real_public_documents_support_all_interactions() {
let json = std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../dist/worldspace.json"
))
.unwrap();
let data: WorldspaceData = serde_json::from_str(&json).unwrap();
assert!(data.nodes.len() > 10);
let world = Worldspace::from_json(&json).unwrap();
let full = world
.observe(ObserveRequest {
path: "/".into(),
depth: MAX_DEPTH,
})
.unwrap();
assert_eq!(full.children.unwrap()[0].object.name, "book");
let descriptor = world
.fetch_interface(FetchInterfaceRequest {
interface: Worldspace::document_interface(),
})
.unwrap()
.descriptor;
for node in &data.nodes {
assert!(!node.display_path.starts_with("/draft/"));
assert!(!node.html.is_empty());
let request = read(&node.path);
let response = world.call_operation(request.clone()).unwrap();
response
.validate_for(&descriptor, &request.operation)
.unwrap();
let content = DocumentContent::decode(response.result).unwrap();
assert_eq!(content.path, node.path);
assert_eq!(content.href, node.href);
assert_eq!(content.html, node.html);
if let Some(parent) = &node.parent {
assert_eq!(
node.path,
format!("{}/{}", parent.trim_end_matches('/'), node.name)
);
}
}
assert!(data.nodes.iter().any(|node| node.path
== "/book/1-concept.md/1.1-interaction-abstraction.md"
&& node.display_path == "/1.1-interaction-abstraction.md"
&& node.href.ends_with("/book/1.1-interaction-abstraction/")));
for path in ["/draft", "/book/draft", "/book/draft/6-security.md"] {
assert_eq!(
world.call_operation(read(path)).unwrap_err().code,
ProtocolErrorCode::NotFound
);
}
}