mirror of
https://github.com/block/buzz.git
synced 2026-11-07 01:41:06 +00:00
test(desktop): add §7 sink, latch, and early-gate coordinator fixtures (P25/P26/P29-C1)
Three harness-hermetic §7 fixtures for the C5 identity-transition chunk: - commit_imported_identity closed-world sink scan (P25-C1) — the durable in-memory swap has exactly one call site; a new caller in an existing or new file trips the inventory scan, with two mutation proofs. - signing_keys refusal under the Indeterminate latch (P29-C1) — the checked accessor's integration point, with a Live control read proving transparency. - early-gate zero-disruptive-work schedule (P26-C1) — drives the real coordinator through a superseded early_validity_check and asserts the egress state, persistence generation, and active scope are all untouched; the late gate asserts unreachable. The no-scope-commit and None->Some activation-race §7 schedules are not included here: both require the coordinator to reach a durable commit, which persists through the real process-global keyring and the real app_data_dir under mock_builder() (empty identifier). Routing that hermetically is a separate question raised to Paul. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
@@ -424,3 +424,238 @@ fn import_identity_blocking<R: tauri::Runtime>(
|
||||
reset_failed: false,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
//! Closed-world identity-swap sink test (P25-C1, spec §7): the durable
|
||||
//! in-memory identity swap has exactly ONE reachable site —
|
||||
//! [`commit_imported_identity`], called ONLY from this coordinator's
|
||||
//! `Committed` arm. A third path calling it directly would re-open the
|
||||
//! P25/P26 split-state (swap identity B without the drain / scope clear /
|
||||
//! generation bump the coordinator performs), so the number of CALL sites
|
||||
//! is fixed by inventory. Adding a caller — in a new file or an existing
|
||||
//! one — trips this scan until its row is updated, which is the deliberate
|
||||
//! act that must accompany routing a new swap path through the coordinator.
|
||||
|
||||
/// Per-file inventory of expected `commit_imported_identity` CALL sites
|
||||
/// (`(file suffix, expected calls)`). The `fn commit_imported_identity`
|
||||
/// DEFINITION and doc/comment mentions are excluded by [`call_sites`]; only
|
||||
/// call expressions count. The sole legitimate caller is this module.
|
||||
const SINK_INVENTORY: &[(&str, usize)] = &[("src/commands/identity_transition.rs", 1)];
|
||||
|
||||
fn needle() -> String {
|
||||
// Assembled at runtime so this scan file's own inventory row (1) is the
|
||||
// real call in `import_identity_blocking`, not a literal in the table.
|
||||
["commit_imported", "_identity"].concat()
|
||||
}
|
||||
|
||||
fn src_rust_files() -> Vec<std::path::PathBuf> {
|
||||
fn walk(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
|
||||
for entry in std::fs::read_dir(dir).unwrap() {
|
||||
let path = entry.unwrap().path();
|
||||
if path.is_dir() {
|
||||
walk(&path, out);
|
||||
} else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
let mut out = Vec::new();
|
||||
walk(&root, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
fn read_src_files() -> Vec<(String, String)> {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
src_rust_files()
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
let rel = path
|
||||
.strip_prefix(root)
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/");
|
||||
(rel, std::fs::read_to_string(&path).unwrap())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Count call sites of the sink in one file's content: lines that invoke
|
||||
/// `commit_imported_identity(` but are neither the `fn` definition nor a
|
||||
/// `//` comment. `pub(crate) use ...` re-exports are definitions of a name,
|
||||
/// not calls, and carry no `(`, so they never match.
|
||||
fn call_sites(content: &str, needle: &str) -> usize {
|
||||
let call = format!("{needle}(");
|
||||
content
|
||||
.lines()
|
||||
.filter(|line| {
|
||||
let trimmed = line.trim_start();
|
||||
!trimmed.starts_with("//")
|
||||
&& !trimmed.contains(&format!("fn {call}"))
|
||||
&& trimmed.contains(&call)
|
||||
})
|
||||
.count()
|
||||
}
|
||||
|
||||
fn sink_violations(files: &[(String, String)]) -> Vec<String> {
|
||||
let needle = needle();
|
||||
let mut violations = Vec::new();
|
||||
for (rel, content) in files {
|
||||
let expected = SINK_INVENTORY
|
||||
.iter()
|
||||
.find(|(suffix, _)| rel.ends_with(suffix))
|
||||
.map(|&(_, e)| e)
|
||||
.unwrap_or(0);
|
||||
let found = call_sites(content, &needle);
|
||||
if found != expected {
|
||||
violations.push(format!(
|
||||
"{rel}: found {found} commit_imported_identity call site(s), \
|
||||
inventory expects {expected}"
|
||||
));
|
||||
}
|
||||
}
|
||||
violations
|
||||
}
|
||||
|
||||
/// Closed world: every `commit_imported_identity` call site in
|
||||
/// `desktop/src-tauri/src` matches the inventory — exactly one, in this
|
||||
/// coordinator. A new caller anywhere fails this until its row is updated.
|
||||
#[test]
|
||||
fn commit_imported_identity_sink_is_closed_world() {
|
||||
let violations = sink_violations(&read_src_files());
|
||||
assert!(
|
||||
violations.is_empty(),
|
||||
"identity-swap sink drift — a new commit_imported_identity caller must \
|
||||
route through run_identity_transition, not call the swap directly; then \
|
||||
update SINK_INVENTORY:\n{}",
|
||||
violations.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
/// Mutation proof: a second caller added to an already-inventoried file
|
||||
/// (this one) is caught — the scan is not vacuously passing.
|
||||
#[test]
|
||||
fn sink_scan_catches_a_new_caller_in_an_inventoried_file() {
|
||||
let mut files = read_src_files();
|
||||
let me = files
|
||||
.iter_mut()
|
||||
.find(|(rel, _)| rel.ends_with("src/commands/identity_transition.rs"))
|
||||
.expect("this module must be in the scan set");
|
||||
me.1.push_str(&format!(
|
||||
"\n let _ = {}(&state, dir, keys, storage);\n",
|
||||
needle()
|
||||
));
|
||||
let violations = sink_violations(&files);
|
||||
assert!(
|
||||
violations
|
||||
.iter()
|
||||
.any(|v| v.contains("src/commands/identity_transition.rs")),
|
||||
"a second sink caller must trip the scan: {violations:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Mutation proof: a caller in a brand-new file (no inventory row) is
|
||||
/// caught — the closed world admits no unlisted swap path.
|
||||
#[test]
|
||||
fn sink_scan_catches_a_caller_in_a_new_file() {
|
||||
let mut files = read_src_files();
|
||||
files.push((
|
||||
"src/sneaky_swap.rs".to_string(),
|
||||
format!("fn bypass() {{ {}(&s, d, k, st); }}", needle()),
|
||||
));
|
||||
let violations = sink_violations(&files);
|
||||
assert!(
|
||||
violations.iter().any(|v| v.contains("src/sneaky_swap.rs")),
|
||||
"a sink caller in an unlisted file must trip the scan: {violations:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// EARLY-gate zero-disruptive-work schedule (P26-C1, spec §7): a recovery
|
||||
/// superseded while it queued on the transition locks is rejected by
|
||||
/// `early_validity_check` — which runs under the held `workspace_transition`
|
||||
/// guard, immediately after acquisition and BEFORE the Mesh preflight and
|
||||
/// the `spawn_blocking` body — having done ZERO disruptive work. Because the
|
||||
/// gate short-circuits with `?` before any disruptive call, "zero work" is
|
||||
/// structural; this fixture pins it by driving the real coordinator and
|
||||
/// asserting the durable side-effect surfaces are untouched: egress never
|
||||
/// left `Live` (no `begin_egress_drain`), the persistence generation did not
|
||||
/// move, and no active scope was committed. The `nsec` is deliberately
|
||||
/// invalid — reaching key recovery at all would prove the gate leaked past
|
||||
/// its boundary.
|
||||
#[tokio::test]
|
||||
// The two process-global test-serialization guards are held across the
|
||||
// coordinator `.await` deliberately: they must cover the whole transition
|
||||
// so a sibling egress/scope test cannot observe or mutate the shared
|
||||
// registry mid-drive. No other task contends for them inside this test's
|
||||
// runtime, so holding them across the await cannot stall or deadlock it.
|
||||
#[allow(clippy::await_holding_lock)]
|
||||
async fn early_gate_rejection_does_zero_disruptive_work() {
|
||||
use crate::managed_agents::scope::SCOPE_GENERATION_TEST_LOCK;
|
||||
use crate::owner_identity_egress::{
|
||||
current_identity_persistence_generation, identity_persistence_state,
|
||||
reset_registry_for_test, IdentityPersistenceState, EGRESS_REGISTRY_TEST_LOCK,
|
||||
};
|
||||
use tauri::Manager;
|
||||
|
||||
// Serialize against every other egress/scope-sensitive test.
|
||||
let _egress_guard = EGRESS_REGISTRY_TEST_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let _scope_guard = SCOPE_GENERATION_TEST_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
reset_registry_for_test();
|
||||
|
||||
let app = tauri::test::mock_builder()
|
||||
.manage(crate::app_state::build_app_state())
|
||||
.build(tauri::test::mock_context(tauri::test::noop_assets()))
|
||||
.expect("failed to build mock app");
|
||||
let app_handle = app.handle().clone();
|
||||
let state = app.state::<crate::app_state::AppState>();
|
||||
|
||||
let generation_before = current_identity_persistence_generation();
|
||||
assert_eq!(identity_persistence_state(), IdentityPersistenceState::Live);
|
||||
assert!(
|
||||
state.capture_active_scope().is_none(),
|
||||
"pre-condition: no active scope"
|
||||
);
|
||||
|
||||
// Drive the coordinator with an early gate that is already superseded.
|
||||
// The late gate would never be reached, so it asserts unreachable.
|
||||
let result = super::run_identity_transition(
|
||||
app_handle,
|
||||
"nsec-invalid-must-never-be-parsed".to_string(),
|
||||
None,
|
||||
None,
|
||||
|| Err("superseded while queued".to_string()),
|
||||
|| unreachable!("late gate must not run after an early-gate rejection"),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
result.err().expect("early-gate rejection must return Err"),
|
||||
"superseded while queued",
|
||||
"the early-gate error must propagate verbatim"
|
||||
);
|
||||
|
||||
// Zero disruptive work: egress untouched (never drained), no generation
|
||||
// movement, no scope committed.
|
||||
assert_eq!(
|
||||
identity_persistence_state(),
|
||||
IdentityPersistenceState::Live,
|
||||
"early-gate rejection must not begin the egress drain"
|
||||
);
|
||||
assert_eq!(
|
||||
current_identity_persistence_generation(),
|
||||
generation_before,
|
||||
"early-gate rejection must not bump the persistence generation"
|
||||
);
|
||||
assert!(
|
||||
state.capture_active_scope().is_none(),
|
||||
"early-gate rejection must not commit a scope"
|
||||
);
|
||||
|
||||
reset_registry_for_test();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -884,4 +884,35 @@ mod tests {
|
||||
);
|
||||
crate::relay_admission::reset_rate_limit_gate();
|
||||
}
|
||||
|
||||
/// Accessor-level proof of the P29-C1 latch integration: once the
|
||||
/// coordinator latches `Indeterminate`, [`AppState::signing_keys`] refuses —
|
||||
/// a completed transition that could not prove either durable identity
|
||||
/// canonical must not sign under the unresolved identity. The `guard()`
|
||||
/// resets the registry on entry (so the control read below is Live) and on
|
||||
/// drop (so the latch never leaks into other crate tests).
|
||||
#[test]
|
||||
fn signing_keys_refuses_under_the_indeterminate_latch() {
|
||||
let _g = guard();
|
||||
let state = crate::app_state::build_app_state();
|
||||
|
||||
// Control: with the registry Live, the gate is transparent.
|
||||
assert!(
|
||||
state.signing_keys().is_ok(),
|
||||
"signing_keys must sign while the identity is Live"
|
||||
);
|
||||
|
||||
// Latch indeterminate (drain first — the latch is only reachable from a
|
||||
// draining transition, matching the coordinator's fail-closed exit).
|
||||
begin_egress_drain().unwrap();
|
||||
latch_identity_indeterminate();
|
||||
|
||||
let err = state
|
||||
.signing_keys()
|
||||
.expect_err("signing_keys must refuse under the indeterminate latch");
|
||||
assert!(
|
||||
err.contains("indeterminate"),
|
||||
"the refusal must name the indeterminate recovery state: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user