refactor: bind checkout target lifetimes to Object observations
This commit is contained in:
@@ -116,13 +116,21 @@ impl Provider {
|
|||||||
}
|
}
|
||||||
Some((decode_identity(slug)?, WorkdirPath::new(tail).ok()?))
|
Some((decode_identity(slug)?, WorkdirPath::new(tail).ok()?))
|
||||||
}
|
}
|
||||||
fn wrap_validator(&self, alias: &str, generation: u64, workdir: &str, raw: &[u8]) -> Vec<u8> {
|
fn wrap_validator(
|
||||||
|
&self,
|
||||||
|
alias: &str,
|
||||||
|
generation: u64,
|
||||||
|
workdir: &str,
|
||||||
|
target: &WorkdirPath,
|
||||||
|
raw: &[u8],
|
||||||
|
) -> Vec<u8> {
|
||||||
let mut hash = Sha256::new();
|
let mut hash = Sha256::new();
|
||||||
for value in [
|
for value in [
|
||||||
self.incarnation.as_bytes(),
|
self.incarnation.as_bytes(),
|
||||||
alias.as_bytes(),
|
alias.as_bytes(),
|
||||||
&generation.to_be_bytes(),
|
&generation.to_be_bytes(),
|
||||||
workdir.as_bytes(),
|
workdir.as_bytes(),
|
||||||
|
target.as_str().as_bytes(),
|
||||||
raw,
|
raw,
|
||||||
] {
|
] {
|
||||||
hash.update(value.len().to_be_bytes());
|
hash.update(value.len().to_be_bytes());
|
||||||
@@ -287,7 +295,8 @@ impl Provider {
|
|||||||
"directory"
|
"directory"
|
||||||
};
|
};
|
||||||
let primary = *contracts.first()?;
|
let primary = *contracts.first()?;
|
||||||
let validator = self.wrap_validator(alias, generation, workdir, &observation.validator);
|
let validator =
|
||||||
|
self.wrap_validator(alias, generation, workdir, &target, &observation.validator);
|
||||||
let handler = Arc::new(FileHandler {
|
let handler = Arc::new(FileHandler {
|
||||||
provider: self.clone(),
|
provider: self.clone(),
|
||||||
alias: alias.into(),
|
alias: alias.into(),
|
||||||
@@ -511,8 +520,13 @@ impl FileHandler {
|
|||||||
if let Some(raw) = result.validator {
|
if let Some(raw) = result.validator {
|
||||||
Ok(WipOperationOutput::native_with_validator(
|
Ok(WipOperationOutput::native_with_validator(
|
||||||
value,
|
value,
|
||||||
self.provider
|
self.provider.wrap_validator(
|
||||||
.wrap_validator(&self.alias, self.generation, &self.workdir, &raw),
|
&self.alias,
|
||||||
|
self.generation,
|
||||||
|
&self.workdir,
|
||||||
|
&self.target,
|
||||||
|
&raw,
|
||||||
|
),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(WipOperationOutput::native(value))
|
Ok(WipOperationOutput::native(value))
|
||||||
@@ -533,6 +547,16 @@ impl WipOperationHandler for FileHandler {
|
|||||||
if !self.provider.allowed("Read",&json!({"target_workdir":self.alias,"file_path":self.target.as_str(),"offset":offset,"limit":limit})) {
|
if !self.provider.allowed("Read",&json!({"target_workdir":self.alias,"file_path":self.target.as_str(),"offset":offset,"limit":limit})) {
|
||||||
return Err(protocol(ProtocolErrorCode::PermissionDenied,"text read denied"));
|
return Err(protocol(ProtocolErrorCode::PermissionDenied,"text read denied"));
|
||||||
}
|
}
|
||||||
|
let _permit =
|
||||||
|
tokio::time::timeout(self.provider.deadline, self.provider.permits.acquire())
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
protocol(
|
||||||
|
ProtocolErrorCode::ResourceLimitExceeded,
|
||||||
|
"text read capacity deadline",
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.map_err(|_| protocol(ProtocolErrorCode::Internal, "text read unavailable"))?;
|
||||||
let result = tokio::time::timeout(
|
let result = tokio::time::timeout(
|
||||||
self.provider.deadline,
|
self.provider.deadline,
|
||||||
tools::checkout::read_checkout_text(
|
tools::checkout::read_checkout_text(
|
||||||
@@ -569,8 +593,13 @@ impl WipOperationHandler for FileHandler {
|
|||||||
return Ok(match validator {
|
return Ok(match validator {
|
||||||
Some(raw) => WipOperationOutput::native_with_validator(
|
Some(raw) => WipOperationOutput::native_with_validator(
|
||||||
value,
|
value,
|
||||||
self.provider
|
self.provider.wrap_validator(
|
||||||
.wrap_validator(&self.alias, self.generation, &self.workdir, &raw),
|
&self.alias,
|
||||||
|
self.generation,
|
||||||
|
&self.workdir,
|
||||||
|
&self.target,
|
||||||
|
&raw,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
None => WipOperationOutput::native(value),
|
None => WipOperationOutput::native(value),
|
||||||
});
|
});
|
||||||
@@ -723,14 +752,6 @@ fn join_under(parent: &WorkdirPath, relative: &str) -> Result<String, WipOperati
|
|||||||
format!("{}/{}", parent.as_str(), child.as_str())
|
format!("{}/{}", parent.as_str(), child.as_str())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
fn parameter(name: &str, required: bool, r#type: TypeExpr) -> ParameterDeclaration {
|
|
||||||
ParameterDeclaration {
|
|
||||||
name: name.into(),
|
|
||||||
required,
|
|
||||||
documentation: None,
|
|
||||||
r#type,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn operation(
|
fn operation(
|
||||||
name: &str,
|
name: &str,
|
||||||
summary: &str,
|
summary: &str,
|
||||||
|
|||||||
@@ -358,7 +358,8 @@ async fn observe_interface(runtime: &WipRuntime, path: &str) -> wip_protocol::In
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn native_call(runtime: &WipRuntime, path: &str, operation: &str, args: Json) -> Value {
|
async fn native_call(runtime: &WipRuntime, path: &str, operation: &str, args: Json) -> Value {
|
||||||
let interface = observe_interface(runtime, path).await;
|
observe_interface(runtime, path).await;
|
||||||
|
let interface = super::checkout_wip_tests::contract(operation).reference();
|
||||||
runtime
|
runtime
|
||||||
.call(
|
.call(
|
||||||
path.into(),
|
path.into(),
|
||||||
@@ -620,7 +621,8 @@ async fn committed_write_unknown(fault: WriteResponseFault) {
|
|||||||
let (runtime, router) = native_runtime(remote);
|
let (runtime, router) = native_runtime(remote);
|
||||||
let file = "/checkouts/main/src/deep/a.txt";
|
let file = "/checkouts/main/src/deep/a.txt";
|
||||||
native_call(&runtime, file, "read", json!({})).await;
|
native_call(&runtime, file, "read", json!({})).await;
|
||||||
let interface = observe_interface(&runtime, file).await;
|
observe_interface(&runtime, file).await;
|
||||||
|
let interface = super::checkout_wip_tests::contract("write").reference();
|
||||||
*fixture.state.write_fault.lock().unwrap() = Some(fault);
|
*fixture.state.write_fault.lock().unwrap() = Some(fault);
|
||||||
let error = runtime
|
let error = runtime
|
||||||
.call(
|
.call(
|
||||||
|
|||||||
@@ -683,7 +683,12 @@ async fn committed_checkout_mutation_losing_connection_is_unknown_not_rejected_o
|
|||||||
let calls = Arc::new(AtomicUsize::new(0));
|
let calls = Arc::new(AtomicUsize::new(0));
|
||||||
let route = object_path("main", &file.target);
|
let route = object_path("main", &file.target);
|
||||||
let mut projection = p.node(&route).await.unwrap().unwrap();
|
let mut projection = p.node(&route).await.unwrap().unwrap();
|
||||||
let interface = projection.interface.clone();
|
let interface = match operation {
|
||||||
|
"write" => crate::file_operation::Contract::Write,
|
||||||
|
"edit" => crate::file_operation::Contract::Edit,
|
||||||
|
_ => crate::file_operation::Contract::CreateFile,
|
||||||
|
}
|
||||||
|
.reference();
|
||||||
projection.handler = Arc::new(DetachBeforePublication {
|
projection.handler = Arc::new(DetachBeforePublication {
|
||||||
file,
|
file,
|
||||||
calls: calls.clone(),
|
calls: calls.clone(),
|
||||||
|
|||||||
@@ -41,13 +41,28 @@ async fn interface(r: &WipRuntime, path: &str) -> wip_protocol::InterfaceReferen
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
p.interface
|
p.interface
|
||||||
}
|
}
|
||||||
|
pub(super) fn contract(operation: &str) -> crate::file_operation::Contract {
|
||||||
|
use crate::file_operation::Contract as C;
|
||||||
|
match operation {
|
||||||
|
"read" => C::ReadLines,
|
||||||
|
"search" => C::SearchText,
|
||||||
|
"edit" => C::Edit,
|
||||||
|
"write" => C::Write,
|
||||||
|
"list" => C::List,
|
||||||
|
"glob" => C::Glob,
|
||||||
|
"grep" => C::Grep,
|
||||||
|
"create_file" => C::CreateFile,
|
||||||
|
_ => panic!("unknown file operation {operation}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
async fn call(
|
async fn call(
|
||||||
r: &WipRuntime,
|
r: &WipRuntime,
|
||||||
path: &str,
|
path: &str,
|
||||||
operation: &str,
|
operation: &str,
|
||||||
args: Json,
|
args: Json,
|
||||||
) -> Result<ToolOutput, ToolError> {
|
) -> Result<ToolOutput, ToolError> {
|
||||||
let i = interface(r, path).await;
|
interface(r, path).await;
|
||||||
|
let i = contract(operation).reference();
|
||||||
r.call(path.into(), i, operation.into(), args, Default::default())
|
r.call(path.into(), i, operation.into(), args, Default::default())
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -336,12 +351,26 @@ async fn checkout_wip_detach_alias_reuse_and_restore_fence_old_references() {
|
|||||||
let old = r.host.projection_live(path).await.unwrap().unwrap();
|
let old = r.host.projection_live(path).await.unwrap().unwrap();
|
||||||
router.detach(&alias).await.unwrap();
|
router.detach(&alias).await.unwrap();
|
||||||
assert!(r.host.projection_live(path).await.unwrap().is_none());
|
assert!(r.host.projection_live(path).await.unwrap().is_none());
|
||||||
assert!(r.host.fetch_interface_live(&old.interface).await.is_err());
|
assert_eq!(
|
||||||
|
r.host
|
||||||
|
.fetch_interface_live(&old.interface)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.validator,
|
||||||
|
old.interface_validator
|
||||||
|
);
|
||||||
router.attach(alias, session(&b, false)).unwrap();
|
router.attach(alias, session(&b, false)).unwrap();
|
||||||
let new = r.host.projection_live(path).await.unwrap().unwrap();
|
let new = r.host.projection_live(path).await.unwrap().unwrap();
|
||||||
assert_ne!(old.interface, new.interface);
|
assert_eq!(old.interface, new.interface);
|
||||||
assert_ne!(old.object.validator, new.object.validator);
|
assert_ne!(old.object.validator, new.object.validator);
|
||||||
assert!(r.host.fetch_interface_live(&old.interface).await.is_err());
|
assert_eq!(
|
||||||
|
r.host
|
||||||
|
.fetch_interface_live(&old.interface)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.validator,
|
||||||
|
old.interface_validator
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
call(
|
call(
|
||||||
&r,
|
&r,
|
||||||
@@ -354,14 +383,16 @@ async fn checkout_wip_detach_alias_reuse_and_restore_fence_old_references() {
|
|||||||
);
|
);
|
||||||
let restored = runtime(router, None);
|
let restored = runtime(router, None);
|
||||||
let next = restored.host.projection_live(path).await.unwrap().unwrap();
|
let next = restored.host.projection_live(path).await.unwrap().unwrap();
|
||||||
assert_ne!(new.interface, next.interface);
|
assert_eq!(new.interface, next.interface);
|
||||||
assert_ne!(new.object.validator, next.object.validator);
|
assert_ne!(new.object.validator, next.object.validator);
|
||||||
assert!(
|
assert_eq!(
|
||||||
restored
|
restored
|
||||||
.host
|
.host
|
||||||
.fetch_interface_live(&new.interface)
|
.fetch_interface_live(&new.interface)
|
||||||
.await
|
.await
|
||||||
.is_err()
|
.unwrap()
|
||||||
|
.validator,
|
||||||
|
new.interface_validator
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
std::fs::read_to_string(b.path().join("file.txt")).unwrap(),
|
std::fs::read_to_string(b.path().join("file.txt")).unwrap(),
|
||||||
@@ -497,7 +528,7 @@ async fn checkout_wip_post_operation_validators_support_cached_chains() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let r = runtime(router, None);
|
let r = runtime(router, None);
|
||||||
let path = "/checkouts/main/file.txt";
|
let path = "/checkouts/main/file.txt";
|
||||||
let i = interface(&r, path).await;
|
interface(&r, path).await;
|
||||||
// No rediscovery or Interface refresh between any of these operations.
|
// No rediscovery or Interface refresh between any of these operations.
|
||||||
for (op, args) in [
|
for (op, args) in [
|
||||||
("read", json!({})),
|
("read", json!({})),
|
||||||
@@ -505,16 +536,23 @@ async fn checkout_wip_post_operation_validators_support_cached_chains() {
|
|||||||
("write", json!({"content":"saved\n"})),
|
("write", json!({"content":"saved\n"})),
|
||||||
("read", json!({})),
|
("read", json!({})),
|
||||||
] {
|
] {
|
||||||
r.call(path.into(), i.clone(), op.into(), args, Default::default())
|
r.call(
|
||||||
.await
|
path.into(),
|
||||||
.unwrap();
|
contract(op).reference(),
|
||||||
|
op.into(),
|
||||||
|
args,
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
std::fs::read_to_string(dir.path().join("file.txt")).unwrap(),
|
std::fs::read_to_string(dir.path().join("file.txt")).unwrap(),
|
||||||
"saved\n"
|
"saved\n"
|
||||||
);
|
);
|
||||||
let parent = "/checkouts/main";
|
let parent = "/checkouts/main";
|
||||||
let i = interface(&r, parent).await;
|
interface(&r, parent).await;
|
||||||
|
let i = contract("create_file").reference();
|
||||||
for name in ["one.txt", "nested/two.txt"] {
|
for name in ["one.txt", "nested/two.txt"] {
|
||||||
r.call(
|
r.call(
|
||||||
parent.into(),
|
parent.into(),
|
||||||
@@ -617,11 +655,12 @@ async fn checkout_wip_scoped_child_search_and_revoke_cannot_expose_siblings() {
|
|||||||
.is_none()
|
.is_none()
|
||||||
);
|
);
|
||||||
let file = "/checkouts/child/a.txt";
|
let file = "/checkouts/child/a.txt";
|
||||||
let i = interface(&child, file).await;
|
interface(&child, file).await;
|
||||||
|
let i = contract("write").reference();
|
||||||
child
|
child
|
||||||
.call(
|
.call(
|
||||||
file.into(),
|
file.into(),
|
||||||
i.clone(),
|
contract("read").reference(),
|
||||||
"read".into(),
|
"read".into(),
|
||||||
json!({}),
|
json!({}),
|
||||||
Default::default(),
|
Default::default(),
|
||||||
@@ -686,7 +725,21 @@ pub(super) async fn model_invoke(
|
|||||||
operation: &str,
|
operation: &str,
|
||||||
arguments: Json,
|
arguments: Json,
|
||||||
) -> Result<ToolOutput, ToolError> {
|
) -> Result<ToolOutput, ToolError> {
|
||||||
model_operation(r, "Invoke", json!({"path":inspected["path"], "interface":inspected["interfaces"][0]["reference"], "operation":operation, "arguments":arguments})).await
|
let reference = inspected["interfaces"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|item| &item["reference"])
|
||||||
|
.find(|r| {
|
||||||
|
Some(wip_protocol::InterfaceReference {
|
||||||
|
scope: r["scope"].as_str().unwrap().into(),
|
||||||
|
name: r["name"].as_str().unwrap().into(),
|
||||||
|
})
|
||||||
|
.and_then(|r| crate::file_operation::Contract::from_reference(&r))
|
||||||
|
.is_some_and(|c| c.operation() == operation)
|
||||||
|
})
|
||||||
|
.unwrap_or(&inspected["interfaces"][0]["reference"]);
|
||||||
|
model_operation(r, "Invoke", json!({"path":inspected["path"], "interface":reference, "operation":operation, "arguments":arguments})).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -752,10 +805,7 @@ async fn checkout_model_list_glob_grep_entries_are_lazy_and_never_become_tree_ed
|
|||||||
let r = Arc::new(runtime(router, None));
|
let r = Arc::new(runtime(router, None));
|
||||||
let entrance = model_inspect(r.clone(), "/checkouts/main").await;
|
let entrance = model_inspect(r.clone(), "/checkouts/main").await;
|
||||||
assert_eq!(entrance["path"], "/checkouts/main");
|
assert_eq!(entrance["path"], "/checkouts/main");
|
||||||
assert_eq!(
|
assert_eq!(entrance["interfaces"][0]["reference"]["scope"], "/");
|
||||||
entrance["interfaces"][0]["reference"]["scope"],
|
|
||||||
"/checkouts/main"
|
|
||||||
);
|
|
||||||
let projection = r
|
let projection = r
|
||||||
.host
|
.host
|
||||||
.projection_live("/checkouts/main")
|
.projection_live("/checkouts/main")
|
||||||
@@ -829,10 +879,7 @@ async fn checkout_model_list_glob_grep_entries_are_lazy_and_never_become_tree_ed
|
|||||||
json!([])
|
json!([])
|
||||||
);
|
);
|
||||||
let deep = model_inspect(r.clone(), "/checkouts/main/nested/deep").await;
|
let deep = model_inspect(r.clone(), "/checkouts/main/nested/deep").await;
|
||||||
assert_eq!(
|
assert_eq!(deep["interfaces"][0]["reference"]["scope"], "/");
|
||||||
deep["interfaces"][0]["reference"]["scope"],
|
|
||||||
"/checkouts/main"
|
|
||||||
);
|
|
||||||
for (operation, args) in [
|
for (operation, args) in [
|
||||||
("list", json!({})),
|
("list", json!({})),
|
||||||
("glob", json!({"pattern":"*.txt"})),
|
("glob", json!({"pattern":"*.txt"})),
|
||||||
@@ -926,7 +973,7 @@ async fn checkout_list_permission_cannot_be_bypassed_with_glob_or_command_identi
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn checkout_ancestor_scope_rejects_siblings_prefixes_refs_and_old_connection_before_read() {
|
async fn common_scope_rejects_wrong_namespace_scope_ref_and_old_target_authority() {
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
std::fs::write(dir.path().join("a.txt"), "same\n").unwrap();
|
std::fs::write(dir.path().join("a.txt"), "same\n").unwrap();
|
||||||
let router = Arc::new(WorkdirSessionRouter::new());
|
let router = Arc::new(WorkdirSessionRouter::new());
|
||||||
@@ -943,8 +990,8 @@ async fn checkout_ancestor_scope_rejects_siblings_prefixes_refs_and_old_connecti
|
|||||||
let p = r.host.projection_live(path).await.unwrap().unwrap();
|
let p = r.host.projection_live(path).await.unwrap().unwrap();
|
||||||
let fetched = r.host.fetch_interface_live(&p.interface).await.unwrap();
|
let fetched = r.host.fetch_interface_live(&p.interface).await.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
fetched.scope_ref.is_none(),
|
fetched.scope_ref.is_some(),
|
||||||
"ref-less publication is legitimate"
|
"common namespace uses the Host root scope"
|
||||||
);
|
);
|
||||||
// Ordinary content changes do not terminate the entrance's Interface lifetime.
|
// Ordinary content changes do not terminate the entrance's Interface lifetime.
|
||||||
std::fs::write(dir.path().join("new.txt"), "fixture").unwrap();
|
std::fs::write(dir.path().join("new.txt"), "fixture").unwrap();
|
||||||
@@ -959,12 +1006,12 @@ async fn checkout_ancestor_scope_rejects_siblings_prefixes_refs_and_old_connecti
|
|||||||
let request = |reference, scope_ref| CallOperationRequest {
|
let request = |reference, scope_ref| CallOperationRequest {
|
||||||
target: wip_protocol::Target {
|
target: wip_protocol::Target {
|
||||||
path: path.into(),
|
path: path.into(),
|
||||||
validator: None,
|
validator: p.object.validator.clone(),
|
||||||
},
|
},
|
||||||
interface: wip_protocol::InterfaceTarget {
|
interface: wip_protocol::InterfaceTarget {
|
||||||
reference,
|
reference,
|
||||||
scope_ref,
|
scope_ref,
|
||||||
validator: None,
|
validator: fetched.validator.clone(),
|
||||||
},
|
},
|
||||||
operation: "read".into(),
|
operation: "read".into(),
|
||||||
arguments: BTreeMap::new(),
|
arguments: BTreeMap::new(),
|
||||||
@@ -996,6 +1043,200 @@ async fn checkout_ancestor_scope_rejects_siblings_prefixes_refs_and_old_connecti
|
|||||||
router.attach(main, session(&dir, true)).unwrap();
|
router.attach(main, session(&dir, true)).unwrap();
|
||||||
let forged = request(p.interface.clone(), None);
|
let forged = request(p.interface.clone(), None);
|
||||||
assert!(
|
assert!(
|
||||||
matches!(r.host.call(forged, WipCallContext { execution:Default::default(),security_context:"fixture".into() }).await, Err(WipOperationError::Protocol(e)) if e.code == ProtocolErrorCode::InterfaceMismatch)
|
matches!(r.host.call(forged, WipCallContext { execution:Default::default(),security_context:"fixture".into() }).await, Err(WipOperationError::Protocol(e)) if e.code == ProtocolErrorCode::ValidatorMismatch)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn observed_common_request(
|
||||||
|
path: &str,
|
||||||
|
validator: Option<Vec<u8>>,
|
||||||
|
fetched: &FetchInterfaceResponse,
|
||||||
|
operation: &str,
|
||||||
|
) -> CallOperationRequest {
|
||||||
|
CallOperationRequest {
|
||||||
|
target: wip_protocol::Target {
|
||||||
|
path: path.into(),
|
||||||
|
validator,
|
||||||
|
},
|
||||||
|
interface: wip_protocol::InterfaceTarget {
|
||||||
|
reference: fetched.interface.clone(),
|
||||||
|
scope_ref: fetched.scope_ref.clone(),
|
||||||
|
validator: fetched.validator.clone(),
|
||||||
|
},
|
||||||
|
operation: operation.into(),
|
||||||
|
arguments: BTreeMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn file_context() -> WipCallContext {
|
||||||
|
WipCallContext {
|
||||||
|
execution: Default::default(),
|
||||||
|
security_context: "file-contract-test".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn shared_contract_survives_one_attachment_update_but_old_object_authority_never_transfers() {
|
||||||
|
let left = TempDir::new().unwrap();
|
||||||
|
let right = TempDir::new().unwrap();
|
||||||
|
for root in [&left, &right] {
|
||||||
|
std::fs::write(root.path().join("a.txt"), "same\n").unwrap();
|
||||||
|
}
|
||||||
|
std::fs::write(left.path().join("b.txt"), "same\n").unwrap();
|
||||||
|
let router = Arc::new(WorkdirSessionRouter::new());
|
||||||
|
let alias = WorkdirAttachmentAlias::new("left").unwrap();
|
||||||
|
router.attach(alias.clone(), session(&left, false)).unwrap();
|
||||||
|
router
|
||||||
|
.attach(
|
||||||
|
WorkdirAttachmentAlias::new("right").unwrap(),
|
||||||
|
session(&right, true),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let r = runtime(router.clone(), None);
|
||||||
|
let path = "/checkouts/left/a.txt";
|
||||||
|
let other = "/checkouts/right/a.txt";
|
||||||
|
let first = r.host.projection_live(path).await.unwrap().unwrap();
|
||||||
|
let same_bytes = r
|
||||||
|
.host
|
||||||
|
.projection_live("/checkouts/left/b.txt")
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
let independent = r.host.projection_live(other).await.unwrap().unwrap();
|
||||||
|
assert_eq!(first.interface, independent.interface);
|
||||||
|
assert_eq!(first.descriptor, independent.descriptor);
|
||||||
|
assert_ne!(first.object.validator, same_bytes.object.validator);
|
||||||
|
let definition = r.host.fetch_interface_live(&first.interface).await.unwrap();
|
||||||
|
let request =
|
||||||
|
observed_common_request(path, first.object.validator.clone(), &definition, "read");
|
||||||
|
assert!(r.host.call(request.clone(), file_context()).await.is_ok());
|
||||||
|
// Replace the same path with a different inode containing exactly the same bytes.
|
||||||
|
std::fs::write(left.path().join("replacement"), "same\n").unwrap();
|
||||||
|
std::fs::rename(left.path().join("replacement"), left.path().join("a.txt")).unwrap();
|
||||||
|
assert!(
|
||||||
|
matches!(r.host.call(request.clone(),file_context()).await,Err(WipOperationError::Protocol(e)) if e.code==ProtocolErrorCode::ValidatorMismatch)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
r.host
|
||||||
|
.fetch_interface_live(&first.interface)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.validator,
|
||||||
|
definition.validator
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
r.host
|
||||||
|
.call(
|
||||||
|
observed_common_request(
|
||||||
|
other,
|
||||||
|
independent.object.validator.clone(),
|
||||||
|
&definition,
|
||||||
|
"read"
|
||||||
|
),
|
||||||
|
file_context()
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
call(&r, path, "read", json!({})).await.unwrap();
|
||||||
|
router.detach(&alias).await.unwrap();
|
||||||
|
router.attach(alias, session(&left, false)).unwrap();
|
||||||
|
assert!(
|
||||||
|
matches!(r.host.call(request,file_context()).await,Err(WipOperationError::Protocol(e)) if e.code==ProtocolErrorCode::ValidatorMismatch)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
r.host
|
||||||
|
.call(
|
||||||
|
observed_common_request(other, independent.object.validator, &definition, "read"),
|
||||||
|
file_context()
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
call(&r, path, "read", json!({})).await.unwrap();
|
||||||
|
let before_restore = r.host.projection_live(path).await.unwrap().unwrap();
|
||||||
|
let restored = runtime(router, None);
|
||||||
|
let restored_definition = restored
|
||||||
|
.host
|
||||||
|
.fetch_interface_live(&first.interface)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(definition.descriptor, restored_definition.descriptor);
|
||||||
|
assert_eq!(definition.validator, restored_definition.validator);
|
||||||
|
assert!(
|
||||||
|
matches!(restored.host.call(observed_common_request(path,before_restore.object.validator,&restored_definition,"read"),file_context()).await,Err(WipOperationError::Protocol(e)) if e.code==ProtocolErrorCode::ValidatorMismatch)
|
||||||
|
);
|
||||||
|
call(&restored, path, "read", json!({})).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn shared_readonly_contract_rejects_wrong_membership_operation_and_interface_validator() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
std::fs::write(dir.path().join("a.txt"), "雪.*\r\nother\n").unwrap();
|
||||||
|
let router = Arc::new(WorkdirSessionRouter::new());
|
||||||
|
router
|
||||||
|
.attach(
|
||||||
|
WorkdirAttachmentAlias::new("main").unwrap(),
|
||||||
|
session(&dir, true),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let r = Arc::new(runtime(router, None));
|
||||||
|
let path = "/checkouts/main/a.txt";
|
||||||
|
let observed = model_inspect(r.clone(), path).await;
|
||||||
|
let read = json_content(
|
||||||
|
model_invoke(r.clone(), &observed, "read", json!({"limit":1}))
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
read,
|
||||||
|
json!({"content":" 1\t雪.*\n","next_offset":1,"truncated":true})
|
||||||
|
);
|
||||||
|
let searched = json_content(
|
||||||
|
model_invoke(
|
||||||
|
r.clone(),
|
||||||
|
&observed,
|
||||||
|
"search",
|
||||||
|
json!({"pattern":".*","limit":1}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
searched,
|
||||||
|
json!({"matches":[{"line":1,"text":"雪.*"}],"next_offset":1,"truncated":true})
|
||||||
|
);
|
||||||
|
let p = r.host.projection_live(path).await.unwrap().unwrap();
|
||||||
|
let read = r
|
||||||
|
.host
|
||||||
|
.fetch_interface_live(&contract("read").reference())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let write = r
|
||||||
|
.host
|
||||||
|
.fetch_interface_live(&contract("write").reference())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
matches!(r.host.call(observed_common_request(path,p.object.validator.clone(),&write,"write"),file_context()).await,Err(WipOperationError::Protocol(e)) if e.code==ProtocolErrorCode::InterfaceMismatch)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
matches!(r.host.call(observed_common_request(path,p.object.validator.clone(),&read,"missing"),file_context()).await,Err(WipOperationError::Protocol(e)) if e.code==ProtocolErrorCode::OperationNotFound)
|
||||||
|
);
|
||||||
|
let mut stale = observed_common_request(path, p.object.validator.clone(), &read, "read");
|
||||||
|
stale.interface.validator = Some(vec![0]);
|
||||||
|
assert!(
|
||||||
|
matches!(r.host.call(stale,file_context()).await,Err(WipOperationError::Protocol(e)) if e.code==ProtocolErrorCode::InterfaceValidatorMismatch)
|
||||||
|
);
|
||||||
|
let mut legacy = observed_common_request(path, p.object.validator, &read, "read");
|
||||||
|
legacy.interface.reference = InterfaceReference {
|
||||||
|
scope: "/checkouts/main".into(),
|
||||||
|
name: "yoi.checkout/old/file/v1#target/old".into(),
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
matches!(r.host.call(legacy,file_context()).await,Err(WipOperationError::Protocol(e)) if e.code==ProtocolErrorCode::InterfaceMismatch)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(dir.path().join("a.txt")).unwrap(),
|
||||||
|
"雪.*\r\nother\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user