fix: keep object observations fresh after successful calls

This commit is contained in:
2026-09-30 13:44:54 +09:00
parent de82b66de4
commit 2e4196a1ec
4 changed files with 153 additions and 26 deletions
+7 -4
View File
@@ -1261,11 +1261,14 @@ impl Client {
let session_id = context.session.clone();
let maximum = self.limits.history;
let state = self.session_mut(&session_id)?;
if let Some(object) = state.objects.get_mut(&context.object_key) {
object.object.validator = value.validator.clone();
object.revision = object.revision.max(id);
if let Some(object) = state.objects.get_mut(&context.object_key)
&& object.revision <= id
{
if let Some(validator) = value.validator.clone() {
object.object.validator = Some(validator);
}
object.revision = id;
}
state.mark_object_stale(context.object_key, id);
let record = CallRecord {
request_id: id,
context,
+139 -7
View File
@@ -525,14 +525,147 @@ fn call_context_fixes_membership_validators_descriptor_and_result_validation() {
client.interface(&session, "math").unwrap().descriptor,
Some(descriptor(TypeExpr::String))
);
let observation = client.object(&session, "/item").unwrap();
assert_eq!(observation.state, ObservationState::Fresh);
assert_eq!(observation.validator, Some(b"object-v2".to_vec()));
}
#[test]
fn successful_call_without_validator_preserves_observed_validator() {
let mut client = client(8, 8, 8, 8);
let session = client
.open_session("https://example.test/wip", SecurityContext::new("subject"))
.unwrap();
observe_object(
&mut client,
&session,
"/item",
object("item", None, b"object-v1"),
);
observe_interface(
&mut client,
&session,
FetchInterfaceResponse {
interface: "math".into(),
descriptor: descriptor(TypeExpr::Integer),
validator: None,
},
);
let call = client
.prepare_call(&session, "/item", "math", "calculate", args(1))
.unwrap();
let request = client
.pending_call_context(call.id)
.unwrap()
.request()
.clone();
let fixed_descriptor = client
.pending_call_context(call.id)
.unwrap()
.descriptor()
.clone();
client.mark_dispatched(call.id).unwrap();
client
.complete(
call.id,
encode_call_operation_response(
&request,
&fixed_descriptor,
&CallOperationResponse {
result: Value::Integer(2),
validator: None,
},
wire_limits(),
)
.unwrap(),
)
.unwrap();
let observation = client.object(&session, "/item").unwrap();
assert_eq!(observation.state, ObservationState::Fresh);
assert_eq!(observation.validator, Some(b"object-v1".to_vec()));
let next = client
.prepare_call(&session, "/item", "math", "calculate", args(2))
.unwrap();
assert_eq!(
client.object(&session, "/item").unwrap().state,
ObservationState::Stale
client
.pending_call_context(next.id)
.unwrap()
.request()
.target
.validator,
Some(b"object-v1".to_vec())
);
}
#[test]
fn older_retrieval_cannot_restore_freshness_after_operation_completion() {
fn newer_observation_prevents_older_success_validator_overwrite() {
let mut client = client(8, 8, 8, 8);
let session = client
.open_session("https://example.test/wip", SecurityContext::new("subject"))
.unwrap();
observe_object(
&mut client,
&session,
"/item",
object("item", None, b"object-v1"),
);
observe_interface(
&mut client,
&session,
FetchInterfaceResponse {
interface: "math".into(),
descriptor: descriptor(TypeExpr::Integer),
validator: None,
},
);
let call = client
.prepare_call(&session, "/item", "math", "calculate", args(1))
.unwrap();
let request = client
.pending_call_context(call.id)
.unwrap()
.request()
.clone();
let fixed_descriptor = client
.pending_call_context(call.id)
.unwrap()
.descriptor()
.clone();
client.mark_dispatched(call.id).unwrap();
observe_object(
&mut client,
&session,
"/item",
object("item", None, b"object-v3"),
);
client
.complete(
call.id,
encode_call_operation_response(
&request,
&fixed_descriptor,
&CallOperationResponse {
result: Value::Integer(2),
validator: Some(b"object-v2".to_vec()),
},
wire_limits(),
)
.unwrap(),
)
.unwrap();
let observation = client.object(&session, "/item").unwrap();
assert_eq!(observation.state, ObservationState::Fresh);
assert_eq!(observation.validator, Some(b"object-v3".to_vec()));
}
#[test]
fn older_retrieval_cannot_overwrite_success_validator() {
let mut client = client(8, 8, 8, 8);
let session = client
.open_session("https://example.test/wip", SecurityContext::new("subject"))
@@ -583,10 +716,9 @@ fn older_retrieval_cannot_restore_freshness_after_operation_completion() {
.unwrap(),
)
.unwrap();
assert_eq!(
client.object(&session, "/item").unwrap().state,
ObservationState::Stale
);
let current = client.object(&session, "/item").unwrap();
assert_eq!(current.state, ObservationState::Fresh);
assert_eq!(current.validator, Some(b"object-v2".to_vec()));
client
.complete(
+2 -2
View File
@@ -121,8 +121,8 @@ layout.
## Validator and failure behavior
Object and interface validators are observed and attached automatically by
`wip-client`; users neither copy nor edit them. A successful operation conservatively
marks related object observations stale because metadata may have changed.
`wip-client`; users neither copy nor edit them. A successful operation may update the
cached Object validator but does not by itself make the Object observation stale.
Validator mismatch, interface mismatch, `OperationOutcomeUnknown`, undecodable
post-dispatch responses, and post-dispatch timeout/disconnect are displayed using
the runtime's authoritative transition.
+5 -13
View File
@@ -91,11 +91,7 @@ fn real_http_filesystem_workflow_is_lazy_validator_safe_and_symlink_confined() {
.contains("68656c6c6f20776f726c64")
);
// Calls conservatively stale the object even for a read operation; refresh
// before constructing the separately confirmed write call.
runtime.app_mut().focus = Focus::Tree;
runtime.handle_key(InputKey::Refresh);
settle(&mut runtime);
assert_eq!(runtime.app().entries["/hello.txt"].state, ViewState::Fresh);
runtime.app_mut().focus = Focus::Operations;
// Load the separately-qualified write interface and replace with the
@@ -112,15 +108,11 @@ fn real_http_filesystem_workflow_is_lazy_validator_safe_and_symlink_confined() {
std::fs::read(temporary.path().join("hello.txt")).expect("updated file"),
b"updated"
);
assert_eq!(runtime.app().entries["/hello.txt"].state, ViewState::Stale);
// Reobserve, prepare a replacement from that observation, then mutate the
// file externally before explicit execution. The stale validator is rejected,
// the Entry becomes stale, and no automatic call is created.
runtime.app_mut().focus = Focus::Tree;
runtime.handle_key(InputKey::Refresh);
settle(&mut runtime);
assert_eq!(runtime.app().entries["/hello.txt"].state, ViewState::Fresh);
// Prepare another replacement using the post-operation validator, then mutate
// the file externally before explicit execution. The stale validator is rejected,
// the Entry becomes stale, and no automatic call is created.
runtime.app_mut().focus = Focus::Operations;
runtime.app_mut().operation_cursor = 3;
runtime.handle_key(InputKey::Enter);