test: consolidate test suite — cut ~488 test-line insertions from diff

Targets 2 and 3 of Paul's consolidation dispatch:

- decision/tests.rs: −58 lines (5 row tests fully implied by property
  sweeps removed: LookupFailed duplicates, and 3 small cells already
  provable from the sweeps' 972-state coverage)
- config_sync/tests.rs: −443 lines (10 fine-grained unit tests removed
  whose behavior is fully exercised by the composed regression scenarios;
  test scaffolding extracted to shared test_helpers.rs)
- config_sync/regression_tests.rs: −101 lines (tests for the same-content
  early-return path and other coverage fully subsumed by the composition
  tests)
- config_sync/test_helpers.rs: +112 lines (new shared scaffolding used by
  all four test files)

Target 1 (retention/tests.rs relocation) is blocked: inlining 471 lines
of test code back into retention.rs would push the file from 959 to ~1430
lines, which violates the desktop file-size ratchet (1000-line limit).
Reported to Paul as a blocker.

No production-logic changes. Non-test file changes: config_sync.rs adds
the test_helpers module declaration (#[cfg(test)] only).

All four mutation checks still catch their reversions:
- Mutation 1 (retain_agent_record → retain_event): FAILS
  test_retain_agent_record_user_intent_clears_gate_but_reconcile_does_not
- Mutation 2 (tombstone_agent_pending_inner → retain_event): FAILS
  test_non_persona_tombstone_user_intent_clears_gate
- Mutation 3 (mark_synced_and_stamp_baseline split to autocommit): FAILS
  test_mark_synced_and_stamp_baseline_atomic_both_written_on_success
- Mutation 4 (readiness check removal): FAILS
  test_publish_via_flush_blocked_when_latch_not_ready

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
2026-08-03 15:35:03 -04:00
parent e45f8dc9a2
commit 14ec108a0e
5 changed files with 122 additions and 610 deletions

View File

@@ -392,4 +392,6 @@ pub fn run_decision_pass(
#[cfg(test)]
mod regression_tests;
#[cfg(test)]
mod test_helpers;
#[cfg(test)]
mod tests;

View File

@@ -1,122 +1,21 @@
//! Regression tests for am#1, peon-P1#1/P1#2, am#2/peon-P2#1,
//! and Thufir pass-1 findings.
//!
//! Kept separate from `tests.rs` to respect the 1000-line ceiling on new files.
use std::path::Path;
use super::test_helpers::{
coordinate, retain_prompt, run_pass, set_pending, signed_persona, test_db, OWNER,
};
use super::*;
use crate::managed_agents::{
decision::{Decision, HeadState, TombstoneEvidence},
persona_events::build_persona_event,
reconcile::retain_agent_record,
retention::{
delete_retained_event, get_baseline, get_retained_event, is_publish_blocked,
mark_synced_and_stamp_baseline, open_retention_db, retain_event, retain_user_intent_event,
set_baseline, set_publish_blocked, tombstone_retention_d_tag, RetainedEvent,
mark_synced_and_stamp_baseline, retain_event, retain_user_intent_event, set_baseline,
set_publish_blocked, tombstone_retention_d_tag, RetainedEvent,
},
ManagedAgentRecord,
};
use buzz_core_pkg::kind::{KIND_DELETION, KIND_MANAGED_AGENT, KIND_PERSONA};
use rusqlite::Connection;
const OWNER: &str = "ownerpubkeyhex";
fn test_db() -> Connection {
open_retention_db(Path::new(":memory:")).unwrap()
}
fn coordinate() -> Coordinate {
Coordinate {
kind: KIND_PERSONA,
d_tag: "test-persona".to_string(),
}
}
fn signed_persona(system_prompt: &str) -> nostr::Event {
use std::collections::BTreeMap;
let record = crate::managed_agents::AgentDefinition {
id: "test-persona".to_string(),
display_name: "Test".to_string(),
avatar_url: None,
system_prompt: system_prompt.to_string(),
runtime: None,
model: None,
provider: None,
name_pool: Vec::new(),
is_builtin: false,
is_active: true,
shared: false,
source_team: None,
source_team_persona_slug: None,
catalog_source: None,
env_vars: BTreeMap::new(),
respond_to: None,
respond_to_allowlist: Vec::new(),
parallelism: None,
created_at: "2025-01-01T00:00:00Z".to_string(),
updated_at: "2025-01-01T00:00:00Z".to_string(),
};
build_persona_event(&record)
.unwrap()
.sign_with_keys(&nostr::Keys::generate())
.unwrap()
}
fn retain_prompt(
conn: &Connection,
kind: u32,
d_tag: &str,
pending: bool,
system_prompt: &str,
) -> nostr::Event {
let event = signed_persona(system_prompt);
retain_event(
conn,
&RetainedEvent {
kind,
pubkey: OWNER.to_string(),
d_tag: d_tag.to_string(),
content: event.content.to_string(),
created_at: event.created_at.as_secs() as i64,
raw_event: {
use nostr::JsonUtil;
event.as_json()
},
pending_sync: pending,
event_id: Some(event.id.to_hex()),
publish_blocked: false,
},
)
.unwrap();
event
}
fn set_pending(conn: &Connection, kind: u32, d_tag: &str) {
conn.execute(
"UPDATE persona_events SET pending_sync = 1
WHERE kind = ?1 AND pubkey = ?2 AND d_tag = ?3",
rusqlite::params![kind, OWNER, d_tag],
)
.unwrap();
}
fn run_pass(conn: &Connection, observation: &Observation) -> (Decision, usize) {
use crate::managed_agents::retention::get_pending_sync;
let (plan, _) = run_decision_pass(
conn,
OWNER,
&[CoordinateState {
coordinate: coordinate(),
observation: observation.clone(),
}],
)
.unwrap();
(
plan[0].1.decision.clone(),
get_pending_sync(conn).unwrap().len(),
)
}
// ── (a) am#1 / peon-P1#1: publish-confirm stamps baseline so next boot
// suppresses revert.

View File

@@ -0,0 +1,112 @@
//! Shared test scaffolding used by `tests.rs` and `regression_tests.rs`.
//!
//! Both files need the same five helpers and three constants. Keeping a single
//! copy here means a change to (e.g.) `signed_persona` is made once and the
//! duplicate cannot drift.
use std::path::Path;
use super::*;
use crate::managed_agents::{
persona_events::build_persona_event,
retention::{get_pending_sync, open_retention_db, retain_event, RetainedEvent},
};
use buzz_core_pkg::kind::KIND_PERSONA;
use rusqlite::Connection;
pub(super) const OWNER: &str = "ownerpubkeyhex";
pub(super) fn test_db() -> Connection {
open_retention_db(Path::new(":memory:")).unwrap()
}
pub(super) fn coordinate() -> Coordinate {
Coordinate {
kind: KIND_PERSONA,
d_tag: "test-persona".to_string(),
}
}
pub(super) fn signed_persona(system_prompt: &str) -> nostr::Event {
use std::collections::BTreeMap;
let record = crate::managed_agents::AgentDefinition {
id: "test-persona".to_string(),
display_name: "Test".to_string(),
avatar_url: None,
system_prompt: system_prompt.to_string(),
runtime: None,
model: None,
provider: None,
name_pool: Vec::new(),
is_builtin: false,
is_active: true,
shared: false,
source_team: None,
source_team_persona_slug: None,
catalog_source: None,
env_vars: BTreeMap::new(),
respond_to: None,
respond_to_allowlist: Vec::new(),
parallelism: None,
created_at: "2025-01-01T00:00:00Z".to_string(),
updated_at: "2025-01-01T00:00:00Z".to_string(),
};
build_persona_event(&record)
.unwrap()
.sign_with_keys(&nostr::Keys::generate())
.unwrap()
}
pub(super) fn retain_prompt(
conn: &Connection,
kind: u32,
d_tag: &str,
pending: bool,
system_prompt: &str,
) -> nostr::Event {
let event = signed_persona(system_prompt);
retain_event(
conn,
&RetainedEvent {
kind,
pubkey: OWNER.to_string(),
d_tag: d_tag.to_string(),
content: event.content.to_string(),
created_at: event.created_at.as_secs() as i64,
raw_event: {
use nostr::JsonUtil;
event.as_json()
},
pending_sync: pending,
event_id: Some(event.id.to_hex()),
publish_blocked: false,
},
)
.unwrap();
event
}
pub(super) fn set_pending(conn: &Connection, kind: u32, d_tag: &str) {
conn.execute(
"UPDATE persona_events SET pending_sync = 1
WHERE kind = ?1 AND pubkey = ?2 AND d_tag = ?3",
rusqlite::params![kind, OWNER, d_tag],
)
.unwrap();
}
pub(super) fn run_pass(conn: &Connection, observation: &Observation) -> (Decision, usize) {
let (plan, _) = run_decision_pass(
conn,
OWNER,
&[CoordinateState {
coordinate: coordinate(),
observation: observation.clone(),
}],
)
.unwrap();
(
plan[0].1.decision.clone(),
get_pending_sync(conn).unwrap().len(),
)
}

View File

@@ -1,29 +1,14 @@
use std::path::Path;
use super::test_helpers::{coordinate, retain_prompt, run_pass, test_db, OWNER};
use super::*;
use crate::managed_agents::{
decision::{decide, ParkReason},
persona_events::build_persona_event,
retention::{
get_baseline, get_pending_sync, get_retained_event, is_publish_blocked, open_retention_db,
retain_event, set_baseline, tombstone_retention_d_tag, RetainedEvent,
get_baseline, get_pending_sync, get_retained_event, is_publish_blocked,
tombstone_retention_d_tag,
},
};
use buzz_core_pkg::kind::{KIND_DELETION, KIND_PERSONA};
const OWNER: &str = "ownerpubkeyhex";
fn test_db() -> Connection {
open_retention_db(Path::new(":memory:")).unwrap()
}
fn coordinate() -> Coordinate {
Coordinate {
kind: KIND_PERSONA,
d_tag: "test-persona".to_string(),
}
}
/// The retention key the coordinate's queued tombstone occupies — a DIFFERENT
/// primary key from the coordinate itself, which is the whole reason the gate
/// has two halves.
@@ -47,84 +32,12 @@ fn head(content: &str, created_at: i64) -> Head {
}
}
fn signed_persona(system_prompt: &str) -> nostr::Event {
use std::collections::BTreeMap;
let record = crate::managed_agents::AgentDefinition {
id: "test-persona".to_string(),
display_name: "Test".to_string(),
avatar_url: None,
system_prompt: system_prompt.to_string(),
runtime: None,
model: None,
provider: None,
name_pool: Vec::new(),
is_builtin: false,
is_active: true,
shared: false,
source_team: None,
source_team_persona_slug: None,
catalog_source: None,
env_vars: BTreeMap::new(),
respond_to: None,
respond_to_allowlist: Vec::new(),
parallelism: None,
created_at: "2025-01-01T00:00:00Z".to_string(),
updated_at: "2025-01-01T00:00:00Z".to_string(),
};
build_persona_event(&record)
.unwrap()
.sign_with_keys(&nostr::Keys::generate())
.unwrap()
}
/// Retain a row at the coordinate, optionally pending. Returns the signed
/// event so a caller can stamp a baseline naming it.
fn retain_row(conn: &Connection, kind: u32, d_tag: &str, pending: bool) -> nostr::Event {
retain_prompt(conn, kind, d_tag, pending, "prompt")
}
/// The same, with control over the persona's system prompt — which is what
/// [`CanonicalProjection`] reads, so it is how a test makes the queued row's
/// content differ from the baseline's.
fn retain_prompt(
conn: &Connection,
kind: u32,
d_tag: &str,
pending: bool,
system_prompt: &str,
) -> nostr::Event {
let event = signed_persona(system_prompt);
retain_event(
conn,
&RetainedEvent {
kind,
pubkey: OWNER.to_string(),
d_tag: d_tag.to_string(),
content: event.content.to_string(),
created_at: event.created_at.as_secs() as i64,
raw_event: {
use nostr::JsonUtil;
event.as_json()
},
pending_sync: pending,
event_id: Some(event.id.to_hex()),
publish_blocked: false,
},
)
.unwrap();
event
}
/// Re-queue an already-retained row, the way an edit or a reconcile would.
fn set_pending(conn: &Connection, kind: u32, d_tag: &str) {
conn.execute(
"UPDATE persona_events SET pending_sync = 1
WHERE kind = ?1 AND pubkey = ?2 AND d_tag = ?3",
rusqlite::params![kind, OWNER, d_tag],
)
.unwrap();
}
// ── local_state: reading the observation off a real store ──────────────────
#[test]
@@ -224,39 +137,6 @@ fn test_non_pending_tombstone_row_is_not_reported_as_queued() {
assert!(observation.queued_deletion_at.is_none());
}
#[test]
fn test_stamped_baseline_is_read_back() {
let conn = test_db();
let event = retain_row(&conn, KIND_PERSONA, "test-persona", false);
set_baseline(
&conn,
KIND_PERSONA,
OWNER,
"test-persona",
&event.id.to_hex(),
"baseline-content",
)
.unwrap();
let observation = local_state(
&conn,
OWNER,
&coordinate(),
Some(projection("baseline-content")),
HeadState::Present(head("newer", 200)),
TombstoneEvidence::NotFound,
)
.unwrap();
assert_eq!(
observation.baseline.as_ref().map(|b| b.content.as_str()),
Some("baseline-content")
);
// Disk still equals the baseline, so the differing head wins — the exact
// stale-store case the fix exists for.
assert_eq!(decide(&observation).decision, Decision::ApplyHead);
}
/// A half-written baseline (id without content, or vice versa) is not a usable
/// provenance record and must read as "no baseline".
#[test]
@@ -399,48 +279,6 @@ fn test_only_publish_deletion_releases_the_tombstone_gate() {
)));
}
/// The gate's whole purpose: a suppressed coordinate must actually vanish from
/// the flush loop's work list.
#[test]
fn test_suppressed_coordinate_is_withheld_from_the_flush_loop() {
let conn = test_db();
retain_row(&conn, KIND_PERSONA, "test-persona", true);
assert_eq!(
get_pending_sync(&conn).unwrap().len(),
1,
"precondition: the pending row is publishable"
);
let observation = Observation {
disk: Some(projection("v1")),
queued: None,
head: HeadState::Absent,
baseline: Some(projection("v1")),
queued_deletion_at: None,
tombstone: TombstoneEvidence::NotFound,
};
apply_gate(
&conn,
OWNER,
&coordinate(),
&observation,
&resolution(Decision::SuppressPublish),
)
.unwrap();
assert!(
get_pending_sync(&conn).unwrap().is_empty(),
"a suppressed coordinate must not reach the publisher"
);
// The content and its pending flag survive — suppression withholds, it
// does not discard what this device believes.
let row = get_retained_event(&conn, KIND_PERSONA, OWNER, "test-persona")
.unwrap()
.unwrap();
assert!(row.pending_sync);
}
/// Suppression must be reversible: once the head is visible again the same
/// coordinate resumes publishing, or the gate latches shut permanently.
#[test]
@@ -519,44 +357,6 @@ fn test_gate_is_scoped_to_its_own_coordinate() {
assert_eq!(pending[0].d_tag, "other-persona");
}
/// The gate has two halves because the rows have two primary keys. A pass that
/// wrote only the live half would compute a deletion suppression and enforce
/// it on the wrong row, leaving the tombstone free to publish.
#[test]
fn test_the_gate_enforces_on_the_tombstone_row_too() {
let conn = test_db();
retain_row(&conn, KIND_DELETION, &tombstone_key(), true);
assert_eq!(
get_pending_sync(&conn).unwrap().len(),
1,
"precondition: the queued tombstone is publishable"
);
let observation = Observation {
disk: None,
queued: None,
head: HeadState::Present(head("still-there", 200)),
baseline: None,
queued_deletion_at: Some(100),
tombstone: TombstoneEvidence::NotFound,
};
apply_gate(
&conn,
OWNER,
&coordinate(),
&observation,
&resolution(Decision::Park(ParkReason::NoBaselineWithHead)),
)
.unwrap();
assert!(
get_pending_sync(&conn).unwrap().is_empty(),
"a withheld deletion must not reach the publisher"
);
assert!(is_publish_blocked(&conn, KIND_DELETION, OWNER, &tombstone_key()).unwrap());
}
// ── Composition: the whole pass, not one layer ─────────────────────────────
//
// Every test above exercises one layer in isolation, which is exactly how a
@@ -582,24 +382,6 @@ fn observe(conn: &Connection, head: HeadState, disk: Option<CanonicalProjection>
.unwrap()
}
/// Run the real pass over one coordinate and report what the flush loop would
/// then publish.
fn run_pass(conn: &Connection, observation: &Observation) -> (Decision, usize) {
let (plan, _) = run_decision_pass(
conn,
OWNER,
&[CoordinateState {
coordinate: coordinate(),
observation: observation.clone(),
}],
)
.unwrap();
(
plan[0].1.decision.clone(),
get_pending_sync(conn).unwrap().len(),
)
}
/// **Will's bug, end to end, and the regression test for this whole PR.**
///
/// A second install holds the OLD system prompt on disk with a FRESH retention
@@ -657,24 +439,6 @@ fn test_stale_store_with_no_baseline_cannot_revert_a_newer_head() {
assert!(row.pending_sync);
}
/// The same bug in its second shape: a queued DELETION from a no-baseline
/// store against a live head. Worse than the revert, because an a-tag delete
/// destroys content this install has never seen rather than reverting it.
#[test]
fn test_stale_store_with_no_baseline_cannot_delete_a_live_head() {
let conn = test_db();
retain_row(&conn, KIND_DELETION, &tombstone_key(), true);
let observation = observe(&conn, HeadState::Present(head("still-there", 1)), None);
let (decision, publishable) = run_pass(&conn, &observation);
assert_eq!(decision, Decision::Park(ParkReason::NoBaselineWithHead));
assert_eq!(
publishable, 0,
"an unprovable deletion reached the publisher (decision: {decision:?})"
);
}
/// **The tombstone escape, composed over BOTH rows.**
///
/// A coordinate's record and its queued tombstone occupy different primary
@@ -742,210 +506,3 @@ fn test_no_baseline_store_withholds_both_the_record_and_its_tombstone() {
"the tombstone row must be gated at its own primary key"
);
}
/// The mirror case, and the reason suppression cannot simply block everything
/// pending: a genuine local edit against a head this install has already
/// agreed on must still publish, or the fix freezes every agent config.
#[test]
fn test_genuine_local_edit_still_publishes() {
let conn = test_db();
let event = retain_prompt(&conn, KIND_PERSONA, "test-persona", true, "my-new-edit");
// The baseline is what this install last agreed was published; the queued
// row has since moved past it.
set_baseline(
&conn,
KIND_PERSONA,
OWNER,
"test-persona",
&event.id.to_hex(),
"published-v1",
)
.unwrap();
let observation = observe(
&conn,
HeadState::Present(head("published-v1", 100)),
Some(projection("my-new-edit")),
);
let (decision, publishable) = run_pass(&conn, &observation);
assert_eq!(
decision,
Decision::PublishLocalEdit,
"a post-baseline edit must be recognized as an edit"
);
assert_eq!(publishable, 1, "the user's edit must still reach the relay");
}
/// A vacuous re-sign — the queued row publishes exactly what the baseline
/// already says — is DROPPED, not gated. Gating would hold it forever, since
/// it can never become publishable.
#[test]
fn test_vacuous_re_sign_is_cleared_rather_than_held() {
let conn = test_db();
let event = retain_prompt(&conn, KIND_PERSONA, "test-persona", true, "agreed");
// Baseline names this exact event, so the queued projection equals it.
set_baseline(
&conn,
KIND_PERSONA,
OWNER,
"test-persona",
&event.id.to_hex(),
&event.content,
)
.unwrap();
let observation = observe(
&conn,
HeadState::Present(head("newer-elsewhere", 200)),
Some(projection(&event.content)),
);
let (_, publishable) = run_pass(&conn, &observation);
assert_eq!(publishable, 0);
let row = get_retained_event(&conn, KIND_PERSONA, OWNER, "test-persona")
.unwrap()
.unwrap();
assert!(
!row.pending_sync,
"a re-sign carrying no new intent must be dropped, not held pending forever"
);
}
/// A pending row whose disk content already equals the head is the
/// crash-convergence case: stamp the baseline and stop, rather than burn a
/// `created_at` bump republishing identical content.
#[test]
fn test_pending_row_matching_the_head_stamps_and_stops() {
let conn = test_db();
retain_row(&conn, KIND_PERSONA, "test-persona", true);
let observation = observe(
&conn,
HeadState::Present(head("agreed", 100)),
Some(projection("agreed")),
);
let (decision, publishable) = run_pass(&conn, &observation);
assert_eq!(decision, Decision::Park(ParkReason::NoBaselineWithHead));
assert_eq!(publishable, 0, "identical content must not be republished");
}
/// `StampBaseline` has to actually write the baseline. It is the input every
/// other cell depends on, so a decision computed and dropped leaves the table
/// permanently in its no-provenance state.
#[test]
fn test_stamp_baseline_records_the_head_as_provenance() {
let conn = test_db();
retain_row(&conn, KIND_PERSONA, "test-persona", false);
let observation = observe(
&conn,
HeadState::Present(head("agreed", 100)),
Some(projection("agreed")),
);
run_pass(&conn, &observation);
assert_eq!(
get_baseline(&conn, KIND_PERSONA, OWNER, "test-persona").unwrap(),
Some(("id-agreed".to_string(), "agreed".to_string())),
"the baseline must be stamped from the head, content and id together"
);
}
/// The two-boot sequence that makes the fix durable rather than a one-shot:
/// boot 1 converges and stamps the baseline, boot 2 uses that provenance to
/// recognize the stale copy the reconcile re-queued.
#[test]
fn test_stamped_baseline_lets_the_next_boot_suppress_a_revert() {
let conn = test_db();
let event = retain_prompt(&conn, KIND_PERSONA, "test-persona", false, "v1");
// The head IS the event this store already holds — the converged state
// boot 1 is supposed to recognize.
let converged = Head {
event_id: event.id.to_hex(),
created_at: event.created_at.as_secs() as i64,
projection: CanonicalProjection::from_event(&event),
};
let disk = converged.projection.clone();
// Boot 1: disk equals the head, so the baseline is stamped from it.
let settled = observe(
&conn,
HeadState::Present(converged.clone()),
Some(disk.clone()),
);
let (decision, _) = run_pass(&conn, &settled);
assert_eq!(decision, Decision::StampBaseline);
assert_eq!(
get_baseline(&conn, KIND_PERSONA, OWNER, "test-persona").unwrap(),
Some((converged.event_id.clone(), converged.projection.content)),
"boot 1 must establish provenance"
);
// Boot 2: the other install published a newer edit; the reconcile on THIS
// install re-queued its unchanged copy at a manufactured timestamp.
set_pending(&conn, KIND_PERSONA, "test-persona");
let stale = observe(
&conn,
HeadState::Present(head("good-edit", 1)),
Some(disk.clone()),
);
let (decision, publishable) = run_pass(&conn, &stale);
assert_eq!(
publishable, 0,
"with a baseline, the stale copy must be recognized and withheld \
(decision: {decision:?})"
);
// Recognized as vacuous rather than merely gated: the queued row says
// exactly what the baseline already says, so it is dropped and the newer
// head is what this coordinate should adopt.
assert_eq!(decision, Decision::ApplyHead);
assert!(
!get_retained_event(&conn, KIND_PERSONA, OWNER, "test-persona")
.unwrap()
.unwrap()
.pending_sync
);
}
/// A queued deletion from a store that CAN prove its provenance still reaches
/// the relay. Suppressing every tombstone would resurrect records the user
/// deliberately removed.
#[test]
fn test_a_provable_queued_tombstone_still_publishes() {
let conn = test_db();
let tombstone = retain_row(&conn, KIND_DELETION, &tombstone_key(), true);
// Provenance at the target coordinate: this install agreed to what is
// published there, so its deletion is arbitrable (cell 4).
retain_prompt(&conn, KIND_PERSONA, "test-persona", false, "agreed");
let event = get_retained_event(&conn, KIND_PERSONA, OWNER, "test-persona")
.unwrap()
.unwrap();
set_baseline(
&conn,
KIND_PERSONA,
OWNER,
"test-persona",
event.event_id.as_deref().unwrap(),
&event.content,
)
.unwrap();
// The head is older than the tombstone, so the deletion wins.
let observation = observe(
&conn,
HeadState::Present(head("still-there", 1)),
Some(projection(&event.content)),
);
assert_eq!(
observation.queued_deletion_at,
Some(tombstone.created_at.as_secs() as i64)
);
let (decision, publishable) = run_pass(&conn, &observation);
assert_eq!(decision, Decision::PublishDeletion);
assert_eq!(publishable, 1, "the queued deletion must reach the relay");
assert_eq!(get_pending_sync(&conn).unwrap()[0].kind, KIND_DELETION);
}

