feat(wip): integrate scoped SDK and object-centered tools
Vendor the reproducible 1cbe03b4 Client/HTTP/Protocol/Text View snapshot with Cargo checksums and credential-free Nix dependency closure. Migrate native and compatibility publications to structured references and scope_ref, preserve target-first preconditions and frozen call descriptors, and expose Tree/Inspect/Invoke with complete edge coverage and official signatures. Validation: worker 888 unit + 110 integration tests; pinned upstream Client63, HTTP39, Protocol23, TextView20 tests/doctests; root cargo check --locked --offline; format/diff checks; 43-file vendor checksum verification; nix build --no-link .#yoi.cargoDeps. Details and provider migration seams: docs/development/wip-integration-validation.md.
This commit is contained in:
@@ -54,6 +54,7 @@ base64 = { workspace = true }
|
||||
wip-client = { workspace = true }
|
||||
wip-http = { workspace = true }
|
||||
wip-protocol = { workspace = true }
|
||||
wip-text-view = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
axum = { workspace = true }
|
||||
|
||||
@@ -90,7 +90,7 @@ manifests of already-running Workers.
|
||||
|
||||
Public integration seams: `WorkspaceConfigFeature::for_workspace`,
|
||||
`workspace_config::wip::mount_workspace_config_wip`, and
|
||||
`WipRuntime::from_mounts` plus `discover`/`inspect`/`call`. Worker tests exercise the
|
||||
`WipRuntime::from_mounts` plus `tree`/`inspect`/`invoke`. Worker tests exercise the
|
||||
real Feature registry, native mount and WIP Client/Host through a typed in-process
|
||||
contract router: deep discovery, slash/native attach, read/edit/write,
|
||||
create/delete, atomic multi-source updates, grant restrictions/revocation,
|
||||
|
||||
@@ -146,8 +146,10 @@ impl Provider {
|
||||
kind: &str,
|
||||
handler: Arc<dyn WipOperationHandler>,
|
||||
) -> WipProjection {
|
||||
let encoded: String = path.as_bytes().iter().map(|b| format!("{b:02x}")).collect();
|
||||
let interface = format!("yoi.checkout/{}/{kind}/v1/@/{encoded}", self.incarnation);
|
||||
let interface = crate::wip::contextual_reference(
|
||||
&format!("yoi.checkout/{}/{kind}/v1", self.incarnation),
|
||||
path,
|
||||
);
|
||||
let interface_validator = Sha256::digest(format!("{descriptor:?}")).to_vec();
|
||||
WipProjection {
|
||||
route: path.into(),
|
||||
|
||||
@@ -320,8 +320,8 @@ fn native_runtime(remote: Arc<RemoteWorkdirSession>) -> (WipRuntime, Arc<Workdir
|
||||
(runtime, router)
|
||||
}
|
||||
|
||||
async fn observe_interface(runtime: &WipRuntime, path: &str) -> String {
|
||||
runtime.discover(path.into(), 0, true).await.unwrap();
|
||||
async fn observe_interface(runtime: &WipRuntime, path: &str) -> wip_protocol::InterfaceReference {
|
||||
runtime.tree(path.into(), 0, true).await.unwrap();
|
||||
// Select from the real Client's Known Space, not a direct handler invocation.
|
||||
let interface = {
|
||||
let state = runtime.state.lock().unwrap();
|
||||
@@ -337,7 +337,7 @@ async fn observe_interface(runtime: &WipRuntime, path: &str) -> String {
|
||||
.interfaces[0]
|
||||
.clone()
|
||||
};
|
||||
runtime.inspect(interface.clone(), true).await.unwrap();
|
||||
runtime.inspect(path.into(), true).await.unwrap();
|
||||
interface
|
||||
}
|
||||
|
||||
|
||||
@@ -33,10 +33,10 @@ fn runtime(
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
async fn interface(r: &WipRuntime, path: &str) -> String {
|
||||
r.discover(path.into(), 0, true).await.unwrap();
|
||||
async fn interface(r: &WipRuntime, path: &str) -> wip_protocol::InterfaceReference {
|
||||
r.tree(path.into(), 0, true).await.unwrap();
|
||||
let p = r.host.projection_live(path).await.unwrap().unwrap();
|
||||
r.inspect(p.interface.clone(), true).await.unwrap();
|
||||
r.inspect(path.into(), true).await.unwrap();
|
||||
p.interface
|
||||
}
|
||||
async fn call(
|
||||
|
||||
@@ -101,7 +101,8 @@ pub fn mount_workspace_workdir_wip(
|
||||
for domain in [Domain::Repository, Domain::Workdir, Domain::Attachment] {
|
||||
let namespace =
|
||||
registry.allocate_namespace(domain.owner(), domain.route().trim_start_matches('/'))?;
|
||||
let interface = format!("yoi.{}/collection/v1", domain.owner());
|
||||
let interface =
|
||||
crate::wip::root_reference(&format!("yoi.{}/collection/v1", domain.owner()));
|
||||
let handler = Arc::new(CollectionHandler {
|
||||
provider: provider.clone(),
|
||||
domain,
|
||||
@@ -130,7 +131,7 @@ pub fn mount_workspace_workdir_wip(
|
||||
handler,
|
||||
})?;
|
||||
}
|
||||
let item_interface = format!("yoi.{}/item/v1", domain.owner());
|
||||
let item_interface = crate::wip::root_reference(&format!("yoi.{}/item/v1", domain.owner()));
|
||||
let resolver = Arc::new(ItemResolver {
|
||||
provider: provider.clone(),
|
||||
domain,
|
||||
@@ -668,7 +669,7 @@ impl WipDynamicItemResolver for ItemResolver {
|
||||
}
|
||||
let raw = self.provider.raw_item(self.domain, &id).ok()?;
|
||||
let projected = self.provider.projected(self.domain, &raw).ok()?;
|
||||
let interface = format!("yoi.{}/item/v1", self.domain.owner());
|
||||
let interface = crate::wip::root_reference(&format!("yoi.{}/item/v1", self.domain.owner()));
|
||||
Some(WipDynamicItem {
|
||||
object: object(&item_path(self.domain, &id), &id, &interface, &projected),
|
||||
handler: Arc::new(ItemHandler {
|
||||
@@ -836,7 +837,12 @@ impl WipOperationHandler for ItemHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fn object(path: &str, identity: &str, interface: &str, value: &Json) -> Object {
|
||||
fn object(
|
||||
path: &str,
|
||||
identity: &str,
|
||||
interface: &wip_protocol::InterfaceReference,
|
||||
value: &Json,
|
||||
) -> Object {
|
||||
let mut digest = Sha256::new();
|
||||
digest.update(path);
|
||||
digest.update([0]);
|
||||
@@ -844,7 +850,7 @@ fn object(path: &str, identity: &str, interface: &str, value: &Json) -> Object {
|
||||
Object {
|
||||
name: path.rsplit('/').next().unwrap_or_default().into(),
|
||||
description: Some(format!("Workspace authority Object: {identity}")),
|
||||
interfaces: vec![interface.into()],
|
||||
interfaces: vec![interface.clone()],
|
||||
r#ref: Some(identity.into()),
|
||||
validator: Some(digest.finalize().to_vec()),
|
||||
}
|
||||
@@ -1658,7 +1664,7 @@ pub(crate) mod tests {
|
||||
registry.contribute_operations(WipOperationContribution {
|
||||
route: WORKDIRS.into(),
|
||||
contributor: "another-feature".into(),
|
||||
interface: "yoi.workdir/collection/v1".into(),
|
||||
interface: crate::wip::root_reference("yoi.workdir/collection/v1"),
|
||||
descriptor: collection_descriptor(Domain::Workdir, true),
|
||||
handler: Arc::new(CollectionHandler {
|
||||
provider: provider(Arc::new(CatalogClient::default()), true, true, None),
|
||||
|
||||
@@ -559,14 +559,16 @@ pub fn mount_workspace_http_merge_request_wip(
|
||||
description: Some(
|
||||
"Authoritative Merge Request collection through scoped Backend authority".into(),
|
||||
),
|
||||
interfaces: vec![MERGE_REQUEST_COLLECTION_INTERFACE.into()],
|
||||
interfaces: vec![crate::wip::root_reference(
|
||||
MERGE_REQUEST_COLLECTION_INTERFACE,
|
||||
)],
|
||||
r#ref: Some("merge-request:collection".into()),
|
||||
validator: Some(merge_request_route_validator(
|
||||
&collection_route,
|
||||
"collection",
|
||||
)),
|
||||
},
|
||||
interface: MERGE_REQUEST_COLLECTION_INTERFACE.into(),
|
||||
interface: crate::wip::root_reference(MERGE_REQUEST_COLLECTION_INTERFACE),
|
||||
interface_validator: Some(merge_request_descriptor_validator(&collection_descriptor)),
|
||||
descriptor: collection_descriptor,
|
||||
handler: Arc::new(MergeRequestCollectionWipHandler {
|
||||
@@ -585,7 +587,7 @@ pub fn mount_workspace_http_merge_request_wip(
|
||||
registry.mount_dynamic(WipDynamicMount {
|
||||
collection_route: collection_route.clone(),
|
||||
capability: "merge-request:item".into(),
|
||||
interface: MERGE_REQUEST_ITEM_INTERFACE.into(),
|
||||
interface: crate::wip::root_reference(MERGE_REQUEST_ITEM_INTERFACE),
|
||||
interface_validator: Some(merge_request_descriptor_validator(&item_descriptor)),
|
||||
descriptor: item_descriptor,
|
||||
resolver: Arc::new(MergeRequestItemResolver {
|
||||
@@ -683,7 +685,7 @@ impl WipDynamicItemResolver for MergeRequestItemResolver {
|
||||
object: Object {
|
||||
name: item_reference.into(),
|
||||
description: Some("Authoritative Merge Request bound to this object route".into()),
|
||||
interfaces: vec![MERGE_REQUEST_ITEM_INTERFACE.into()],
|
||||
interfaces: vec![crate::wip::root_reference(MERGE_REQUEST_ITEM_INTERFACE)],
|
||||
r#ref: Some(format!("merge-request:{item_reference}")),
|
||||
validator: Some(merge_request_route_validator(&route, &revision)),
|
||||
},
|
||||
|
||||
@@ -578,11 +578,11 @@ pub fn mount_workspace_http_objective_wip(
|
||||
"Authoritative Objective collection; search and create through Backend authority"
|
||||
.into(),
|
||||
),
|
||||
interfaces: vec![OBJECTIVE_COLLECTION_INTERFACE.into()],
|
||||
interfaces: vec![crate::wip::root_reference(OBJECTIVE_COLLECTION_INTERFACE)],
|
||||
r#ref: Some("objective:collection".into()),
|
||||
validator: Some(route_validator(&collection_route, "collection")),
|
||||
},
|
||||
interface: OBJECTIVE_COLLECTION_INTERFACE.into(),
|
||||
interface: crate::wip::root_reference(OBJECTIVE_COLLECTION_INTERFACE),
|
||||
descriptor: collection_descriptor,
|
||||
interface_validator: Some(collection_validator),
|
||||
handler: collection_handler,
|
||||
@@ -593,7 +593,7 @@ pub fn mount_workspace_http_objective_wip(
|
||||
registry.mount_dynamic(WipDynamicMount {
|
||||
collection_route: collection_route.clone(),
|
||||
capability: "objective:item".into(),
|
||||
interface: OBJECTIVE_ITEM_INTERFACE.into(),
|
||||
interface: crate::wip::root_reference(OBJECTIVE_ITEM_INTERFACE),
|
||||
descriptor: item_descriptor,
|
||||
interface_validator: Some(item_validator),
|
||||
resolver: Arc::new(ObjectiveItemResolver {
|
||||
@@ -744,7 +744,7 @@ impl WipDynamicItemResolver for ObjectiveItemResolver {
|
||||
object: Object {
|
||||
name: item_reference.to_string(),
|
||||
description: Some("Authoritative Objective bound to this object route".into()),
|
||||
interfaces: vec![OBJECTIVE_ITEM_INTERFACE.into()],
|
||||
interfaces: vec![crate::wip::root_reference(OBJECTIVE_ITEM_INTERFACE)],
|
||||
r#ref: Some(format!("objective:{item_reference}")),
|
||||
validator: Some(route_validator(&route, &revision)),
|
||||
},
|
||||
|
||||
@@ -1814,11 +1814,11 @@ pub fn mount_workspace_http_ticket_wip(
|
||||
description: Some(
|
||||
"Authoritative Ticket collection through scoped Backend authority".into(),
|
||||
),
|
||||
interfaces: vec![TICKET_COLLECTION_INTERFACE.into()],
|
||||
interfaces: vec![crate::wip::root_reference(TICKET_COLLECTION_INTERFACE)],
|
||||
r#ref: Some("ticket:collection".into()),
|
||||
validator: Some(route_validator(&collection_route, "collection")),
|
||||
},
|
||||
interface: TICKET_COLLECTION_INTERFACE.into(),
|
||||
interface: crate::wip::root_reference(TICKET_COLLECTION_INTERFACE),
|
||||
interface_validator: Some(descriptor_validator(&collection_descriptor)),
|
||||
descriptor: collection_descriptor,
|
||||
handler: Arc::new(TicketCollectionWipHandler {
|
||||
@@ -1837,7 +1837,7 @@ pub fn mount_workspace_http_ticket_wip(
|
||||
registry.mount_dynamic(WipDynamicMount {
|
||||
collection_route: collection_route.clone(),
|
||||
capability: "ticket:item".into(),
|
||||
interface: TICKET_ITEM_INTERFACE.into(),
|
||||
interface: crate::wip::root_reference(TICKET_ITEM_INTERFACE),
|
||||
interface_validator: Some(descriptor_validator(&item_descriptor)),
|
||||
descriptor: item_descriptor,
|
||||
resolver: Arc::new(TicketItemResolver {
|
||||
@@ -1923,7 +1923,7 @@ impl WipDynamicItemResolver for TicketItemResolver {
|
||||
object: Object {
|
||||
name: item_reference.into(),
|
||||
description: Some("Authoritative Ticket bound to this object route".into()),
|
||||
interfaces: vec![TICKET_ITEM_INTERFACE.into()],
|
||||
interfaces: vec![crate::wip::root_reference(TICKET_ITEM_INTERFACE)],
|
||||
r#ref: Some(format!("ticket:{item_reference}")),
|
||||
validator: Some(route_validator(&route, &revision)),
|
||||
},
|
||||
|
||||
@@ -318,7 +318,7 @@ impl FeatureInvocationHandler for WorkspaceConfigFeature {
|
||||
result.access.as_str()
|
||||
),
|
||||
context: Some(format!(
|
||||
"Workspace configuration is available as logical attachment `{ATTACHMENT_ALIAS}` at WIP `{CONTENT_ROOT}` with {} access. Discover structure through WIP; read and change bodies through published Operations. This attachment grants no OS paths or command execution. Backend validation, concurrency checks and activation rules apply; saving does not imply running Workers adopted new configuration.",
|
||||
"Workspace configuration is available as logical attachment `{ATTACHMENT_ALIAS}` at WIP `{CONTENT_ROOT}` with {} access. Use Tree for indexable structure, Inspect with a known Object path for its complete signatures, and Invoke for published Operations. This attachment grants no OS paths or command execution. Backend validation, concurrency checks and activation rules apply; saving does not imply running Workers adopted new configuration.",
|
||||
result.access.as_str()
|
||||
)),
|
||||
})
|
||||
|
||||
@@ -205,7 +205,7 @@ impl WipSubtreeProvider for ConfigProvider {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut projection = super::wip::attach_projection(&self.feature);
|
||||
projection.interface = contextual_reference(&projection.interface, path);
|
||||
projection.interface = contextual_reference(&projection.interface.name, path);
|
||||
projection.object.interfaces = vec![projection.interface.clone()];
|
||||
return Ok(Some(projection));
|
||||
};
|
||||
|
||||
@@ -428,7 +428,7 @@ fn runtime(client: Arc<dyn WorkspaceClient>) -> (WorkspaceConfigFeature, WipRunt
|
||||
let runtime = WipRuntime::from_mounts(mounts, "worker-1@workspace-1".into()).unwrap();
|
||||
(feature, runtime)
|
||||
}
|
||||
fn interface(path: &str, attached: bool) -> String {
|
||||
fn interface(path: &str, attached: bool) -> wip_protocol::InterfaceReference {
|
||||
crate::wip::contextual_reference(
|
||||
if attached {
|
||||
"yoi.workspace-config/node/v1"
|
||||
@@ -438,12 +438,9 @@ fn interface(path: &str, attached: bool) -> String {
|
||||
path,
|
||||
)
|
||||
}
|
||||
async fn observe_interface(runtime: &WipRuntime, path: &str, attached: bool) {
|
||||
runtime.discover(path.into(), 0, true).await.unwrap();
|
||||
runtime
|
||||
.inspect(interface(path, attached), true)
|
||||
.await
|
||||
.unwrap();
|
||||
async fn observe_interface(runtime: &WipRuntime, path: &str, _attached: bool) {
|
||||
runtime.tree(path.into(), 0, true).await.unwrap();
|
||||
runtime.inspect(path.into(), true).await.unwrap();
|
||||
}
|
||||
async fn call(
|
||||
runtime: &WipRuntime,
|
||||
@@ -497,7 +494,7 @@ async fn workspace_config_production_slash_deep_discover_read_edit_and_atomic_im
|
||||
.starts_with("Already attached")
|
||||
);
|
||||
let discovered = runtime
|
||||
.discover(CONTENT_ROOT.into(), 5, true)
|
||||
.tree(CONTENT_ROOT.into(), 5, true)
|
||||
.await
|
||||
.unwrap()
|
||||
.content
|
||||
@@ -606,7 +603,7 @@ async fn workspace_config_production_native_attach_create_delete_and_detach_end_
|
||||
);
|
||||
assert!(
|
||||
runtime
|
||||
.discover("/workspace-config/main.dcdl".into(), 0, true)
|
||||
.tree("/workspace-config/main.dcdl".into(), 0, true)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
@@ -633,19 +630,15 @@ async fn workspace_config_production_revocation_ro_cross_workspace_and_cache_iso
|
||||
);
|
||||
observe_interface(&runtime, path, true).await;
|
||||
let descriptor = runtime
|
||||
.inspect(interface(path, true), true)
|
||||
.inspect(path.into(), true)
|
||||
.await
|
||||
.unwrap()
|
||||
.content
|
||||
.unwrap();
|
||||
let descriptor: Json = serde_json::from_str(&descriptor).unwrap();
|
||||
let names = descriptor["descriptor"]["operations"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|op| op["name"].as_str().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(names, ["read"]);
|
||||
let signature = descriptor["interfaces"][0]["signature"].as_str().unwrap();
|
||||
assert!(signature.contains("operation read("));
|
||||
assert!(!signature.contains("operation write("));
|
||||
assert!(
|
||||
call(&runtime, path, "write", json!({"content":"DENIED"}))
|
||||
.await
|
||||
@@ -657,7 +650,7 @@ async fn workspace_config_production_revocation_ro_cross_workspace_and_cache_iso
|
||||
});
|
||||
let (feature, other) = runtime_for_cross(cross);
|
||||
assert!(feature.attach("cross", None).await.is_err());
|
||||
assert!(other.discover(path.into(), 0, false).await.is_err());
|
||||
assert!(other.tree(path.into(), 0, false).await.is_err());
|
||||
assert!(client.state.lock().unwrap().commits.is_empty());
|
||||
}
|
||||
fn runtime_for_cross(client: Arc<dyn WorkspaceClient>) -> (WorkspaceConfigFeature, WipRuntime) {
|
||||
@@ -760,21 +753,16 @@ async fn workspace_config_production_bounds_and_traversal_reject_before_content_
|
||||
let client = Router::new();
|
||||
let (feature, runtime) = runtime(client.clone());
|
||||
feature.attach("initial", None).await.unwrap();
|
||||
assert!(
|
||||
runtime
|
||||
.discover(CONTENT_ROOT.into(), 9, true)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(runtime.tree(CONTENT_ROOT.into(), 9, true).await.is_err());
|
||||
for path in [
|
||||
"/workspace-config/../secret",
|
||||
"/workspace-config//main.dcdl",
|
||||
"/workspace-config/./main.dcdl",
|
||||
] {
|
||||
assert!(runtime.discover(path.into(), 0, true).await.is_err());
|
||||
assert!(runtime.tree(path.into(), 0, true).await.is_err());
|
||||
}
|
||||
let path = format!("/workspace-config/{}", "a".repeat(513));
|
||||
assert!(runtime.discover(path, 0, true).await.is_err());
|
||||
assert!(runtime.tree(path, 0, true).await.is_err());
|
||||
assert!(client.state.lock().unwrap().commits.is_empty());
|
||||
}
|
||||
|
||||
@@ -837,7 +825,7 @@ async fn workspace_config_production_cumulative_node_limit_and_edit_growth_are_b
|
||||
// No individual depth-1 response exceeds the cap; the whole Host traversal
|
||||
// must nevertheless stop before exceeding 256 Objects.
|
||||
let error = runtime
|
||||
.discover(CONTENT_ROOT.into(), 4, true)
|
||||
.tree(CONTENT_ROOT.into(), 4, true)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("ResourceLimitExceeded"));
|
||||
@@ -849,7 +837,7 @@ async fn workspace_config_production_backend_failure_is_not_hidden_as_unauthoriz
|
||||
client.state.lock().unwrap().fail_current = true;
|
||||
let (_, runtime) = runtime(client);
|
||||
let error = runtime
|
||||
.discover(CONTENT_ROOT.into(), 0, true)
|
||||
.tree(CONTENT_ROOT.into(), 0, true)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let message = error.to_string();
|
||||
@@ -907,16 +895,11 @@ async fn workspace_config_production_ungranted_root_does_not_poison_worldspace_d
|
||||
let client = Router::new();
|
||||
client.state.lock().unwrap().granted = false;
|
||||
let (_, runtime) = runtime(client.clone());
|
||||
let discovery = runtime.discover("/".into(), 2, true).await.unwrap();
|
||||
let discovery = runtime.tree("/".into(), 2, true).await.unwrap();
|
||||
let discovery: Json = serde_json::from_str(discovery.content.as_deref().unwrap()).unwrap();
|
||||
assert_eq!(discovery["known_space"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(discovery["known_space"][0]["path"], "/");
|
||||
assert!(
|
||||
runtime
|
||||
.discover(CONTENT_ROOT.into(), 0, true)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(discovery["tree"]["children"], json!([]));
|
||||
assert_eq!(discovery["tree"]["path"], "/");
|
||||
assert!(runtime.tree(CONTENT_ROOT.into(), 0, true).await.is_err());
|
||||
assert!(
|
||||
client
|
||||
.state
|
||||
|
||||
@@ -93,13 +93,13 @@ pub(super) fn attach_projection(feature: &WorkspaceConfigFeature) -> WipProjecti
|
||||
object: Object {
|
||||
name: ATTACHMENT_ALIAS.into(),
|
||||
description: Some("Logical Workspace configuration attachment entrance".into()),
|
||||
interfaces: vec![interface.into()],
|
||||
interfaces: vec![crate::wip::root_reference(interface)],
|
||||
r#ref: None,
|
||||
// Attaching does not modify config content. The content subtree will
|
||||
// supply its own Backend validators for file/directory Operations.
|
||||
validator: None,
|
||||
},
|
||||
interface: interface.into(),
|
||||
interface: crate::wip::root_reference(interface),
|
||||
descriptor,
|
||||
interface_validator,
|
||||
handler: Arc::new(AttachHandler(feature.clone())),
|
||||
|
||||
+537
-773
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,562 @@
|
||||
//! Scope publication and precondition tests through the actual Yoi HTTP adapter.
|
||||
use super::*;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize};
|
||||
|
||||
struct ReadHandler(Arc<AtomicUsize>);
|
||||
#[async_trait]
|
||||
impl WipOperationHandler for ReadHandler {
|
||||
async fn call(
|
||||
&self,
|
||||
_: &str,
|
||||
_: &BTreeMap<String, Value>,
|
||||
_: WipCallContext,
|
||||
) -> Result<WipOperationOutput, WipOperationError> {
|
||||
self.0.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(WipOperationOutput::native(Value::String("value".into())))
|
||||
}
|
||||
}
|
||||
|
||||
fn projection(path: &str, scope: &str, name: &str, calls: Arc<AtomicUsize>) -> WipProjection {
|
||||
let interface = InterfaceReference {
|
||||
scope: scope.into(),
|
||||
name: name.into(),
|
||||
};
|
||||
WipProjection {
|
||||
route: path.into(),
|
||||
capability: "scope:object".into(),
|
||||
kind: WipProjectionKind::Native,
|
||||
object: Object {
|
||||
name: path.rsplit('/').next().unwrap().into(),
|
||||
description: None,
|
||||
interfaces: vec![interface.clone()],
|
||||
r#ref: Some(format!("object:{path}")),
|
||||
validator: None,
|
||||
},
|
||||
interface,
|
||||
descriptor: InterfaceDescriptor {
|
||||
format: INTERFACE_FORMAT_V1.into(),
|
||||
documentation: None,
|
||||
types: vec![],
|
||||
operations: vec![OperationDeclaration {
|
||||
name: "read".into(),
|
||||
documentation: None,
|
||||
parameters: vec![],
|
||||
returns: ReturnDeclaration {
|
||||
documentation: None,
|
||||
r#type: TypeExpr::String,
|
||||
},
|
||||
}],
|
||||
},
|
||||
interface_validator: None,
|
||||
handler: Arc::new(ReadHandler(calls)),
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata_request(
|
||||
path: &str,
|
||||
reference: Json,
|
||||
object_validator: Option<&str>,
|
||||
scope_ref: Option<&str>,
|
||||
) -> Request<Vec<u8>> {
|
||||
let mut target = json!({"path": path});
|
||||
if let Some(v) = object_validator {
|
||||
target["validator"] = json!(v);
|
||||
}
|
||||
let mut interface = json!({"reference": reference});
|
||||
if let Some(v) = scope_ref {
|
||||
interface["scope_ref"] = json!(v);
|
||||
}
|
||||
Request::builder()
|
||||
.body(
|
||||
serde_json::to_vec(
|
||||
&json!({"target":target,"interface":interface,"operation":"read","arguments":{}}),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn rejected_code(runtime: &WipRuntime, request: Request<Vec<u8>>) -> String {
|
||||
let (response, passthrough, outcome) = runtime
|
||||
.dispatch_call(&request, ToolExecutionContext::direct())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(passthrough.is_none());
|
||||
assert_eq!(outcome, WipAuditOutcome::Rejected);
|
||||
let json: Json = serde_json::from_slice(response.body()).unwrap();
|
||||
json["error"]["code"].as_str().unwrap().into()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn adapter_preconditions_preserve_shape_target_validator_and_scope_precedence() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let mut registry = WipMountRegistry::new();
|
||||
registry.allocate_namespace("scope", "github").unwrap();
|
||||
let mut p = projection("/github/item", "/github/item", "read", calls.clone());
|
||||
p.object.validator = Some(vec![1]);
|
||||
registry.mount(p).unwrap();
|
||||
let runtime = WipRuntime::from_mounts(registry, "scope-test".into()).unwrap();
|
||||
assert_eq!(
|
||||
rejected_code(
|
||||
&runtime,
|
||||
metadata_request("/missing", json!("legacy"), None, None)
|
||||
)
|
||||
.await,
|
||||
"invalid_request"
|
||||
);
|
||||
assert_eq!(
|
||||
rejected_code(
|
||||
&runtime,
|
||||
metadata_request(
|
||||
"/missing",
|
||||
json!({"scope":"/sibling","name":"read"}),
|
||||
Some("AA=="),
|
||||
None
|
||||
)
|
||||
)
|
||||
.await,
|
||||
"not_found"
|
||||
);
|
||||
assert_eq!(
|
||||
rejected_code(
|
||||
&runtime,
|
||||
metadata_request(
|
||||
"/github/item",
|
||||
json!({"scope":"/sibling","name":"read"}),
|
||||
Some("AA=="),
|
||||
None
|
||||
)
|
||||
)
|
||||
.await,
|
||||
"validator_mismatch"
|
||||
);
|
||||
for scope in ["/sibling", "/github/item/child", "/github-extra"] {
|
||||
assert_eq!(
|
||||
rejected_code(
|
||||
&runtime,
|
||||
metadata_request(
|
||||
"/github/item",
|
||||
json!({"scope":scope,"name":"read"}),
|
||||
Some("AQ=="),
|
||||
None
|
||||
)
|
||||
)
|
||||
.await,
|
||||
"interface_mismatch"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
rejected_code(
|
||||
&runtime,
|
||||
metadata_request(
|
||||
"/github/item",
|
||||
json!({"scope":"/github/item","name":"read"}),
|
||||
Some("AQ=="),
|
||||
Some("old")
|
||||
)
|
||||
)
|
||||
.await,
|
||||
"interface_mismatch"
|
||||
);
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn actual_provider_root_and_self_scope_inspect_invoke_preserve_optional_refs() {
|
||||
for path in ["/", "/github"] {
|
||||
for with_ref in [true, false] {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let mut registry = WipMountRegistry::new();
|
||||
registry.allocate_namespace("scope", "github").unwrap();
|
||||
let mut p = projection(path, path, "read", calls.clone());
|
||||
if !with_ref {
|
||||
p.object.r#ref = None;
|
||||
}
|
||||
let reference = p.interface.clone();
|
||||
registry.mount(p).unwrap();
|
||||
let runtime = WipRuntime::from_mounts(registry, "scope-test".into()).unwrap();
|
||||
let fetched = runtime.host.fetch_interface_live(&reference).await.unwrap();
|
||||
assert_eq!(
|
||||
fetched.scope_ref,
|
||||
with_ref.then(|| format!("object:{path}"))
|
||||
);
|
||||
runtime.inspect(path.into(), false).await.unwrap();
|
||||
runtime
|
||||
.invoke(
|
||||
path.into(),
|
||||
reference.clone(),
|
||||
"read".into(),
|
||||
json!({}),
|
||||
ToolExecutionContext::direct(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 1);
|
||||
let state = runtime.state.lock().unwrap();
|
||||
let history = state.client.call_history(&state.session).unwrap();
|
||||
assert_eq!(
|
||||
history
|
||||
.back()
|
||||
.unwrap()
|
||||
.context
|
||||
.request()
|
||||
.interface
|
||||
.scope_ref,
|
||||
fetched.scope_ref
|
||||
);
|
||||
drop(state);
|
||||
if !with_ref {
|
||||
assert_eq!(
|
||||
rejected_code(
|
||||
&runtime,
|
||||
metadata_request(
|
||||
path,
|
||||
json!({"scope":path,"name":"read"}),
|
||||
None,
|
||||
Some("invented")
|
||||
)
|
||||
)
|
||||
.await,
|
||||
"interface_mismatch"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multiple_interfaces_same_operation_are_inspected_and_dispatched_without_flattening() {
|
||||
let a = Arc::new(AtomicUsize::new(0));
|
||||
let b = Arc::new(AtomicUsize::new(0));
|
||||
let mut registry = WipMountRegistry::new();
|
||||
registry.allocate_namespace("scope", "github").unwrap();
|
||||
let owner = projection("/github", "/", "same", a.clone());
|
||||
let other = projection("/github", "/github", "same", b.clone());
|
||||
let refs = [owner.interface.clone(), other.interface.clone()];
|
||||
registry.mount(owner).unwrap();
|
||||
registry.mount_interface(other).unwrap();
|
||||
let runtime = WipRuntime::from_mounts(registry, "scope-test".into()).unwrap();
|
||||
let inspected = runtime.inspect("/github".into(), false).await.unwrap();
|
||||
let inspected: Json = serde_json::from_str(inspected.content.as_ref().unwrap()).unwrap();
|
||||
assert_eq!(inspected["interfaces"].as_array().unwrap().len(), 2);
|
||||
for reference in refs {
|
||||
runtime
|
||||
.invoke(
|
||||
"/github".into(),
|
||||
reference,
|
||||
"read".into(),
|
||||
json!({}),
|
||||
ToolExecutionContext::direct(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(a.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(b.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
struct Publication {
|
||||
visible: Arc<AtomicBool>,
|
||||
identity: Arc<Mutex<Option<String>>>,
|
||||
validator: Arc<Mutex<Option<Vec<u8>>>>,
|
||||
calls: Arc<AtomicUsize>,
|
||||
}
|
||||
impl WipDynamicItemResolver for Publication {
|
||||
fn contextual_interface(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn resolve(&self, item: &str) -> Option<WipDynamicItem> {
|
||||
if item != "item" || !self.visible.load(Ordering::SeqCst) {
|
||||
return None;
|
||||
}
|
||||
let mut p = projection("/github/item", "/", "read", self.calls.clone());
|
||||
p.object.r#ref = self.identity.lock().unwrap().clone();
|
||||
p.object.validator = self.validator.lock().unwrap().clone();
|
||||
Some(WipDynamicItem {
|
||||
object: p.object,
|
||||
handler: p.handler,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn current_self_scope_fetch_does_not_attach_a_new_ref_to_an_old_target_snapshot() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let visible = Arc::new(AtomicBool::new(true));
|
||||
let identity = Arc::new(Mutex::new(Some("first".into())));
|
||||
let validator = Arc::new(Mutex::new(None));
|
||||
let mut registry = WipMountRegistry::new();
|
||||
registry.allocate_namespace("scope", "github").unwrap();
|
||||
registry
|
||||
.mount(projection("/github", "/", "collection", calls.clone()))
|
||||
.unwrap();
|
||||
let p = projection("/github/item", "/", "read", calls.clone());
|
||||
registry
|
||||
.mount_dynamic(WipDynamicMount {
|
||||
collection_route: "/github".into(),
|
||||
capability: "scope:item".into(),
|
||||
interface: p.interface,
|
||||
descriptor: p.descriptor,
|
||||
interface_validator: None,
|
||||
resolver: Arc::new(Publication {
|
||||
visible: visible.clone(),
|
||||
identity: identity.clone(),
|
||||
validator: validator.clone(),
|
||||
calls: calls.clone(),
|
||||
}),
|
||||
})
|
||||
.unwrap();
|
||||
let runtime = WipRuntime::from_mounts(registry, "scope-test".into()).unwrap();
|
||||
let reference = contextual_reference("read", "/github/item");
|
||||
runtime.inspect("/github/item".into(), false).await.unwrap();
|
||||
assert_eq!(
|
||||
runtime
|
||||
.host
|
||||
.fetch_interface_live(&reference)
|
||||
.await
|
||||
.unwrap()
|
||||
.scope_ref
|
||||
.as_deref(),
|
||||
Some("first")
|
||||
);
|
||||
// Ordinary content-state updates do not change the scope Object identity.
|
||||
*validator.lock().unwrap() = Some(vec![2]);
|
||||
assert_eq!(
|
||||
runtime
|
||||
.host
|
||||
.fetch_interface_live(&reference)
|
||||
.await
|
||||
.unwrap()
|
||||
.scope_ref
|
||||
.as_deref(),
|
||||
Some("first")
|
||||
);
|
||||
*validator.lock().unwrap() = None;
|
||||
*identity.lock().unwrap() = Some("second".into());
|
||||
let error = runtime
|
||||
.invoke(
|
||||
"/github/item".into(),
|
||||
reference.clone(),
|
||||
"read".into(),
|
||||
json!({}),
|
||||
ToolExecutionContext::direct(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("InterfaceMismatch"));
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 0);
|
||||
// Both target and Interface are invalidated; retrievals are not reexecution.
|
||||
let state = runtime.state.lock().unwrap();
|
||||
assert_eq!(
|
||||
state
|
||||
.client
|
||||
.object(&state.session, "/github/item")
|
||||
.unwrap()
|
||||
.state,
|
||||
ObservationState::Stale
|
||||
);
|
||||
assert_eq!(
|
||||
state
|
||||
.client
|
||||
.interface(&state.session, &reference)
|
||||
.unwrap()
|
||||
.state,
|
||||
ObservationState::Stale
|
||||
);
|
||||
drop(state);
|
||||
runtime
|
||||
.invoke(
|
||||
"/github/item".into(),
|
||||
reference.clone(),
|
||||
"read".into(),
|
||||
json!({}),
|
||||
ToolExecutionContext::direct(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 1);
|
||||
visible.store(false, Ordering::SeqCst);
|
||||
assert!(runtime.host.fetch_interface_live(&reference).await.is_err());
|
||||
assert!(runtime.inspect("/github/item".into(), true).await.is_err());
|
||||
let state = runtime.state.lock().unwrap();
|
||||
assert!(state.client.interface(&state.session, &reference).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn supplied_validators_require_current_values_and_required_object_validator_is_late() {
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let mut registry = WipMountRegistry::new();
|
||||
registry.allocate_namespace("scope", "github").unwrap();
|
||||
registry
|
||||
.mount(projection("/github", "/github", "read", calls.clone()))
|
||||
.unwrap();
|
||||
let runtime = WipRuntime::from_mounts(registry, "scope-test".into()).unwrap();
|
||||
let reference = json!({"scope":"/github","name":"read"});
|
||||
assert_eq!(
|
||||
rejected_code(
|
||||
&runtime,
|
||||
metadata_request("/github", reference.clone(), Some("AQ=="), None)
|
||||
)
|
||||
.await,
|
||||
"validator_mismatch"
|
||||
);
|
||||
let mut request = metadata_request("/github", reference, None, None);
|
||||
let mut body: Json = serde_json::from_slice(request.body()).unwrap();
|
||||
body["interface"]["validator"] = json!("AQ==");
|
||||
*request.body_mut() = serde_json::to_vec(&body).unwrap();
|
||||
assert_eq!(
|
||||
rejected_code(&runtime, request).await,
|
||||
"interface_validator_mismatch"
|
||||
);
|
||||
let mut registry = WipMountRegistry::new();
|
||||
registry.allocate_namespace("scope", "github").unwrap();
|
||||
let mut p = projection("/github", "/github", "read", calls.clone());
|
||||
p.object.validator = Some(vec![1]);
|
||||
registry.mount(p).unwrap();
|
||||
let runtime = WipRuntime::from_mounts(registry, "scope-test".into()).unwrap();
|
||||
assert_eq!(
|
||||
rejected_code(
|
||||
&runtime,
|
||||
metadata_request(
|
||||
"/github",
|
||||
json!({"scope":"/wrong","name":"read"}),
|
||||
None,
|
||||
None
|
||||
)
|
||||
)
|
||||
.await,
|
||||
"interface_mismatch"
|
||||
);
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
|
||||
struct ChangingProvider {
|
||||
generation: Arc<AtomicUsize>,
|
||||
started: Arc<tokio::sync::Notify>,
|
||||
release: Arc<tokio::sync::Notify>,
|
||||
calls: Arc<AtomicUsize>,
|
||||
}
|
||||
struct FrozenResultHandler {
|
||||
generation: usize,
|
||||
started: Arc<tokio::sync::Notify>,
|
||||
release: Arc<tokio::sync::Notify>,
|
||||
calls: Arc<AtomicUsize>,
|
||||
}
|
||||
#[async_trait]
|
||||
impl WipOperationHandler for FrozenResultHandler {
|
||||
async fn call(
|
||||
&self,
|
||||
_: &str,
|
||||
_: &BTreeMap<String, Value>,
|
||||
_: WipCallContext,
|
||||
) -> Result<WipOperationOutput, WipOperationError> {
|
||||
self.calls.fetch_add(1, Ordering::SeqCst);
|
||||
if self.generation == 0 {
|
||||
self.started.notify_one();
|
||||
self.release.notified().await;
|
||||
Ok(WipOperationOutput::native(Value::String(
|
||||
"old result".into(),
|
||||
)))
|
||||
} else {
|
||||
Ok(WipOperationOutput::native(Value::Integer(2)))
|
||||
}
|
||||
}
|
||||
}
|
||||
#[async_trait]
|
||||
impl WipSubtreeProvider for ChangingProvider {
|
||||
async fn projection(&self, path: &str) -> Result<Option<WipProjection>, ProtocolError> {
|
||||
if path != "/github" {
|
||||
return Ok(None);
|
||||
}
|
||||
let generation = self.generation.load(Ordering::SeqCst);
|
||||
let mut p = projection(path, path, "read", self.calls.clone());
|
||||
p.interface_validator = Some(vec![generation as u8]);
|
||||
if generation != 0 {
|
||||
p.descriptor.operations[0].returns.r#type = TypeExpr::Integer;
|
||||
}
|
||||
p.handler = Arc::new(FrozenResultHandler {
|
||||
generation,
|
||||
started: self.started.clone(),
|
||||
release: self.release.clone(),
|
||||
calls: self.calls.clone(),
|
||||
});
|
||||
Ok(Some(p))
|
||||
}
|
||||
async fn children(&self, _: &str) -> Result<Vec<String>, ProtocolError> {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn in_flight_result_uses_frozen_host_and_client_descriptors_after_refresh() {
|
||||
let generation = Arc::new(AtomicUsize::new(0));
|
||||
let started = Arc::new(tokio::sync::Notify::new());
|
||||
let release = Arc::new(tokio::sync::Notify::new());
|
||||
let calls = Arc::new(AtomicUsize::new(0));
|
||||
let mut registry = WipMountRegistry::new();
|
||||
registry.allocate_namespace("scope", "github").unwrap();
|
||||
registry
|
||||
.mount(projection("/github", "/github", "read", calls.clone()))
|
||||
.unwrap();
|
||||
registry
|
||||
.mount_subtree(WipSubtreeMount {
|
||||
root: "/github".into(),
|
||||
provider: Arc::new(ChangingProvider {
|
||||
generation: generation.clone(),
|
||||
started: started.clone(),
|
||||
release: release.clone(),
|
||||
calls: calls.clone(),
|
||||
}),
|
||||
})
|
||||
.unwrap();
|
||||
let runtime = WipRuntime::from_mounts(registry, "scope-test".into()).unwrap();
|
||||
runtime.inspect("/github".into(), false).await.unwrap();
|
||||
let pending = runtime.invoke(
|
||||
"/github".into(),
|
||||
contextual_reference("read", "/github"),
|
||||
"read".into(),
|
||||
json!({}),
|
||||
ToolExecutionContext::direct(),
|
||||
);
|
||||
let update = async {
|
||||
started.notified().await;
|
||||
generation.store(1, Ordering::SeqCst);
|
||||
let output = runtime.inspect("/github".into(), true).await.unwrap();
|
||||
assert!(output.content.unwrap().contains("integer"));
|
||||
release.notify_one();
|
||||
};
|
||||
let (old_result, ()) = tokio::time::timeout(std::time::Duration::from_secs(5), async {
|
||||
tokio::join!(pending, update)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
old_result.unwrap().content.as_deref(),
|
||||
Some("\"old result\"")
|
||||
);
|
||||
assert_eq!(calls.load(Ordering::SeqCst), 1);
|
||||
let state = runtime.state.lock().unwrap();
|
||||
let record = state
|
||||
.client
|
||||
.call_history(&state.session)
|
||||
.unwrap()
|
||||
.back()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
record.context.descriptor().operations[0].returns.r#type,
|
||||
TypeExpr::String
|
||||
);
|
||||
assert_eq!(
|
||||
state
|
||||
.client
|
||||
.interface(&state.session, &contextual_reference("read", "/github"))
|
||||
.unwrap()
|
||||
.descriptor
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.operations[0]
|
||||
.returns
|
||||
.r#type,
|
||||
TypeExpr::Integer
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user