fix: retain observed scope bindings through client refresh states
Address MR review sequence 5: retain last accepted Object-ref evidence during loading, ordinary staleness and retrieval failure. Mark confirmed path unavailability separately within bounded Entry records so old lifetime evidence cannot contaminate republished ref-less descriptors. Compare retained Object refs on later replacement even when newer Interface ownership previously suppressed an older scope observation. Add accepted-descriptor and pending-first-fetch old-to-new/ref-loss matrices during Loading/Error, edge-stale evidence, and confirmed-deletion/republication/reset tracking regressions. Validation: 50 runtime + 13 adapter tests; narrow Protocol/HTTP; workspace check/tests/all-target tests; Nix Clippy -D warnings; warning-free rustdoc; fmt/diff checks.
This commit is contained in:
@@ -3170,3 +3170,222 @@ fn corroborated_same_scope_ref_preserves_immutability_when_wire_metadata_becomes
|
||||
let unchanged = scoped_descriptor("/scope", Some("same"), None, TypeExpr::Integer);
|
||||
observe_interface(&mut client, &session, unchanged);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retained_object_binding_survives_loading_and_failed_refresh_for_first_ref_less_fetch() {
|
||||
for new_ref in [None, Some("new")] {
|
||||
for accepted_descriptor in [false, true] {
|
||||
for failed_refresh in [false, true] {
|
||||
let (mut client, session) = scope_client();
|
||||
observe_object(
|
||||
&mut client,
|
||||
&session,
|
||||
"/scope",
|
||||
object("scope", Some("old"), b"v1"),
|
||||
);
|
||||
let loading_scope = client.refresh_object(&session, "/scope").unwrap();
|
||||
if failed_refresh {
|
||||
client
|
||||
.fail_transport(loading_scope.id, "temporary failure")
|
||||
.unwrap_err();
|
||||
}
|
||||
let old = scoped_descriptor("/scope", None, None, TypeExpr::Integer);
|
||||
let pending = client
|
||||
.prepare_interface(&session, old.interface.clone())
|
||||
.unwrap();
|
||||
if accepted_descriptor {
|
||||
client
|
||||
.complete(pending.id, interface_response(&old))
|
||||
.unwrap();
|
||||
}
|
||||
observe_object(
|
||||
&mut client,
|
||||
&session,
|
||||
"/scope",
|
||||
object("scope", new_ref, b"v2"),
|
||||
);
|
||||
let cached = client.interface(&session, &old.interface).unwrap();
|
||||
assert_eq!(cached.state, ObservationState::Stale);
|
||||
assert!(cached.descriptor.is_none());
|
||||
assert_eq!(cached.scope_ref, None);
|
||||
if !accepted_descriptor {
|
||||
assert_eq!(
|
||||
client
|
||||
.complete(pending.id, interface_response(&old))
|
||||
.unwrap(),
|
||||
Completion::StaleResponseRejected(pending.id)
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
client
|
||||
.prepare_call(
|
||||
&session,
|
||||
"/scope/item",
|
||||
&old.interface,
|
||||
"calculate",
|
||||
args(1)
|
||||
)
|
||||
.unwrap_err()
|
||||
.kind,
|
||||
ClientErrorKind::MissingObservation
|
||||
);
|
||||
let current = scoped_descriptor("/scope", None, None, TypeExpr::String);
|
||||
observe_interface(&mut client, &session, current.clone());
|
||||
let call = client
|
||||
.prepare_call(
|
||||
&session,
|
||||
"/scope/item",
|
||||
&old.interface,
|
||||
"calculate",
|
||||
args(2),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
client
|
||||
.pending_call_context(call.id)
|
||||
.unwrap()
|
||||
.request()
|
||||
.interface
|
||||
.scope_ref,
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
client.pending_call_context(call.id).unwrap().descriptor(),
|
||||
¤t.descriptor
|
||||
);
|
||||
if !failed_refresh {
|
||||
assert_eq!(
|
||||
client
|
||||
.complete(
|
||||
loading_scope.id,
|
||||
observation_response(
|
||||
"/scope",
|
||||
&object("scope", Some("old"), b"v1")
|
||||
)
|
||||
)
|
||||
.unwrap(),
|
||||
Completion::StaleResponseRejected(loading_scope.id)
|
||||
);
|
||||
assert_eq!(
|
||||
client.interface(&session, &old.interface).unwrap().state,
|
||||
ObservationState::Fresh
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_ref_less_descriptor_keeps_lifetime_evidence_when_scope_is_edge_stale() {
|
||||
let (mut client, session) = scope_client();
|
||||
let request = ObserveRequest {
|
||||
path: "/".into(),
|
||||
depth: 1,
|
||||
};
|
||||
for children in [
|
||||
vec![TestObservation {
|
||||
object: object("scope", Some("old"), b"scope"),
|
||||
children: vec![],
|
||||
}],
|
||||
vec![],
|
||||
] {
|
||||
let pending = client.refresh_children(&session, "/").unwrap();
|
||||
client
|
||||
.complete(
|
||||
pending.id,
|
||||
encode_test_observation_response(
|
||||
&request,
|
||||
&TestObservationResponse {
|
||||
root: TestObservation {
|
||||
object: object("", None, b"root"),
|
||||
children,
|
||||
},
|
||||
},
|
||||
wire_limits(),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(
|
||||
client.object(&session, "/scope").unwrap().state,
|
||||
ObservationState::Stale
|
||||
);
|
||||
let old = scoped_descriptor("/scope", None, None, TypeExpr::Integer);
|
||||
observe_interface(&mut client, &session, old.clone());
|
||||
observe_object(
|
||||
&mut client,
|
||||
&session,
|
||||
"/scope",
|
||||
object("scope", Some("new"), b"new"),
|
||||
);
|
||||
assert_eq!(
|
||||
client.interface(&session, &old.interface).unwrap().state,
|
||||
ObservationState::Stale
|
||||
);
|
||||
assert!(
|
||||
client
|
||||
.interface(&session, &old.interface)
|
||||
.unwrap()
|
||||
.descriptor
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmed_missing_scope_does_not_corroborate_republished_descriptor_with_expired_ref() {
|
||||
let (mut client, session) = scope_client();
|
||||
observe_object(
|
||||
&mut client,
|
||||
&session,
|
||||
"/scope",
|
||||
object("scope", Some("old"), b"v1"),
|
||||
);
|
||||
let deletion = client.refresh_object(&session, "/scope").unwrap();
|
||||
client
|
||||
.complete(
|
||||
deletion.id,
|
||||
failure_response(ProtocolInteraction::Observe, ProtocolErrorCode::NotFound),
|
||||
)
|
||||
.unwrap();
|
||||
let current = scoped_descriptor("/scope", None, None, TypeExpr::String);
|
||||
observe_interface(&mut client, &session, current.clone());
|
||||
// Loading state alone must not make the confirmed ended Object binding live.
|
||||
let loading = client.refresh_object(&session, "/scope").unwrap();
|
||||
observe_interface(&mut client, &session, current.clone());
|
||||
client
|
||||
.complete(
|
||||
loading.id,
|
||||
observation_response("/scope", &object("scope", Some("new"), b"v2")),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
client
|
||||
.interface(&session, ¤t.interface)
|
||||
.unwrap()
|
||||
.state,
|
||||
ObservationState::Fresh
|
||||
);
|
||||
assert_eq!(
|
||||
client
|
||||
.interface(&session, ¤t.interface)
|
||||
.unwrap()
|
||||
.scope_ref,
|
||||
None
|
||||
);
|
||||
// A successful new observation restores tracking for subsequent replacements.
|
||||
observe_object(
|
||||
&mut client,
|
||||
&session,
|
||||
"/scope",
|
||||
object("scope", Some("another"), b"v3"),
|
||||
);
|
||||
assert_eq!(
|
||||
client
|
||||
.interface(&session, ¤t.interface)
|
||||
.unwrap()
|
||||
.state,
|
||||
ObservationState::Stale
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user