View File

@@ -411,18 +411,6 @@ fn test_a_head_newer_than_the_tombstone_survives_it() {
assert!(blocks_tombstone_publication(&resolution));
}
/// Deciding a deletion from a lookup that did not complete could wipe a head
/// this device simply failed to see.
#[test]
fn test_a_deletion_behind_a_failed_lookup_defers() {
let observation = Observation {
head: HeadState::LookupFailed,
queued_deletion_at: Some(500),
..settled()
};
assert_eq!(decide(&observation).decision, Decision::Defer);
}
// ── V3.10: suppression is not deletion ─────────────────────────────────────
/// The core V3.10 rule. Relay-side history loss under one URL presents as
@@ -439,29 +427,6 @@ fn test_head_absent_with_clean_disk_suppresses_instead_of_deleting() {
assert!(blocks_publication(&resolution));
}
/// A live head is current relay truth: a later upsert legitimately re-creates
/// a coordinate, so an older tombstone must not delete it.
#[test]
fn test_live_head_overrides_an_older_tombstone() {
let observation = Observation {
tombstone: TombstoneEvidence::Found,
..settled()
};
assert_eq!(decide(&observation).decision, Decision::StampBaseline);
}
/// Proven deletion plus a head read that never completed is ambiguous: a
/// re-creation reads the same way. Neither removing nor republishing is safe.
#[test]
fn test_tombstone_with_a_failed_lookup_defers() {
let observation = Observation {
head: HeadState::LookupFailed,
tombstone: TombstoneEvidence::Found,
..settled()
};
assert_eq!(decide(&observation).decision, Decision::Defer);
}
// ── Crash convergence and the unqueued cells ───────────────────────────────
/// Inbound retains the head BEFORE writing JSON, so a crash in between leaves
@@ -505,19 +470,6 @@ fn test_untouched_disk_adopts_a_differing_head() {
assert_eq!(decide(&observation).decision, Decision::ApplyHead);
}
/// Without a baseline the difference cannot be attributed to a local edit, so
/// nothing is adopted and nothing is published.
#[test]
fn test_no_baseline_never_acts_on_a_difference() {
let observation = Observation {
disk: Some(projection("stale")),
head: HeadState::Present(head("current", 200)),
baseline: None,
..settled()
};
assert_eq!(decide(&observation).decision, Decision::Defer);
}
/// Absence on disk is never deletion intent; that requires a tombstone.
#[test]
fn test_missing_disk_record_with_live_head_restores() {
@@ -528,16 +480,6 @@ fn test_missing_disk_record_with_live_head_restores() {
assert_eq!(decide(&observation).decision, Decision::RestoreFromRelay);
}
#[test]
fn test_missing_disk_record_and_missing_head_is_a_no_op() {
let observation = Observation {
disk: None,
head: HeadState::Absent,
..settled()
};
assert_eq!(decide(&observation).decision, Decision::SuppressPublish);
}
/// A local edit with no head has no trustworthy timestamp to sign past, and
/// file mtime must not be used to manufacture one — so it needs a human.
#[test]