feat: integrate WIP 0.2.0 object-centered tools
This commit is contained in:
Generated
+16
-6
@@ -6339,9 +6339,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wip-client"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73a82416411af69b7eb4ad0ed9a954df062769d3a03b3a1c59b0b4bf617f3534"
|
||||
checksum = "def7dc8d056477bf8cb7c12f2dc70cabdb3eee3527cf95ba6131a8a44519ed7b"
|
||||
dependencies = [
|
||||
"wip-http",
|
||||
"wip-protocol",
|
||||
@@ -6349,9 +6349,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wip-http"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ec0d32d1e56e52fb9ce925db808354a3432b3ebbd112fab8c443c9b865c58c6"
|
||||
checksum = "da1f6a0023c0c599cbdc5571e9b131c8bb6b113ef04c20467d77634d66b44a5a"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"http",
|
||||
@@ -6363,9 +6363,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wip-protocol"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "275ffaebf544a077de7578fd3674c062a098a72c5a904cf66e934f76bd9efbab"
|
||||
checksum = "5a23ee0fe02badd2ef77847652cff241c6d868c8f162a911475500831ff348fe"
|
||||
|
||||
[[package]]
|
||||
name = "wip-text-view"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c95fd8e3fba673de44084c2f89c11b50d744c6db73726c50e04975c974d2759c"
|
||||
dependencies = [
|
||||
"wip-protocol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wiremock"
|
||||
@@ -6549,6 +6558,7 @@ dependencies = [
|
||||
"wip-client",
|
||||
"wip-http",
|
||||
"wip-protocol",
|
||||
"wip-text-view",
|
||||
"workdir",
|
||||
]
|
||||
|
||||
|
||||
+11
-3
@@ -161,8 +161,16 @@ tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||
url = "2.5"
|
||||
uuid = "1.23"
|
||||
wip-client = "0.1.0"
|
||||
wip-http = "0.1.0"
|
||||
wip-protocol = "0.1.0"
|
||||
# Released structured scope/lifetime contract and canonical Text View, one registry source.
|
||||
wip-client = "=0.2.0"
|
||||
wip-http = "=0.2.0"
|
||||
wip-protocol = "=0.2.0"
|
||||
wip-text-view = "=0.2.0"
|
||||
zeroize = "1"
|
||||
webauthn-rs = { version = "0.5.2", features = ["danger-allow-state-serialisation", "danger-credential-internals"] }
|
||||
|
||||
[workspace.metadata.wip-upstream]
|
||||
repository = "https://gitea.hareworks.net/Hare/wip-rs"
|
||||
version = "0.2.0"
|
||||
reference-revision = "6086f3c5ef10aa1464ce9c824750d7f667217bfd"
|
||||
source = "crates.io"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -23,8 +23,8 @@ use crate::feature::builtin::manage_workdir::wip::{decode_identity, encode_ident
|
||||
use crate::permission::permission_action_for;
|
||||
use crate::wip::{
|
||||
WipCallContext, WipMountError, WipMountRegistry, WipOperationError, WipOperationHandler,
|
||||
WipOperationOutput, WipProjection, WipProjectionKind, WipSubtreeMount, WipSubtreeProvider,
|
||||
json_to_wip, wip_to_json,
|
||||
WipOperationOutput, WipProjection, WipProjectionKind, WipPublication, WipSubtreeMount,
|
||||
WipSubtreeProvider, json_to_wip, wip_to_json,
|
||||
};
|
||||
|
||||
const ROOT: &str = "/checkouts";
|
||||
@@ -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(),
|
||||
@@ -286,7 +288,7 @@ impl Provider {
|
||||
|
||||
#[async_trait]
|
||||
impl WipSubtreeProvider for Arc<Provider> {
|
||||
async fn projection(&self, path: &str) -> Result<Option<WipProjection>, ProtocolError> {
|
||||
async fn publication(&self, path: &str) -> Result<Option<WipPublication>, ProtocolError> {
|
||||
let _permit = tokio::time::timeout(self.deadline, self.permits.acquire())
|
||||
.await
|
||||
.map_err(|_| {
|
||||
@@ -304,6 +306,7 @@ impl WipSubtreeProvider for Arc<Provider> {
|
||||
"checkout observation deadline",
|
||||
)
|
||||
})?
|
||||
.and_then(|projection| projection.map(WipPublication::self_scoped).transpose())
|
||||
}
|
||||
async fn children(&self, path: &str) -> Result<Vec<String>, ProtocolError> {
|
||||
let _permit = tokio::time::timeout(self.deadline, self.permits.acquire())
|
||||
|
||||
@@ -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()
|
||||
)),
|
||||
})
|
||||
|
||||
@@ -185,16 +185,8 @@ async fn observe(
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WipSubtreeProvider for ConfigProvider {
|
||||
fn max_depth(&self) -> u32 {
|
||||
MAX_DEPTH
|
||||
}
|
||||
fn max_nodes(&self) -> usize {
|
||||
MAX_NODES
|
||||
}
|
||||
|
||||
async fn projection(&self, path: &str) -> Result<Option<WipProjection>, ProtocolError> {
|
||||
impl ConfigProvider {
|
||||
async fn projection_at(&self, path: &str) -> Result<Option<WipProjection>, ProtocolError> {
|
||||
let source = source_path(path).map_err(protocol)?;
|
||||
let Some(attachment) = visible_metadata(self.backend.current_async().await)? else {
|
||||
return Ok(None);
|
||||
@@ -205,7 +197,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));
|
||||
};
|
||||
@@ -259,6 +251,26 @@ impl WipSubtreeProvider for ConfigProvider {
|
||||
}),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WipSubtreeProvider for ConfigProvider {
|
||||
fn max_depth(&self) -> u32 {
|
||||
MAX_DEPTH
|
||||
}
|
||||
fn max_nodes(&self) -> usize {
|
||||
MAX_NODES
|
||||
}
|
||||
|
||||
async fn publication(
|
||||
&self,
|
||||
path: &str,
|
||||
) -> Result<Option<crate::wip::WipPublication>, ProtocolError> {
|
||||
self.projection_at(path)
|
||||
.await?
|
||||
.map(crate::wip::WipPublication::self_scoped)
|
||||
.transpose()
|
||||
}
|
||||
|
||||
async fn children(&self, path: &str) -> Result<Vec<String>, ProtocolError> {
|
||||
let source = source_path(path).map_err(protocol)?;
|
||||
|
||||
@@ -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())),
|
||||
|
||||
+639
-792
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,540 @@
|
||||
//! Regression coverage for MR P1_CURRENT_ANCESTOR_SCOPE, through real adapters.
|
||||
use super::*;
|
||||
|
||||
struct State {
|
||||
visible: bool,
|
||||
identity: Option<String>,
|
||||
name: String,
|
||||
generation: usize,
|
||||
target_validator: Option<Vec<u8>>,
|
||||
lookups: Vec<String>,
|
||||
}
|
||||
|
||||
struct AncestorProvider {
|
||||
scope_path: String,
|
||||
target_path: String,
|
||||
state: Arc<Mutex<State>>,
|
||||
calls: Arc<AtomicUsize>,
|
||||
blocked: bool,
|
||||
started: Arc<tokio::sync::Notify>,
|
||||
release: Arc<tokio::sync::Notify>,
|
||||
}
|
||||
|
||||
impl AncestorProvider {
|
||||
fn scope_projection(&self, state: &State) -> WipProjection {
|
||||
let mut p = projection(
|
||||
&self.scope_path,
|
||||
&self.scope_path,
|
||||
&state.name,
|
||||
self.calls.clone(),
|
||||
);
|
||||
p.object.r#ref = state.identity.clone();
|
||||
self.version(p, state.generation)
|
||||
}
|
||||
|
||||
fn version(&self, mut p: WipProjection, generation: usize) -> WipProjection {
|
||||
p.descriptor.documentation = Some(Documentation {
|
||||
summary: format!("Published generation {generation}"),
|
||||
details: None,
|
||||
});
|
||||
if self.blocked {
|
||||
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(),
|
||||
});
|
||||
}
|
||||
p
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WipSubtreeProvider for AncestorProvider {
|
||||
async fn publication(&self, path: &str) -> Result<Option<WipPublication>, ProtocolError> {
|
||||
// One lock captures target, scope lifetime, descriptor/validator and
|
||||
// handler. No separate scope lookup can splice new identity onto old data.
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.lookups.push(path.into());
|
||||
if path == self.scope_path {
|
||||
return if state.visible {
|
||||
WipPublication::self_scoped(self.scope_projection(&state)).map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
};
|
||||
}
|
||||
if path == "/github" && self.scope_path != path {
|
||||
return if state.visible {
|
||||
WipPublication::self_scoped(projection(path, path, "root", self.calls.clone()))
|
||||
.map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
};
|
||||
}
|
||||
if path != self.target_path {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut target = projection(path, &self.scope_path, &state.name, self.calls.clone());
|
||||
target.object.validator = state.target_validator.clone();
|
||||
let target = self.version(target, state.generation);
|
||||
// Retain a target artifact after deleting the scope to exercise Host
|
||||
// fail-closed behavior, not merely a provider that already hides orphans.
|
||||
let scope = state.visible.then(|| self.scope_projection(&state).object);
|
||||
Ok(Some(WipPublication {
|
||||
projection: target,
|
||||
scope,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn children(&self, path: &str) -> Result<Vec<String>, ProtocolError> {
|
||||
let state = self.state.lock().unwrap();
|
||||
if !state.visible {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
if path == self.scope_path {
|
||||
return Ok(vec![self.target_path.clone()]);
|
||||
}
|
||||
if path == "/github" && path != self.scope_path {
|
||||
return Ok(vec![self.scope_path.clone()]);
|
||||
}
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
fn fixture(scope: &str, with_ref: bool, blocked: bool) -> (WipRuntime, Arc<AncestorProvider>) {
|
||||
let provider = Arc::new(AncestorProvider {
|
||||
scope_path: scope.into(),
|
||||
target_path: if scope == "/" {
|
||||
"/item".into()
|
||||
} else {
|
||||
format!("{scope}/item")
|
||||
},
|
||||
state: Arc::new(Mutex::new(State {
|
||||
visible: true,
|
||||
identity: with_ref.then(|| "live-scope".into()),
|
||||
name: "read".into(),
|
||||
generation: 0,
|
||||
target_validator: None,
|
||||
lookups: vec![],
|
||||
})),
|
||||
calls: Arc::new(AtomicUsize::new(0)),
|
||||
blocked,
|
||||
started: Arc::new(tokio::sync::Notify::new()),
|
||||
release: Arc::new(tokio::sync::Notify::new()),
|
||||
});
|
||||
let mut mounts = WipMountRegistry::new();
|
||||
mounts.allocate_namespace("scope", "github").unwrap();
|
||||
let mount_root = if scope == "/" { "/" } else { "/github" };
|
||||
let mut placeholder = projection(mount_root, mount_root, "read", provider.calls.clone());
|
||||
placeholder.object.r#ref = Some("registration-placeholder".into());
|
||||
mounts.mount(placeholder).unwrap();
|
||||
mounts
|
||||
.mount_subtree(WipSubtreeMount {
|
||||
root: mount_root.into(),
|
||||
provider: provider.clone(),
|
||||
})
|
||||
.unwrap();
|
||||
(
|
||||
WipRuntime::from_mounts(mounts, "ancestor-test".into()).unwrap(),
|
||||
provider,
|
||||
)
|
||||
}
|
||||
|
||||
fn reference(scope: &str, name: &str) -> InterfaceReference {
|
||||
InterfaceReference {
|
||||
scope: scope.into(),
|
||||
name: name.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_root_and_intermediate_ancestors_use_one_current_publication_not_placeholder() {
|
||||
for scope in ["/", "/github", "/github/team"] {
|
||||
for with_ref in [true, false] {
|
||||
let (runtime, provider) = fixture(scope, with_ref, false);
|
||||
let selected = reference(scope, "read");
|
||||
let fetched = runtime.host.fetch_interface_live(&selected).await.unwrap();
|
||||
assert_eq!(fetched.scope_ref, with_ref.then(|| "live-scope".into()));
|
||||
runtime
|
||||
.inspect(provider.target_path.clone(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
provider.state.lock().unwrap().lookups.clear();
|
||||
runtime
|
||||
.invoke(
|
||||
provider.target_path.clone(),
|
||||
selected,
|
||||
"read".into(),
|
||||
json!({}),
|
||||
ToolExecutionContext::direct(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(provider.calls.load(Ordering::SeqCst), 1);
|
||||
assert_eq!(
|
||||
provider.state.lock().unwrap().lookups,
|
||||
[provider.target_path.clone()]
|
||||
);
|
||||
let state = runtime.state.lock().unwrap();
|
||||
let call = state
|
||||
.client
|
||||
.call_history(&state.session)
|
||||
.unwrap()
|
||||
.back()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
call.context.request().interface.scope_ref,
|
||||
fetched.scope_ref
|
||||
);
|
||||
drop(state);
|
||||
// A specified ref must still match even when the current scope has none.
|
||||
assert_eq!(
|
||||
rejected_code(
|
||||
&runtime,
|
||||
metadata_request(
|
||||
&provider.target_path,
|
||||
json!({"scope":scope,"name":"read"}),
|
||||
None,
|
||||
Some("wrong")
|
||||
)
|
||||
)
|
||||
.await,
|
||||
"interface_mismatch"
|
||||
);
|
||||
assert_eq!(provider.calls.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deletion_without_prior_scope_inspect_rejects_cached_invoke_and_orphan_observation() {
|
||||
for scope in ["/github", "/github/team"] {
|
||||
for with_ref in [true, false] {
|
||||
let (runtime, provider) = fixture(scope, with_ref, false);
|
||||
let selected = reference(scope, "read");
|
||||
runtime
|
||||
.inspect(provider.target_path.clone(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
provider.state.lock().unwrap().visible = false;
|
||||
let failed = runtime
|
||||
.invoke(
|
||||
provider.target_path.clone(),
|
||||
selected.clone(),
|
||||
"read".into(),
|
||||
json!({}),
|
||||
ToolExecutionContext::direct(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(failed.to_string().contains("InterfaceMismatch"), "{failed}");
|
||||
assert_eq!(provider.calls.load(Ordering::SeqCst), 0);
|
||||
assert!(
|
||||
runtime
|
||||
.inspect(provider.target_path.clone(), true)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
runtime
|
||||
.tree(provider.target_path.clone(), 0, true)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
runtime
|
||||
.host
|
||||
.fetch_interface_live(&selected)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.code,
|
||||
ProtocolErrorCode::InterfaceNotFound
|
||||
);
|
||||
let root = runtime.tree("/".into(), 3, true).await.unwrap();
|
||||
assert!(!root.content.unwrap().contains("/github"));
|
||||
assert_eq!(provider.calls.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn republishing_scope_never_restores_old_registration_even_without_refs() {
|
||||
for with_ref in [true, false] {
|
||||
let (runtime, provider) = fixture("/github", with_ref, false);
|
||||
let old = reference("/github", "read");
|
||||
runtime
|
||||
.inspect(provider.target_path.clone(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
provider.state.lock().unwrap().visible = false;
|
||||
assert_eq!(
|
||||
runtime
|
||||
.host
|
||||
.fetch_interface_live(&old)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.code,
|
||||
ProtocolErrorCode::InterfaceNotFound
|
||||
);
|
||||
{
|
||||
let mut state = provider.state.lock().unwrap();
|
||||
state.visible = true;
|
||||
state.identity = with_ref.then(|| "replacement".into());
|
||||
state.name = "new".into();
|
||||
state.generation = 1;
|
||||
}
|
||||
// The old root registration and cached old descriptor both still exist.
|
||||
assert_eq!(
|
||||
runtime
|
||||
.host
|
||||
.fetch_interface_live(&old)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.code,
|
||||
ProtocolErrorCode::InterfaceNotFound
|
||||
);
|
||||
assert!(
|
||||
runtime
|
||||
.invoke(
|
||||
provider.target_path.clone(),
|
||||
old,
|
||||
"read".into(),
|
||||
json!({}),
|
||||
ToolExecutionContext::direct()
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(provider.calls.load(Ordering::SeqCst), 0);
|
||||
runtime
|
||||
.inspect(provider.target_path.clone(), true)
|
||||
.await
|
||||
.unwrap();
|
||||
let new = reference("/github", "new");
|
||||
assert_eq!(
|
||||
runtime
|
||||
.host
|
||||
.fetch_interface_live(&new)
|
||||
.await
|
||||
.unwrap()
|
||||
.scope_ref,
|
||||
with_ref.then(|| "replacement".into())
|
||||
);
|
||||
runtime
|
||||
.invoke(
|
||||
provider.target_path.clone(),
|
||||
new,
|
||||
"read".into(),
|
||||
json!({}),
|
||||
ToolExecutionContext::direct(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(provider.calls.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_ancestor_preserves_wire_shape_target_and_object_validator_precedence() {
|
||||
let (runtime, provider) = fixture("/github", false, false);
|
||||
{
|
||||
let mut state = provider.state.lock().unwrap();
|
||||
state.visible = false;
|
||||
state.target_validator = Some(vec![1]);
|
||||
}
|
||||
assert_eq!(
|
||||
rejected_code(
|
||||
&runtime,
|
||||
metadata_request("/missing", json!("old"), None, None)
|
||||
)
|
||||
.await,
|
||||
"invalid_request"
|
||||
);
|
||||
assert_eq!(
|
||||
rejected_code(
|
||||
&runtime,
|
||||
metadata_request(
|
||||
"/missing",
|
||||
json!({"scope":"/github","name":"read"}),
|
||||
Some("AA=="),
|
||||
None
|
||||
)
|
||||
)
|
||||
.await,
|
||||
"not_found"
|
||||
);
|
||||
assert_eq!(
|
||||
rejected_code(
|
||||
&runtime,
|
||||
metadata_request(
|
||||
&provider.target_path,
|
||||
json!({"scope":"/github","name":"read"}),
|
||||
Some("AA=="),
|
||||
None
|
||||
)
|
||||
)
|
||||
.await,
|
||||
"validator_mismatch"
|
||||
);
|
||||
assert_eq!(
|
||||
rejected_code(
|
||||
&runtime,
|
||||
metadata_request(
|
||||
&provider.target_path,
|
||||
json!({"scope":"/github","name":"read"}),
|
||||
Some("AQ=="),
|
||||
None
|
||||
)
|
||||
)
|
||||
.await,
|
||||
"interface_mismatch"
|
||||
);
|
||||
assert_eq!(provider.calls.load(Ordering::SeqCst), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ancestor_replacement_during_in_flight_call_keeps_the_original_descriptor_and_handler() {
|
||||
let (runtime, provider) = fixture("/github/team", true, true);
|
||||
let selected = reference("/github/team", "read");
|
||||
runtime
|
||||
.inspect(provider.target_path.clone(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
let pending = runtime.invoke(
|
||||
provider.target_path.clone(),
|
||||
selected.clone(),
|
||||
"read".into(),
|
||||
json!({}),
|
||||
ToolExecutionContext::direct(),
|
||||
);
|
||||
let update = async {
|
||||
provider.started.notified().await;
|
||||
{
|
||||
let mut state = provider.state.lock().unwrap();
|
||||
state.identity = Some("replacement".into());
|
||||
state.generation = 1;
|
||||
}
|
||||
let updated = runtime
|
||||
.inspect(provider.target_path.clone(), true)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(updated.content.unwrap().contains("integer"));
|
||||
provider.release.notify_one();
|
||||
};
|
||||
let (old, ()) = tokio::time::timeout(std::time::Duration::from_secs(5), async {
|
||||
tokio::join!(pending, update)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(old.unwrap().content.as_deref(), Some("\"old result\""));
|
||||
assert_eq!(provider.calls.load(Ordering::SeqCst), 1);
|
||||
let state = runtime.state.lock().unwrap();
|
||||
let history = state.client.call_history(&state.session).unwrap();
|
||||
let old = history.back().unwrap();
|
||||
assert_eq!(
|
||||
old.context.request().interface.scope_ref.as_deref(),
|
||||
Some("live-scope")
|
||||
);
|
||||
assert_eq!(
|
||||
old.context.descriptor().operations[0].returns.r#type,
|
||||
TypeExpr::String
|
||||
);
|
||||
drop(state);
|
||||
let new = runtime
|
||||
.invoke(
|
||||
provider.target_path.clone(),
|
||||
selected,
|
||||
"read".into(),
|
||||
json!({}),
|
||||
ToolExecutionContext::direct(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(new.content.as_deref(), Some("2"));
|
||||
assert_eq!(provider.calls.load(Ordering::SeqCst), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn same_name_scope_replacement_or_loss_of_ref_rejects_old_metadata_without_replay() {
|
||||
for identity in [Some("replacement"), None] {
|
||||
let (runtime, provider) = fixture("/github/team", true, false);
|
||||
let selected = reference("/github/team", "read");
|
||||
runtime
|
||||
.inspect(provider.target_path.clone(), false)
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let mut state = provider.state.lock().unwrap();
|
||||
state.identity = identity.map(Into::into);
|
||||
if identity.is_some() {
|
||||
state.generation = 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
rejected_code(
|
||||
&runtime,
|
||||
metadata_request(
|
||||
&provider.target_path,
|
||||
json!({"scope":"/github/team","name":"read"}),
|
||||
None,
|
||||
Some("live-scope")
|
||||
)
|
||||
)
|
||||
.await,
|
||||
"interface_mismatch"
|
||||
);
|
||||
let failed = runtime
|
||||
.invoke(
|
||||
provider.target_path.clone(),
|
||||
selected.clone(),
|
||||
"read".into(),
|
||||
json!({}),
|
||||
ToolExecutionContext::direct(),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(failed.to_string().contains("InterfaceMismatch"), "{failed}");
|
||||
assert_eq!(provider.calls.load(Ordering::SeqCst), 0);
|
||||
let fetched = runtime.host.fetch_interface_live(&selected).await.unwrap();
|
||||
assert_eq!(fetched.scope_ref.as_deref(), identity);
|
||||
assert_eq!(
|
||||
fetched.descriptor.documentation.unwrap().summary,
|
||||
if identity.is_some() {
|
||||
"Published generation 1"
|
||||
} else {
|
||||
"Published generation 0"
|
||||
}
|
||||
);
|
||||
runtime
|
||||
.inspect(provider.target_path.clone(), true)
|
||||
.await
|
||||
.unwrap();
|
||||
runtime
|
||||
.invoke(
|
||||
provider.target_path.clone(),
|
||||
selected,
|
||||
"read".into(),
|
||||
json!({}),
|
||||
ToolExecutionContext::direct(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(provider.calls.load(Ordering::SeqCst), 1);
|
||||
let state = runtime.state.lock().unwrap();
|
||||
let call = state
|
||||
.client
|
||||
.call_history(&state.session)
|
||||
.unwrap()
|
||||
.back()
|
||||
.unwrap();
|
||||
// Omitted scope_ref is never filled from the placeholder or other observations.
|
||||
assert_eq!(
|
||||
call.context.request().interface.scope_ref.as_deref(),
|
||||
identity
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,565 @@
|
||||
//! Scope publication and precondition tests through the actual Yoi HTTP adapter.
|
||||
use super::*;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize};
|
||||
|
||||
#[path = "ancestor_tests.rs"]
|
||||
mod ancestor_tests;
|
||||
|
||||
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 publication(&self, path: &str) -> Result<Option<WipPublication>, 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(),
|
||||
});
|
||||
WipPublication::self_scoped(p).map(Some)
|
||||
}
|
||||
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
|
||||
);
|
||||
}
|
||||
@@ -181,9 +181,9 @@ truncation under the existing search contract.
|
||||
|
||||
## Typical sequence
|
||||
|
||||
1. Discover `/checkouts` with depth 1; inspect its Interface and call `list`.
|
||||
2. Discover an entrance or known deep directory, inspect it, call `glob`/`grep`.
|
||||
3. Follow a returned file `path`, discover/inspect, call line `read`.
|
||||
1. Use Tree on `/checkouts` with depth 1; Inspect `/checkouts` directly and Invoke `list`.
|
||||
2. Inspect an entrance or a known deep directory directly; Invoke `glob`/`grep`.
|
||||
3. Follow a returned file `path`, Inspect that path and Invoke line `read`.
|
||||
4. Call `edit` or `write`; Client-managed validators protect the exact observed
|
||||
object while the shared tracker enforces the prior Read.
|
||||
5. Rediscover after external changes; a stale rejection is not permission to skip
|
||||
|
||||
@@ -165,16 +165,16 @@ Ticket/Objective/Merge Request routes retain their T-698 contracts.
|
||||
|
||||
## First-Workspace example
|
||||
|
||||
1. `Discover(path="/", depth=1)` finds `repositories`, `workdirs` and
|
||||
1. `Tree(path="/", depth=1)` finds `repositories`, `workdirs` and
|
||||
`workdir-attachments` when permitted.
|
||||
2. Inspect the returned Repository collection Interface and call `list` with a
|
||||
2. Inspect the Repository collection path and Invoke `list` with a
|
||||
bounded limit. An empty registered/visible catalog explicitly returns an empty
|
||||
list; do not invent a key. Follow `next_cursor` when supplied.
|
||||
3. Discover the returned item `path`, inspect its Interface and call `read`.
|
||||
3. Inspect the returned item `path` directly and Invoke `read`.
|
||||
`default_selector: null` means no configured default, not `main`.
|
||||
4. Inspect the Workdir collection and `create` using the discovered formal key,
|
||||
with optional selector, display name and Runtime under the existing contract.
|
||||
5. Follow created `item.path`, discover/inspect and `attach` with a chosen
|
||||
5. Follow created `item.path`, Inspect directly and Invoke `attach` with a chosen
|
||||
Worker-local alias. This is separate from creation.
|
||||
6. Follow `attachments_path`, `list`, then discover/read the returned connection
|
||||
path to confirm alias, Workdir and effective capabilities.
|
||||
|
||||
@@ -24,15 +24,17 @@ worker = { mode = "wip" }
|
||||
|
||||
Omission resolves to `tools`, so existing manifests, profiles, persisted snapshots, and restored Workers retain the ordinary individual-tool surface. A direct Internal SubWorker selected with `profile = "inherit"` inherits the parent mode as reusable behavior. `default` and named profile selections use that profile's own mode and do not implicitly inherit WIP.
|
||||
|
||||
The implementation uses the crates.io `wip-client`, `wip-protocol`, and `wip-http` **0.1.0** releases. It does not use Git/path dependencies or the old `wip-https` name. Those releases require Rust 1.88; the workspace declares `rust-version = "1.88"`, and the changed crates inherit it. WIP Host/interface crates are not needed for the async compatibility adapter: the published client/protocol/HTTP state machines remain authoritative while Yoi supplies an in-process async Host transport.
|
||||
The implementation uses the published **WIP 0.2.0** Client, HTTP, Protocol and official Text View packages from **crates.io**. Exact workspace version constraints and Cargo.lock registry checksums reproduce one Protocol type source; no local attachment, path override, Git snapshot or old registry 0.1.0 substitutes for the release. Canonical scope/Text View contracts were checked against wip-reference **6086f3c5ef10aa1464ce9c824750d7f667217bfd**. See [validation and resolution checks](../development/wip-integration-validation.md). CI/Nix dependency acquisition requires no upstream Git credentials. Yoi retains its async provider/transport and existing execution boundaries; upstream owns Client cache/lifecycle, codecs and signatures.
|
||||
|
||||
## LLM surface and route contract
|
||||
|
||||
A WIP Worker exposes exactly these generic LLM tools instead of exposing every enabled ordinary tool twice:
|
||||
|
||||
- `Discover`: observe an object path and bounded descendants, beginning at `/`.
|
||||
- `Inspect`: fetch the descriptor for an opaque interface reference returned by discovery.
|
||||
- `Call`: call an operation using fresh object and interface observations.
|
||||
- `Tree(path, depth)`: retrieve the full indexable range within depth 0..8, or report incomplete/loading/failed/limited coverage as an error. Children at the requested boundary remain unobserved (null), not empty. Cached nonindexable paths are never included by prefix scanning Known Space.
|
||||
- `Inspect(path)`: directly acquire an Object, then every published Interface, and return complete official Object/Interface signatures in declaration order. It requires no prior Tree or manual descriptor fetching. The path is explicit JSON envelope context outside the path-free signature. Summaries are complete; runtime metadata and documentation details are not appended to signatures.
|
||||
- `Invoke(path, interface, operation, arguments)`: use a structured `{scope, name}` reference and named argument record. The Client owns validators and observed scope_ref. Missing/stale/failed observations are recovered with at most one pre-dispatch retrieval per subject. A dispatched operation is never automatically retried.
|
||||
|
||||
Tree and Inspect accept explicit `refresh: true` for blocked observations. An ensure API returning None is never itself readiness evidence: freshness, complete edge coverage and retained data are checked. Loading and capacity pressure are reported rather than superseded or busy-looped.
|
||||
|
||||
The compatibility projection mounts enabled ordinary tools at:
|
||||
|
||||
@@ -40,13 +42,13 @@ The compatibility projection mounts enabled ordinary tools at:
|
||||
/tools/<exact-tool-registration-name>
|
||||
```
|
||||
|
||||
Each object has one opaque interface reference:
|
||||
Each compatibility Object publishes an explicit root-scoped reference:
|
||||
|
||||
```text
|
||||
yoi.tool/<exact-tool-registration-name>/v1
|
||||
{"scope":"/", "name":"yoi.tool/<exact-tool-registration-name>/v1"}
|
||||
```
|
||||
|
||||
Its single operation is `call`. For this compatibility route, `Call.arguments` is the original ordinary tool argument object. Native projections instead receive a JSON object keyed by the descriptor's declared parameter names. Native input is decoded with the published `wip-http` descriptor-bound codec, so named and composite types are resolved, an integral JSON number remains a WIP `Number` when declared as such, and WIP `Bytes` use the codec's canonical RFC 4648 base64 JSON representation. Tool argument IDs remain JSON values; the adapter never infers a domain object or Worldspace route from them.
|
||||
Its single operation is `call`. For this compatibility route, `Invoke.arguments` is `{ "input": <original ordinary tool argument object> }`, matching the descriptor's named Json parameter. Native projections instead receive a JSON object keyed by the descriptor's declared parameter names. Native input is decoded with the published `wip-http` descriptor-bound codec, so named and composite types are resolved, an integral JSON number remains a WIP `Number` when declared as such, and WIP `Bytes` use the codec's canonical RFC 4648 base64 JSON representation. Tool argument IDs remain JSON values; the adapter never infers a domain object or Worldspace route from them.
|
||||
|
||||
The interface descriptor uses WIP `Json` for compatibility input and includes the exact original JSON Schema in descriptor documentation. Before execution, the Host validates the JSON value with that original schema and then delegates to the original async `Tool`. Constraints are therefore neither approximated nor silently dropped. A schema that cannot be compiled prevents WIP startup instead of creating a weaker projection.
|
||||
|
||||
@@ -65,7 +67,7 @@ Object resolution and operations are separate contracts. One mounted `WipProject
|
||||
- A native projection replaces a compatibility projection either by the original same-route semantic capability or by an explicit registry capability claim owned by a mounted native route.
|
||||
- Conflicting native claims fail startup. When native is selected, the claimed compatibility tool is hidden rather than exposed through a second entry. Normal Tool mode is unaffected because it does not install the WIP registry.
|
||||
|
||||
`Inspect` renders the complete effective descriptor, including contributed operations, descriptor-local named declarations, and recursive record, list, enum, and union shapes with required flags and declaration documentation. The tests `host_namespace_object_resolution_and_operation_contributions_are_independent`, `operation_contributions_reject_interface_and_target_conflicts`, and `native_projection_preserves_descriptor_and_decodes_typed_arguments_end_to_end` demonstrate root allocation, unique resolution, contribution merging, collision rejection, current call-time authorization, complete descriptor inspection, and descriptor-typed invocation.
|
||||
`Inspect` delegates to official `wip-text-view::render_object` and `render_interface`; it renders the complete effective descriptor, including contributed operations, descriptor-local named declarations, and recursive record, list, enum, and union shapes with required flags and declaration documentation. The tests `host_namespace_object_resolution_and_operation_contributions_are_independent`, `operation_contributions_reject_interface_and_target_conflicts`, and `native_projection_preserves_descriptor_and_decodes_typed_arguments_end_to_end` demonstrate root allocation, unique resolution, contribution merging, collision rejection, current call-time authorization, complete descriptor inspection, and descriptor-typed invocation.
|
||||
|
||||
The built-in native route migration is:
|
||||
|
||||
@@ -83,7 +85,7 @@ The built-in Repository/Workdir reference provider allocates `/repositories`, `/
|
||||
2. Mount a native collection `WipProjection` at that root with capability `repository:collection`, matching Object name and one declared interface. Static Objects underneath the root use the same capability owner prefix. A mounted collection retains its own identity, interfaces, and validator even when it has static children.
|
||||
3. For dynamic items, register one `WipDynamicMount` on the collection with capability `repository:item`. A foreign capability owner is rejected. Its `WipDynamicItemResolver` owns reference acceptance, Object identity, and current Object validator. It returns only a bounded direct child; it must not reinterpret a reference as another namespace or grant permission from existence.
|
||||
4. The reference provider supplies its read operations and handlers independently of management Feature enablement. An Object-only provider may use an empty operation descriptor. Enabled management Features register `WipOperationContribution` for static Objects or `WipDynamicOperationContribution` for a dynamic family; they do not allocate the namespace again or replace its resolver.
|
||||
5. Contributions use the existing interface reference and identical format, documentation, and type declarations, supplying only their new operations. Interface references are global: extending a descriptor that is also mounted elsewhere must not create different descriptors/validators under the same reference (registration fails). Use separate references for collection and item contracts. Failed contributions leave the registry unchanged.
|
||||
5. Contributions use the existing interface reference and identical format, documentation, and type declarations, supplying only their new operations. Interface references are exact (scope, name) keys: extending a descriptor that is also mounted elsewhere must not create different descriptors/validators under the same reference (registration fails). Use separate references for collection and item contracts. Failed contributions leave the registry unchanged.
|
||||
6. Handlers are responsible for checking current target capability and current subject authorization before dispatch. Registration/discovery is metadata, not a grant. A resolver/handler must retain the original permission identity and domain audit/failure contracts. Revocation after discovery must fail before provider dispatch. Compatibility mounts are restricted to `/tools/<tool-name>` and cannot bypass namespace ownership or republish legacy routes.
|
||||
|
||||
Tests `management_enablement_is_independent_of_read_and_operation_permissions` and `contributed_interface_invalidates_observations_and_restore_requires_discovery` cover all eight enablement/read/manage combinations, direct-call rejection, effective descriptor changes, fresh runtime isolation under the same Worker identity, reset, and post-observation revocation.
|
||||
@@ -103,13 +105,13 @@ The Worldspace contains only the operations registered from enabled Features and
|
||||
|
||||
Filesystem scope, provider Workdir capability checks, Backend Workspace authority, Feature enablement, and input-specific restrictions stay in the original tool implementation and are checked again at invocation. `ask` permissions remain denied fail-closed because the runtime has no approval protocol. The gateway does not mint authority or expose authentication material.
|
||||
|
||||
Pre/post Engine history still records one bounded `Call` result, while the compatibility result retains the original `ToolOutput`, including attachments and normal Engine output pruning. WIP call audit records retain the route, operation, request identity, and terminal classification.
|
||||
Pre/post Engine history still records one bounded `Invoke` result, while the compatibility result retains the original `ToolOutput`, including attachments and normal Engine output pruning. WIP call audit records retain the route, operation, request identity, and terminal classification.
|
||||
|
||||
## Client lifetime, restoration, and compaction
|
||||
|
||||
The stateful `wip-client::Client` is owned by one Worker WIP runtime. Sessions are keyed by the canonical endpoint and an opaque security-context identity derived by the Host from Workspace, Worker, and session identity. Authentication credentials are not included in that identity and are never emitted to descriptors, tool output, or history.
|
||||
|
||||
Known Space and interface observations are therefore not shared across Workers, endpoints, or security contexts. A restored Worker creates a new runtime/client and must explore again. `Discover(reset = true)` drops all observations before reconnect/authority-change exploration; `refresh = true` explicitly supersedes one cached observation. A normal compaction keeps the live runtime but the three gateway schemas are supplied again on every LLM request, including the instruction to begin discovery at `/`; the compacted transcript is not treated as cache authority.
|
||||
Known Space and interface observations are therefore not shared across Workers, endpoints, or security contexts. A restored Worker creates a new runtime/client and must explore again. `refresh = true` explicitly supersedes requested retrievals. Old string references in saved history remain historical text, not cache/wire authority; observations are runtime-only and are not persisted or restored. Resume by inspecting the target path, not by migrating an opaque display string. A normal compaction keeps the live runtime but the three gateway schemas are supplied again on every LLM request, including direct Inspect and indexable Tree semantics; the compacted transcript is not treated as cache authority.
|
||||
|
||||
Saved Session/history entries remain append-only evidence and are never rewritten or replayed as calls. In particular, historical `/features/<feature>/...` text stays displayable but is not a current route alias. Passing such a legacy path to discovery or call returns `NotFound` with guidance to rediscover from `/`; the Host does not translate it, resolve it under another namespace, or expose old and new routes in parallel.
|
||||
|
||||
@@ -138,3 +140,13 @@ The runtime tracks bounded counters for:
|
||||
- successful operations.
|
||||
|
||||
These values are returned with discovery/inspection output and are intended for comparing prompt input volume, exploration round trips, and representative operation success with ordinary Tool mode. No fixed reduction percentage is promised.
|
||||
|
||||
## Scoped publication and display boundaries
|
||||
|
||||
References are structured everywhere; wire requests reject legacy/display strings. Root and self scopes are valid; ancestry uses canonical path segments, not string prefixes. Contextual descriptors use the resolved Object path as their explicit self scope, not an encoded suffix on the local name. Fetching a self-scoped Interface freezes descriptor/validator and optional scope Object ref from the same live projection. A scope without a ref is valid; the adapter does not invent one. Calls check target existence, Object preconditions, scope/membership/ref, then Interface preconditions and arguments, with the selected descriptor and handler pinned for execution/result validation. Existing providers repeat current authorization and their captured Workdir/Workspace metadata checks at their execution/commit boundary. The registered artifacts alone are not call authority.
|
||||
|
||||
Separate Interface namespaces on one static Object can be registered with `mount_interface`; the owner/identity cannot be replaced and duplicate Interface keys fail. Inspect preserves each Interface reference, even with identical operation names, and Invoke selects the exact namespace. Contribution merging retains one Interface and disjoint operation names.
|
||||
|
||||
Official signature quoting, escapes and summary layout are preserved verbatim. Host documentation is untrusted data in Tool output, not instructions. RenderError categories and the failed path/reference are reported as display failures. Library input limits and Yoi envelope limits reject the entire display rather than truncating it or reporting no operations. No signature parser, shorthand-input parser, independent lexer, or custom escape engine is installed.
|
||||
|
||||
Typed Entry results remain values; no eager result acquisition, guessed string-path conversion, or inferred indexable edges occur. A later Inspect failure never reclassifies a completed successful operation. FS indexable/list policy is a separate provider concern (T-714), not Client-side search/ranking.
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
# Scoped WIP integration validation
|
||||
|
||||
## Released dependency and resolution
|
||||
|
||||
The adopted Client, HTTP, Protocol and official Text View are **crates.io 0.2.0**,
|
||||
with exact `=0.2.0` workspace declarations and registry checksums in Cargo.lock.
|
||||
`cargo info <package>@0.2.0` confirmed all four published packages (Rust 1.88).
|
||||
`node scripts/verify-wip-dependencies.mjs` checks actual locked/offline Cargo
|
||||
resolution: all WIP packages use registry 0.2.0, each required package occurs
|
||||
once, and Protocol has one source. No path/Git override or attached checkout is
|
||||
used. The earlier unreleased 0.1.0 source snapshot and vendor verifier were
|
||||
removed when T-713 thread sequence 26 specified 0.2.0.
|
||||
|
||||
Canonical contract reference: wip-reference
|
||||
`6086f3c5ef10aa1464ce9c824750d7f667217bfd`. This reference documents the contract;
|
||||
Cargo resolves the release, not that checkout or an unreleased Rust revision.
|
||||
|
||||
## Executed checks on registry 0.2.0
|
||||
|
||||
- `cargo test -p worker --locked --offline`: **894 unit + 110 integration tests
|
||||
passed**, no ignored/failed tests. This includes ordinary Tools mode, Feature
|
||||
permission/revocation, restore, session/history and controller integration.
|
||||
- Narrow development checks: `wip::binding` (25), `wip::provider_tests` (12), existing
|
||||
`wip::tests` and workdir projection tests (43), checkout transports/providers
|
||||
(20), workspace-config production/authority tests (19). These are included
|
||||
in the complete worker run.
|
||||
- Published SDK tests via `cargo test -p wip-client -p wip-http -p wip-protocol
|
||||
-p wip-text-view --locked --offline`: Text View **16 public signature/token/golden
|
||||
tests + 4 doctests**; Client **63 adapter/runtime tests**; HTTP **38 tests + 1
|
||||
doctest**; Protocol **22 tests + 1 doctest**. Tests run from the registry packages,
|
||||
not substituted source or removed vendor directories.
|
||||
- Root `cargo check --locked --offline` passed, including normal TUI, CLI,
|
||||
Runtime and Workspace Server compile closure.
|
||||
- `cargo fmt --all -- --check` and `git diff --check HEAD` passed.
|
||||
- `nix build --no-link .#yoi.cargoDeps` passed with the updated registry dependency
|
||||
hash. This validates credential-free dependency acquisition, not a full Nix
|
||||
binary/image build or deployment update.
|
||||
|
||||
## Current target compatibility (preview, not integration)
|
||||
|
||||
Source runtime at `23cf6c4f882ce78985ba4bd673751cebe64e2d87` and current develop
|
||||
`b5ea162f838fc36e364edec9ab6689292b59cffd` produce the conflict-free `git merge-tree`
|
||||
preview tree `7ef3545b635cbdfc5e24658f6d1532e0f8b0947c`. An archive of this tree,
|
||||
without a branch/ref change, passed root locked/offline Cargo check, all **91**
|
||||
Server API TypeScript tests (including OpenAPI/checked-in generator freshness),
|
||||
registry WIP resolution checks, and Nix dependency acquisition with the same hash.
|
||||
T-722's API macros, Server API/Drive/Server source, OpenAPI and generated TS blobs
|
||||
are unchanged from develop in the combined tree; no artifact overwrite or
|
||||
regeneration is necessary for this source change. The later added same-name scope
|
||||
regression changes tests only, not the validated runtime or shared contracts.
|
||||
|
||||
The initial archive check reused a Cargo target directory; returning to the older
|
||||
main checkout exposed stale preview Server API artifacts. All local workspace
|
||||
artifacts were invalidated, and a fresh main root check and complete **894 + 110**
|
||||
Worker suite passed. Use an isolated target directory for future cross-tree checks.
|
||||
No target push, integration branch, deployment, or live dogfood update was made.
|
||||
Orchestrator readiness/integration must still resolve the then-current target.
|
||||
|
||||
## Contract evidence
|
||||
|
||||
`crates/worker/src/wip/binding.rs` tests exercise fresh/loading/stale/failed and
|
||||
capacity-blocked acquisition, explicit refresh, superseded responses, retrieval
|
||||
ownership cleanup, output limits, direct unseen-path Inspect, official unmodified
|
||||
signature/path envelopes, structured Interface input and complete edge-only trees.
|
||||
They invoke the registered three-tool surface for a search returning typed Entry
|
||||
paths, inspect only a selected nonindexable path, dispatch the selected operation,
|
||||
and verify that a later Inspect failure neither fails nor replays an earlier
|
||||
successful operation. Interface acquisition failures are not empty operation lists.
|
||||
|
||||
`provider_tests.rs` exercises the actual Yoi adapter for root/self scope, optional
|
||||
refs, copied scope_ref, different scopes with identical names/operations, shape vs
|
||||
membership errors, validator/ref absence/mismatch and protocol failure precedence.
|
||||
It replaces a dynamic scope Object identity, observes both stale sides after
|
||||
InterfaceMismatch, reacquires without replay, and confirms direct NotFound ends
|
||||
safe Interface reuse. An in-flight operation survives descriptor refresh using
|
||||
its exact frozen Host and Client descriptors rather than the newer return type.
|
||||
|
||||
`ancestor_tests.rs` covers the independent review finding P1_CURRENT_ANCESTOR_SCOPE:
|
||||
valid live ancestor refs that differ from registration placeholders, root `/` and
|
||||
intermediate provider-owned ancestors, ref-less and ref-bearing scope deletion
|
||||
without a prior scope Inspect, rejected cached dispatch and orphan observation,
|
||||
replacement/republication without restoring old Interface registrations,
|
||||
same-name scope replacement or loss of public ref rejecting copied old metadata
|
||||
without replay (and later explicit Inspect/Invoke copying only the new optional ref),
|
||||
shape/target/Object-validator/Interface-mismatch precedence, and frozen in-flight
|
||||
ancestor Descriptor/handler/result validation after replacement. Valid Invoke
|
||||
makes one provider publication call for the target, not a separate scope lookup.
|
||||
|
||||
Existing checkout tests retain commit-time captured Workdir/connection/validator
|
||||
checks, alias reuse/restore fencing, inode replacement rejection, authorization,
|
||||
shared prior-read tracking, exact post-operation validators, cooperative
|
||||
cancellation and unknown/lost/malformed responses without retries. Existing native
|
||||
Ticket/Objective/Merge Request tests retain scoped Workspace authority and Feature
|
||||
permission separation; compatibility Invoke retains original schema validation,
|
||||
permission identity and ToolOutput/attachments.
|
||||
|
||||
## Downstream provider seams
|
||||
|
||||
- Public projection/contribution Interface fields use
|
||||
`wip_protocol::InterfaceReference { scope, name }` from registry 0.2.0.
|
||||
- `WipSubtreeProvider::publication` returns a `WipPublication` that captures target,
|
||||
current scope Object, Descriptor/validator and handler at one provider boundary.
|
||||
Mutable ancestors must not be resolved independently after selecting a target.
|
||||
`scope: None` ends usable publication, even without a public ref; it is not a
|
||||
ref-less published Object. Host checks and observation never substitute the
|
||||
root registration placeholder, and Interface fetch never falls back to retained
|
||||
provider registrations. Old registrations must end when their scope lifetime
|
||||
ends. Published ancestor scopes outside the subtree remain Host-owned.
|
||||
- Contextual/self-scope publishers use `WipPublication::self_scoped` on their exact
|
||||
provider snapshot; references are explicit paths, not an encoded `/@/` suffix.
|
||||
Checkout and Workspace Config retain their captured provider/Backend execution
|
||||
and commit fences. Ref-less publication remains legitimate.
|
||||
- `WipRuntime::{tree, inspect, invoke}` is the object-centered integration seam.
|
||||
Inspect uses official summary-only signatures and typed rendering failures.
|
||||
- `WipMountRegistry::mount_interface` adds a separate static Interface namespace
|
||||
without replacing Object ownership or flattening same-named operations. It does
|
||||
not overlay stale registration metadata onto a live subtree provider.
|
||||
- FS indexable boundaries and directory.list remain provider policy. This change
|
||||
deliberately does not implement the separate T-714 boundary/list migration.
|
||||
Relevant transport/commit tests are `checkout_wip_tests.rs`,
|
||||
`checkout_http_tests.rs`, and `checkout_race_tests.rs`.
|
||||
- Worldspace state is runtime-only. Historical opaque references in append-only
|
||||
session/tool history are not migrated into cache or parsed for a guessed scope;
|
||||
a restored Client starts with fresh observations and direct path Inspect.
|
||||
+2
-1
@@ -43,7 +43,8 @@ rustPlatform.buildRustPackage rec {
|
||||
filter = sourceFilter;
|
||||
};
|
||||
|
||||
cargoHash = "sha256-LjswbsalQA0Yny0DIBJg3FiqwdmTADZ1CnqNJ9oo9Kw=";
|
||||
# All four WIP SDK crates resolve from crates.io 0.2.0 via Cargo.lock checksums.
|
||||
cargoHash = "sha256-L1RR+zdcVxMGvEonGxERgcvzjxzXro4n2j2syBeR/m0=";
|
||||
|
||||
depsExtraArgs = {
|
||||
# Older fetchCargoVendor utilities used crates.io's API download endpoint,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env node
|
||||
// Verify actual Cargo resolution, not just version strings in workspace manifests.
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const cwd = fileURLToPath(new URL('../', import.meta.url));
|
||||
const host = execFileSync('rustc', ['-vV'], { cwd, encoding: 'utf8' })
|
||||
.match(/^host: (.+)$/m)?.[1];
|
||||
assert.ok(host, 'rustc must report the validation host');
|
||||
const metadata = JSON.parse(execFileSync('cargo', [
|
||||
'metadata', '--locked', '--offline', '--format-version=1', '--filter-platform', host,
|
||||
], { cwd, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }));
|
||||
const expected = ['wip-client', 'wip-http', 'wip-protocol', 'wip-text-view'];
|
||||
const source = 'registry+https://github.com/rust-lang/crates.io-index';
|
||||
const sdk = metadata.packages.filter(pkg => pkg.name.startsWith('wip-'));
|
||||
for (const pkg of sdk) {
|
||||
assert.equal(pkg.version, '0.2.0', `${pkg.name}: no old WIP version is permitted`);
|
||||
assert.equal(pkg.source, source, `${pkg.name}: must resolve from crates.io, not a path/Git substitute`);
|
||||
}
|
||||
for (const name of expected) {
|
||||
assert.equal(sdk.filter(pkg => pkg.name === name).length, 1, `${name}: expected one package source`);
|
||||
}
|
||||
console.log(`Verified ${expected.join(', ')} at crates.io 0.2.0; one Protocol source (${host})`);
|
||||
Reference in New Issue
Block a user