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:
2026-10-09 00:12:38 +09:00
parent 60ec815237
commit 85c9b218dd
74 changed files with 20237 additions and 869 deletions
+1
View File
@@ -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 }
+1 -1
View File
@@ -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,
+4 -2
View File
@@ -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(),
+3 -3
View File
@@ -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
}
+3 -3
View File
@@ -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)),
},
+4 -4
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+562
View File
@@ -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
);
}