fix: separate playground values from object descriptions

This commit is contained in:
2026-10-26 10:12:22 +09:00
parent 75a36f2298
commit 41e88e3c40
4 changed files with 190 additions and 92 deletions
+96 -21
View File
@@ -28,6 +28,22 @@ fn collection(s: &mut BrowserSession, operation: &str, name: &str) -> Result<Jso
&json!({"name":{"kind":"string","value":name}}).to_string(),
)
}
fn assert_value(s: &mut BrowserSession, name: &str, value: i64) {
let calls = s.calls;
let observes = s.observes;
let interfaces = s.interfaces;
let result = s
.manual_call(&format!("/playground/{name}"), "/", "counter", "get", "{}")
.unwrap();
assert_eq!(
result["result"],
json!({"kind":"integer","value":value.to_string()})
);
assert_eq!(s.calls, calls + 1, "get dispatches exactly once");
assert_eq!(s.observes, observes, "get does not implicitly Observe");
assert_eq!(s.interfaces, interfaces, "get does not implicitly fetch");
}
fn names(observed: &Json) -> Vec<&str> {
observed["observation"]["children"]
.as_array()
@@ -38,14 +54,14 @@ fn names(observed: &Json) -> Vec<&str> {
}
#[test]
fn explicit_observe_refreshes_counter_and_parent_but_does_not_retry_or_fill_interfaces() {
fn explicit_observe_preserves_counter_metadata_and_does_not_call_retry_or_fill_interfaces() {
let mut s = session();
s.manual_observe("/book", 1.0).unwrap();
let initial = s.manual_observe("/playground", 1.0).unwrap();
assert_eq!(names(&initial), ["apple", "orange"]);
assert_eq!(
initial["observation"]["children"][0]["object"]["description"],
"3"
"An in-memory counter."
);
assert!(
increment(&mut s, 4).is_err(),
@@ -53,6 +69,7 @@ fn explicit_observe_refreshes_counter_and_parent_but_does_not_retry_or_fill_inte
);
assert_eq!(s.calls, 0);
s.manual_interface("/", "counter").unwrap();
assert_value(&mut s, "apple", 3);
let stats = s.manual_stats();
let result = increment(&mut s, 4).unwrap();
assert_eq!(result["result"], json!({"kind":"integer","value":"7"}));
@@ -60,7 +77,8 @@ fn explicit_observe_refreshes_counter_and_parent_but_does_not_retry_or_fill_inte
assert!(result.get("textView").is_none());
assert_eq!(s.observes, stats["observes"].as_u64().unwrap() as u32);
assert_eq!(s.interfaces, stats["interfaces"].as_u64().unwrap() as u32);
// A result did not silently fabricate a refreshed Client Object.
assert_value(&mut s, "apple", 7);
// Counter metadata is descriptive, not a cached counter value.
assert_eq!(
s.client
.object(&s.session, "/playground/apple")
@@ -69,9 +87,12 @@ fn explicit_observe_refreshes_counter_and_parent_but_does_not_retry_or_fill_inte
.unwrap()
.description
.as_deref(),
Some("3")
Some("An in-memory counter.")
);
let calls = s.calls;
let parent = s.manual_observe("/playground", 1.0).unwrap();
assert_eq!(s.calls, calls, "Observe must not implicitly call get");
assert_eq!(parent["observation"], initial["observation"]);
assert_eq!(parent["cached"], false);
assert_eq!(
parent["observation"]["children"][0]["object"]["name"],
@@ -79,18 +100,22 @@ fn explicit_observe_refreshes_counter_and_parent_but_does_not_retry_or_fill_inte
);
assert_eq!(
parent["observation"]["children"][0]["object"]["description"],
"7"
"An in-memory counter."
);
let selected = s.manual_observe("/playground/apple", 1.0).unwrap();
assert_eq!(selected["cached"], false);
assert_eq!(selected["observation"]["object"]["description"], "7");
assert_eq!(
selected["observation"]["object"],
initial["observation"]["children"][0]["object"]
);
assert_eq!(s.calls, calls, "Observe must not implicitly call get");
assert_eq!(
selected["textView"],
wip_text_view::render_object(&s.fresh_object("/playground/apple").unwrap()).unwrap()
);
assert_eq!(s.manual_observe("/book", 1.0).unwrap()["cached"], true);
assert_eq!(s.manual_interface("/", "counter").unwrap()["cached"], true);
assert_eq!(s.calls, 1);
assert_eq!(s.calls, 3, "two explicit gets and one increment");
}
#[test]
@@ -122,19 +147,36 @@ fn collection_refresh_removes_stale_children_and_recovers_from_retained_not_foun
collection(&mut s, "create", "apple").unwrap();
// Explicit fresh refresh recovers even from a retained Client NotFound.
let selected = s.manual_observe("/playground/apple", 1.0).unwrap();
assert_eq!(selected["observation"]["object"]["description"], "0");
assert_eq!(
selected["observation"]["object"]["description"],
"An in-memory counter."
);
assert_value(&mut s, "apple", 0);
assert_eq!(
names(&s.manual_observe("/playground", 1.0).unwrap()),
["apple", "orange", "pear"]
);
assert_eq!(increment(&mut s, 2).unwrap()["result"]["value"], "2");
s.manual_observe("/playground", 1.0).unwrap();
s.manual_call("/playground/apple", "/", "counter", "reset", "{}")
.unwrap();
assert_value(&mut s, "apple", 2);
let calls = s.calls;
let incremented = s.manual_observe("/playground/apple", 1.0).unwrap();
assert_eq!(incremented["observation"], selected["observation"]);
let parent = s.manual_observe("/playground", 1.0).unwrap();
assert_eq!(s.calls, calls, "Observe must not implicitly call get");
assert_eq!(
s.manual_observe("/playground", 1.0).unwrap()["observation"]["children"][0]["object"]["description"],
"0"
s.manual_call("/playground/apple", "/", "counter", "reset", "{}")
.unwrap()["result"],
json!({"kind":"integer","value":"0"})
);
assert_value(&mut s, "apple", 0);
assert_eq!(s.calls, calls + 2, "one reset and one explicit get");
let reset = s.manual_observe("/playground/apple", 1.0).unwrap();
assert_eq!(reset["observation"], selected["observation"]);
assert_eq!(
s.manual_observe("/playground", 1.0).unwrap()["observation"],
parent["observation"]
);
assert_eq!(s.calls, calls + 2, "Observe must not implicitly call get");
}
#[test]
@@ -143,9 +185,14 @@ fn failures_are_atomic_and_each_explicit_call_dispatches_at_most_once() {
s.manual_observe("/playground", 2.0).unwrap();
s.manual_interface("/", "counter").unwrap();
s.manual_interface("/", "collection").unwrap();
increment(&mut s, wip_protocol::MAX_SAFE_INTEGER - 3).unwrap();
let maximum = s.manual_observe("/playground/apple", 1.0).unwrap();
assert_eq!(
increment(&mut s, wip_protocol::MAX_SAFE_INTEGER - 3).unwrap()["result"],
json!({"kind":"integer","value":wip_protocol::MAX_SAFE_INTEGER.to_string()})
);
assert_value(&mut s, "apple", wip_protocol::MAX_SAFE_INTEGER);
let calls = s.calls;
let maximum = s.manual_observe("/playground/apple", 1.0).unwrap();
assert_eq!(s.calls, calls, "Observe must not implicitly call get");
let failure = increment(&mut s, 1).unwrap_err();
assert_eq!(failure.kind, "protocol");
assert_eq!(failure.code.as_deref(), Some("Internal"));
@@ -154,15 +201,31 @@ fn failures_are_atomic_and_each_explicit_call_dispatches_at_most_once() {
s.manual_observe("/playground/apple", 1.0).unwrap()["observation"],
maximum["observation"]
);
assert_eq!(s.calls, calls + 1, "Observe must not implicitly call get");
assert_value(&mut s, "apple", wip_protocol::MAX_SAFE_INTEGER);
assert_eq!(
s.calls,
calls + 2,
"one failed increment and one explicit get"
);
for name in ["apple", "bad/name", "<img>", ""] {
let before = s.manual_observe("/playground", 1.0).unwrap();
let calls = s.calls;
let before = s.manual_observe("/playground", 1.0).unwrap();
assert_eq!(s.calls, calls, "Observe must not implicitly call get");
assert!(collection(&mut s, "create", name).is_err());
assert_eq!(s.calls, calls + 1);
assert_eq!(
s.manual_observe("/playground", 1.0).unwrap()["observation"],
before["observation"]
);
assert_eq!(s.calls, calls + 1, "Observe must not implicitly call get");
assert_value(&mut s, "apple", wip_protocol::MAX_SAFE_INTEGER);
assert_value(&mut s, "orange", 0);
assert_eq!(
s.calls,
calls + 3,
"one failed create and two explicit gets"
);
}
let calls = s.calls;
assert!(increment(&mut s, i64::MAX).is_err());
@@ -170,6 +233,8 @@ fn failures_are_atomic_and_each_explicit_call_dispatches_at_most_once() {
s.calls, calls,
"out-of-range typed arguments rejected before Host dispatch"
);
assert_value(&mut s, "apple", wip_protocol::MAX_SAFE_INTEGER);
assert_eq!(s.calls, calls + 1, "only the explicit get dispatches");
}
#[test]
@@ -189,29 +254,39 @@ fn normal_tree_and_document_reads_ignore_dynamic_children_and_reload_resets_stat
let book = s.read("/book").unwrap();
s.manual_interface("/", "counter").unwrap();
s.manual_interface("/", "collection").unwrap();
increment(&mut s, 10).unwrap();
assert_eq!(increment(&mut s, 10).unwrap()["result"]["value"], "13");
assert_value(&mut s, "apple", 13);
collection(&mut s, "create", "pear").unwrap();
collection(&mut s, "remove", "orange").unwrap();
s.manual_observe("/playground", 1.0).unwrap();
assert_eq!(s.tree().unwrap(), tree);
assert_eq!(s.read("/playground").unwrap(), document);
assert_eq!(s.read("/book").unwrap(), book);
let calls = s.calls;
assert_eq!(
s.manual_observe("/playground/apple", 1.0).unwrap()["observation"]["object"]["description"],
"13",
"navigation does not reset state"
"An in-memory counter."
);
assert_eq!(s.calls, calls, "Observe must not implicitly call get");
// Navigation does not reset state; verify through an explicit operation.
assert_value(&mut s, "apple", 13);
let mut reloaded = session();
let initial = reloaded.manual_observe("/playground", 1.0).unwrap();
assert_eq!(names(&initial), ["apple", "orange"]);
assert_eq!(
initial["observation"]["children"][0]["object"]["description"],
"3"
"An in-memory counter."
);
assert_eq!(
initial["observation"]["children"][1]["object"]["description"],
"0"
"An in-memory counter."
);
assert_eq!(reloaded.calls, 0, "Observe must not implicitly call get");
reloaded.manual_interface("/", "counter").unwrap();
assert_value(&mut reloaded, "apple", 3);
assert_value(&mut reloaded, "orange", 0);
assert_eq!(reloaded.calls, 2, "two explicit gets after reload");
assert_value(&mut s, "apple", 13);
}
#[test]