fix(client): preserve observation freshness

This commit is contained in:
2026-09-30 08:59:53 +09:00
parent 2da2a28a67
commit 20b3986ab6
2 changed files with 470 additions and 21 deletions
+250
View File
@@ -252,6 +252,10 @@ fn tree_expansion_is_lazy_truncated_and_refreshes_edges() {
.unwrap();
assert!(client.tree(&session, "/").unwrap().children.is_empty());
assert!(!client.tree(&session, "/").unwrap().truncated);
assert_eq!(
client.object(&session, "/items").unwrap().state,
ObservationState::Stale
);
}
#[test]
@@ -486,6 +490,252 @@ fn protocol_and_transport_unknown_outcomes_stale_without_retry() {
assert_eq!(client.call_history(&session).unwrap().len(), 2);
}
#[test]
fn overlapping_tree_work_preserves_newer_subject_request_ownership() {
let mut client = client(8, 8, 8, 8);
let session = client
.open_session("https://example.test/wip", SecurityContext::new("subject"))
.unwrap();
let tree_request = FetchTreeRequest {
path: "/".into(),
depth: 1,
limit: 4,
};
let tree_value = FetchTreeResponse {
root: ObjectTreeNode {
object: object("", None, b"root"),
children: vec![ObjectTreeNode {
object: object("child", None, b"tree-child"),
children: vec![],
}],
},
truncated: false,
};
let old_tree = client.prepare_tree(&session, "/", 1, 4).unwrap();
let newer_fetch = client.prepare_fetch(&session, "/child").unwrap();
client
.complete(
old_tree.id,
encode_fetch_tree_response(&tree_request, &tree_value, wire_limits()).unwrap(),
)
.unwrap();
assert!(matches!(
client.object(&session, "/child").unwrap().state,
ObservationState::Loading(id) if id == newer_fetch.id
));
client
.complete(
newer_fetch.id,
fetch_response("/child", &object("child", None, b"newer-fetch")),
)
.unwrap();
assert_eq!(
client.object(&session, "/child").unwrap().validator,
Some(b"newer-fetch".to_vec())
);
let old_tree = client.prepare_tree(&session, "/", 1, 4).unwrap();
let newer_child_tree = client.prepare_tree(&session, "/child", 0, 1).unwrap();
client
.complete(
old_tree.id,
encode_fetch_tree_response(&tree_request, &tree_value, wire_limits()).unwrap(),
)
.unwrap();
assert!(matches!(
client.tree(&session, "/child").unwrap().state,
ObservationState::Loading(id) if id == newer_child_tree.id
));
let child_request = FetchTreeRequest {
path: "/child".into(),
depth: 0,
limit: 1,
};
let child_value = FetchTreeResponse {
root: ObjectTreeNode {
object: object("child", None, b"newer-tree"),
children: vec![],
},
truncated: false,
};
client
.complete(
newer_child_tree.id,
encode_fetch_tree_response(&child_request, &child_value, wire_limits()).unwrap(),
)
.unwrap();
assert_eq!(
client.tree(&session, "/child").unwrap().state,
ObservationState::Fresh
);
}
#[test]
fn superseded_transport_failure_does_not_poison_newer_refresh() {
let mut client = client(8, 8, 8, 8);
let session = client
.open_session("https://example.test/wip", SecurityContext::new("subject"))
.unwrap();
let old = client.prepare_fetch(&session, "/item").unwrap();
let newer = client.prepare_fetch(&session, "/item").unwrap();
assert_eq!(
client
.fail_transport(old.id, "old connection failed")
.unwrap(),
Completion::StaleResponseRejected(old.id)
);
assert!(matches!(
client.object(&session, "/item").unwrap().state,
ObservationState::Loading(id) if id == newer.id
));
client
.complete(
newer.id,
fetch_response("/item", &object("item", None, b"new")),
)
.unwrap();
let old = client.prepare_tree(&session, "/", 0, 1).unwrap();
let newer_tree = client.prepare_tree(&session, "/", 0, 1).unwrap();
assert_eq!(
client
.fail_transport(old.id, "old tree connection failed")
.unwrap(),
Completion::StaleResponseRejected(old.id)
);
assert!(matches!(
client.tree(&session, "/").unwrap().state,
ObservationState::Loading(id) if id == newer_tree.id
));
client
.complete(
newer_tree.id,
encode_fetch_tree_response(
&FetchTreeRequest {
path: "/".into(),
depth: 0,
limit: 1,
},
&FetchTreeResponse {
root: ObjectTreeNode {
object: object("", None, b"root"),
children: vec![],
},
truncated: false,
},
wire_limits(),
)
.unwrap(),
)
.unwrap();
let old = client.prepare_interface(&session, "math").unwrap();
let newer = client.prepare_interface(&session, "math").unwrap();
assert_eq!(
client
.fail_transport(old.id, "old connection failed")
.unwrap(),
Completion::StaleResponseRejected(old.id)
);
assert!(matches!(
client.interface(&session, "math").unwrap().state,
ObservationState::Loading(id) if id == newer.id
));
let value = FetchInterfaceResponse {
interface: "math".into(),
descriptor: descriptor(TypeExpr::Integer),
validator: Some(b"current".to_vec()),
};
client
.complete(
newer.id,
encode_fetch_interface_response(
&FetchInterfaceRequest {
interface: "math".into(),
},
&value,
wire_limits(),
)
.unwrap(),
)
.unwrap();
}
#[test]
fn validator_free_interface_observation_remains_immutable() {
let mut client = client(8, 8, 8, 8);
let session = client
.open_session("https://example.test/wip", SecurityContext::new("subject"))
.unwrap();
observe_interface(
&mut client,
&session,
FetchInterfaceResponse {
interface: "math".into(),
descriptor: descriptor(TypeExpr::Integer),
validator: None,
},
);
let refresh = client.prepare_interface(&session, "math").unwrap();
let contradictory = FetchInterfaceResponse {
interface: "math".into(),
descriptor: descriptor(TypeExpr::Integer),
validator: Some(b"became-mutable".to_vec()),
};
let error = client
.complete(
refresh.id,
encode_fetch_interface_response(
&FetchInterfaceRequest {
interface: "math".into(),
},
&contradictory,
wire_limits(),
)
.unwrap(),
)
.unwrap_err();
assert_eq!(error.kind, ClientErrorKind::InvalidResponse);
assert_eq!(client.interface(&session, "math").unwrap().validator, None);
}
#[test]
fn failed_entry_and_tree_observation_metadata_is_bounded() {
let mut client = client(2, 8, 2, 8);
let session = client
.open_session("https://example.test/wip", SecurityContext::new("subject"))
.unwrap();
for path in ["/one", "/two", "/three"] {
let request = client.prepare_fetch(&session, path).unwrap();
assert_eq!(
client
.fail_transport(request.id, "retrieval failed")
.unwrap_err()
.kind,
ClientErrorKind::Transport
);
}
assert!(client.object(&session, "/one").is_none());
assert!(client.object(&session, "/two").is_some());
assert!(client.object(&session, "/three").is_some());
for path in ["/alpha", "/beta", "/gamma"] {
let request = client.prepare_tree(&session, path, 0, 1).unwrap();
assert_eq!(
client
.fail_transport(request.id, "tree failed")
.unwrap_err()
.kind,
ClientErrorKind::Transport
);
}
assert!(client.tree(&session, "/alpha").is_none());
assert!(client.tree(&session, "/beta").is_some());
assert!(client.tree(&session, "/gamma").is_some());
}
#[test]
fn in_flight_and_history_are_bounded() {
let mut client = client(8, 8, 1, 1);