Skip to main content

khive_db/
diagnostics.rs

1//! Non-mutating-by-intent WAL/checkpoint diagnostics (read-only operator
2//! surface).
3//!
4//! Answers "is a reader pinning the checkpoint / why is the WAL at 64MiB"
5//! without raw SQL against a production store.
6//!
7//! NOT read-only. [`checkpoint_probe`](crate::diagnostics::checkpoint_probe) issues a real
8//! `PRAGMA wal_checkpoint(PASSIVE)`, and a PASSIVE checkpoint that succeeds
9//! BACKFILLS WAL frames into the main database — ordinary database-page
10//! writes, on the happy path. That I/O is the point: the busy/log_frames/
11//! checkpointed_frames triple is the pin-depth signal this surface exists to
12//! report, and SQLite exposes no read-only API for those counters. The
13//! guarantee is that nothing here changes logical state or destroys
14//! evidence, not that nothing touches the disk.
15//!
16//! What IS guaranteed, and what the narrowings below buy:
17//!
18//! * never creates a missing database file,
19//! * never escalates to TRUNCATE,
20//! * never perturbs the counters it reports,
21//! * never deletes a walpin sidecar entry.
22//!
23//! Deliberate narrowings make those claims true rather than aspirational:
24//!
25//! 1. [`checkpoint_probe`](crate::diagnostics::checkpoint_probe) runs a
26//!    single `PRAGMA wal_checkpoint(PASSIVE)` —
27//!    which never blocks readers or writers — and does NOT route through
28//!    [`crate::checkpoint::checkpoint_once`]: that path mutates
29//!    `TruncateState`, may escalate to TRUNCATE, and double-counts the
30//!    ADR-091 process-global counters. A verb that reports state must not
31//!    perturb the state it reports.
32//! 2. The probe's connection comes from
33//!    `ConnectionPool::open_standalone_writer`, opened without
34//!    `SQLITE_OPEN_CREATE`. A missing database yields `checkpoint_probe:
35//!    null` plus a `checkpoint_probe_error`, never a freshly created file.
36//! 3. The WAL-pin sidecar directory in this crate is enumerated only through
37//!    the read-only OS holder census (`walpin::census_holders`). This
38//!    tree's `khive-db` exposes sidecar directory enumeration only via
39//!    `walpin::enumerate_live`, which unlinks malformed, dead,
40//!    identity-mismatched, and stale entries as part of its cleanup pass —
41//!    a diagnostic request must not be what destroys that forensic
42//!    evidence, so `wal_pin_attribution` deliberately does not call it.
43//!    Sidecar-to-holder reconciliation (`reporting`/`registered_silent_pids`/
44//!    `sidecar_entries`/`fully_attributed`) is therefore reported empty with
45//!    an explicit `unavailable_reason` rather than fabricated from a
46//!    mutating enumeration.
47//!
48//! The counters are process-global statics inside this crate, so a report is
49//! only meaningful when built inside the process that owns the checkpoint
50//! task (the daemon). Every payload therefore carries
51//! [`BuildIdentity`](crate::diagnostics::BuildIdentity), so a reading is
52//! self-labeling about which build's counters it describes.
53
54use std::path::{Path, PathBuf};
55use std::time::Duration;
56
57use rusqlite::Connection;
58use serde::Serialize;
59
60use crate::checkpoint;
61use crate::pool::ConnectionPool;
62
63/// Raw `PRAGMA wal_checkpoint(PASSIVE)` return row.
64///
65/// SQLite returns three columns: `busy` (1 when the checkpoint could not run
66/// to completion because a writer/reader held it back), `log` (frames
67/// currently in the WAL), and `checkpointed` (frames moved into the database
68/// by THIS call). Pin depth is `log - checkpointed`.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
70pub struct CheckpointProbe {
71    pub busy: i64,
72    pub log_frames: i64,
73    pub checkpointed_frames: i64,
74}
75
76impl CheckpointProbe {
77    /// Frames still pinned behind the backfill boundary. Negative components
78    /// (an in-memory or non-WAL database reports `-1`) clamp to 0.
79    pub fn pin_depth(&self) -> i64 {
80        (self.log_frames - self.checkpointed_frames).max(0)
81    }
82}
83
84/// Issue one PASSIVE checkpoint on `conn` and return the raw triple.
85///
86/// PASSIVE never blocks writers or readers, so this is safe against a live
87/// daemon. Touches none of the ADR-091 counters — unlike the periodic
88/// checkpoint task's own observation path, this does not mirror into the
89/// process-global gauges.
90///
91/// This WRITES. A PASSIVE checkpoint that makes progress backfills WAL
92/// frames into the main database file; that is normal checkpoint I/O, and on
93/// the happy path it is what the reported `checkpointed_frames` counts. The
94/// call is non-mutating in the sense that matters for a diagnostic — no
95/// logical state changes, no escalation, no evidence destroyed — but it is
96/// not write-free, and callers must not describe it as such.
97pub fn checkpoint_probe(conn: &Connection) -> rusqlite::Result<CheckpointProbe> {
98    conn.query_row("PRAGMA wal_checkpoint(PASSIVE)", [], |row| {
99        Ok(CheckpointProbe {
100            busy: row.get(0)?,
101            log_frames: row.get(1)?,
102            checkpointed_frames: row.get(2)?,
103        })
104    })
105}
106
107/// The six ADR-091 checkpoint counters, read as one snapshot.
108///
109/// The two `Option` fields carry the `u64::MAX` "never observed" sentinel as
110/// `None`, so a caller serializing this never sees `18446744073709551615`
111/// where it means "no observation yet".
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
113pub struct CheckpointCounters {
114    pub last_observed_wal_pages: Option<u64>,
115    pub truncate_attempts: u64,
116    pub truncate_consecutive_failures: u64,
117    pub checkpoint_skipped_ticks: u64,
118    pub checkpoint_consecutive_skips: u64,
119    pub checkpoint_last_skip_wal_pages: Option<u64>,
120}
121
122/// Snapshot the six process-global ADR-091 counters.
123pub fn checkpoint_counters() -> CheckpointCounters {
124    CheckpointCounters {
125        last_observed_wal_pages: checkpoint::last_observed_wal_pages(),
126        truncate_attempts: checkpoint::truncate_attempts(),
127        truncate_consecutive_failures: checkpoint::truncate_consecutive_failures(),
128        checkpoint_skipped_ticks: checkpoint::checkpoint_skipped_ticks(),
129        checkpoint_consecutive_skips: checkpoint::checkpoint_consecutive_skips(),
130        checkpoint_last_skip_wal_pages: checkpoint::checkpoint_last_skip_wal_pages(),
131    }
132}
133
134/// Which build produced this reading.
135///
136/// The counters above are process-global: a stale daemon reports a stale
137/// build's state. `build_hash` is `None` unless the binary was stamped with
138/// one — this crate does not introduce a build-metadata mechanism, it only
139/// reports whatever the caller already has.
140#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
141pub struct BuildIdentity {
142    pub version: String,
143    pub build_hash: Option<String>,
144}
145
146impl BuildIdentity {
147    /// Identity of the crate that compiled this call site.
148    pub fn from_env(version: &str, build_hash: Option<&str>) -> Self {
149        Self {
150            version: version.to_string(),
151            build_hash: build_hash.map(str::to_string),
152        }
153    }
154}
155
156/// Absolute-free WAL file state for one database path.
157#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
158pub struct WalFileState {
159    pub wal_path: String,
160    /// `None` when the `-wal` sidecar does not exist or could not be stat'd —
161    /// the reason is in `unavailable_reason`.
162    pub wal_size_bytes: Option<u64>,
163    pub unavailable_reason: Option<String>,
164}
165
166/// Stat `<db_path>-wal` and report its size in bytes.
167pub fn wal_file_state(db_path: &Path) -> WalFileState {
168    let wal_path = wal_sidecar_path(db_path);
169    match std::fs::metadata(&wal_path) {
170        Ok(md) => WalFileState {
171            wal_path: wal_path.display().to_string(),
172            wal_size_bytes: Some(md.len()),
173            unavailable_reason: None,
174        },
175        Err(e) => WalFileState {
176            wal_path: wal_path.display().to_string(),
177            wal_size_bytes: None,
178            unavailable_reason: Some(e.to_string()),
179        },
180    }
181}
182
183/// `<db_path>-wal`, built by suffixing the file name (not by replacing an
184/// extension — `khive.db` must map to `khive.db-wal`).
185fn wal_sidecar_path(db_path: &Path) -> PathBuf {
186    let mut s = db_path.as_os_str().to_os_string();
187    s.push("-wal");
188    PathBuf::from(s)
189}
190
191/// WAL-pin attribution: who currently holds the database open, and how
192/// complete that answer is.
193///
194/// Field names and shape mirror the reference diagnostics surface this was
195/// ported from, so external consumers parsing this payload keep working.
196/// This tree's `khive-db` lacks a read-only sidecar enumeration primitive
197/// (see the module docs), so the sidecar-derived fields below are always
198/// empty here; `available`/`unavailable_reason` say so explicitly rather
199/// than silently reporting an incomplete answer as complete.
200#[derive(Debug, Clone, PartialEq, Serialize)]
201pub struct WalPinAttribution {
202    /// `false` plus an `unavailable_reason` whenever the OS census failed,
203    /// or when only a partial (census-only) answer is available.
204    pub available: bool,
205    pub unavailable_reason: Option<String>,
206    /// OS-derived census of every PID holding the DB file open.
207    pub census_holder_pids: Vec<u32>,
208    pub census_uninspectable_pids: Vec<u32>,
209    pub census_truncated: bool,
210    pub census_is_complete: bool,
211    /// Always empty in this port — see the module docs.
212    pub reporting: Vec<WalPinHolder>,
213    /// Always empty in this port — see the module docs.
214    pub registered_silent_pids: Vec<u32>,
215    /// Always empty in this port — see the module docs.
216    pub unknown_pids: Vec<u32>,
217    /// Always empty in this port — see the module docs.
218    pub census_pids_without_attribution: Vec<u32>,
219    /// Always `false` in this port: sidecar reconciliation never ran, so
220    /// completeness can never be claimed.
221    pub fully_attributed: bool,
222    /// Always empty in this port — see the module docs.
223    pub sidecar_entries: Vec<serde_json::Value>,
224    pub sidecar_listing_truncated: bool,
225    pub sidecar_entries_cleanup_would_reap: usize,
226}
227
228/// One PID's heartbeat as reported to an operator. Retained for shape
229/// compatibility with the reference payload; this port never populates it
230/// (see [`WalPinAttribution`] docs).
231#[derive(Debug, Clone, PartialEq, Serialize)]
232pub struct WalPinHolder {
233    pub pid: u32,
234    pub process_role: String,
235    pub current_oldest_tx_age_secs: f64,
236    pub oldest_tx_label: Option<String>,
237    pub attribution_is_evidence_backed: bool,
238}
239
240impl WalPinAttribution {
241    fn unavailable(reason: impl Into<String>) -> Self {
242        Self {
243            available: false,
244            unavailable_reason: Some(reason.into()),
245            census_holder_pids: Vec::new(),
246            census_uninspectable_pids: Vec::new(),
247            census_truncated: false,
248            census_is_complete: false,
249            reporting: Vec::new(),
250            registered_silent_pids: Vec::new(),
251            unknown_pids: Vec::new(),
252            census_pids_without_attribution: Vec::new(),
253            fully_attributed: false,
254            sidecar_entries: Vec::new(),
255            sidecar_listing_truncated: false,
256            sidecar_entries_cleanup_would_reap: 0,
257        }
258    }
259}
260
261/// Build the WAL-pin attribution for `db_path`.
262///
263/// Unix-only: the OS census requires it. Everywhere else this degrades to
264/// `available: false` with a reason rather than failing the whole report.
265#[cfg(unix)]
266pub fn wal_pin_attribution(db_path: &Path, _sweep_interval: Duration) -> WalPinAttribution {
267    use crate::walpin;
268
269    let census = match walpin::census_holders(db_path) {
270        Ok(c) => c,
271        Err(e) => return WalPinAttribution::unavailable(format!("census_holders failed: {e}")),
272    };
273
274    let mut census_holder_pids: Vec<u32> = census.holders.iter().copied().collect();
275    census_holder_pids.sort_unstable();
276
277    WalPinAttribution {
278        available: false,
279        unavailable_reason: Some(
280            "sidecar-to-holder reconciliation not available: this tree's khive-db exposes \
281             sidecar enumeration only via walpin::enumerate_live, which deletes stale/malformed \
282             entries as part of its cleanup pass; a diagnostics probe must not delete forensic \
283             sidecar evidence, so only the OS holder census below was collected"
284                .to_string(),
285        ),
286        census_holder_pids,
287        census_uninspectable_pids: census.uninspectable_pids.clone(),
288        census_truncated: census.truncated,
289        census_is_complete: census.is_complete(),
290        reporting: Vec::new(),
291        registered_silent_pids: Vec::new(),
292        unknown_pids: Vec::new(),
293        census_pids_without_attribution: Vec::new(),
294        fully_attributed: false,
295        sidecar_entries: Vec::new(),
296        sidecar_listing_truncated: false,
297        sidecar_entries_cleanup_would_reap: 0,
298    }
299}
300
301#[cfg(not(unix))]
302pub fn wal_pin_attribution(_db_path: &Path, _sweep_interval: Duration) -> WalPinAttribution {
303    WalPinAttribution::unavailable("WAL-pin attribution requires a Unix platform")
304}
305
306/// The full diagnostics payload.
307#[derive(Debug, Clone, PartialEq, Serialize)]
308pub struct DbDiagnostics {
309    pub build: BuildIdentity,
310    /// `None` for an in-memory backend — the file-backed sections then carry
311    /// their own unavailability reasons.
312    pub db_path: Option<String>,
313    pub wal_file: Option<WalFileState>,
314    pub checkpoint_counters: CheckpointCounters,
315    pub checkpoint_probe: Option<CheckpointProbe>,
316    pub checkpoint_probe_error: Option<String>,
317    pub wal_pin: WalPinAttribution,
318}
319
320/// Assemble the report for `pool`'s database.
321///
322/// The probe target is the pool's own configured path — one source of truth,
323/// so the report can never describe a file the pool is not bound to. An
324/// in-memory pool has no path: the counters are still real (they are
325/// process-global), but every file-backed section degrades to an explicit
326/// "unavailable" with a reason rather than being silently omitted.
327///
328/// The PASSIVE probe goes through `ConnectionPool::open_standalone_writer`,
329/// opened WITHOUT `SQLITE_OPEN_CREATE`, so a diagnostic request against a
330/// missing file returns `checkpoint_probe: null` with a
331/// `checkpoint_probe_error` instead of creating a database. Running on a
332/// standalone connection also keeps checkpoint I/O off the pooled writer
333/// mutex.
334pub fn collect(
335    pool: &ConnectionPool,
336    build: BuildIdentity,
337    sweep_interval: Duration,
338) -> DbDiagnostics {
339    let counters = checkpoint_counters();
340
341    let Some(path) = pool.config().path.clone() else {
342        return DbDiagnostics {
343            build,
344            db_path: None,
345            wal_file: None,
346            checkpoint_counters: counters,
347            checkpoint_probe: None,
348            checkpoint_probe_error: Some(
349                "in-memory database: no WAL file and no checkpoint to probe".to_string(),
350            ),
351            wal_pin: WalPinAttribution::unavailable(
352                "in-memory database: no file for the OS holder census",
353            ),
354        };
355    };
356
357    let (probe, probe_error) = match probe_pool(pool) {
358        Ok(p) => (Some(p), None),
359        Err(e) => (None, Some(e)),
360    };
361
362    DbDiagnostics {
363        build,
364        db_path: Some(path.display().to_string()),
365        wal_file: Some(wal_file_state(&path)),
366        checkpoint_counters: counters,
367        checkpoint_probe: probe,
368        checkpoint_probe_error: probe_error,
369        wal_pin: wal_pin_attribution(&path, sweep_interval),
370    }
371}
372
373/// Run one PASSIVE probe on a guarded standalone connection.
374///
375/// Every failure — missing file, read-only pool, in-memory pool, or the
376/// pragma itself — comes back as an error string the caller surfaces as
377/// `checkpoint_probe_error`. Nothing here can create a file.
378fn probe_pool(pool: &ConnectionPool) -> Result<CheckpointProbe, String> {
379    let conn = pool
380        .open_standalone_writer()
381        .map_err(|e| format!("guarded standalone open refused: {e}"))?;
382    checkpoint_probe(&conn).map_err(|e| format!("PRAGMA wal_checkpoint(PASSIVE) failed: {e}"))
383}
384
385#[cfg(test)]
386mod tests {
387    use serial_test::serial;
388
389    use super::*;
390    use crate::pool::{ConnectionPool, PoolConfig};
391
392    fn seeded_pool(dir: &tempfile::TempDir) -> (ConnectionPool, PathBuf) {
393        let path = dir.path().join("diag.db");
394        let pool = ConnectionPool::new(PoolConfig {
395            path: Some(path.clone()),
396            ..PoolConfig::default()
397        })
398        .expect("pool open");
399        {
400            let writer = pool.try_writer().expect("writer");
401            writer
402                .conn()
403                .execute_batch(
404                    "CREATE TABLE t (x INTEGER); \
405                     INSERT INTO t VALUES (1), (2), (3);",
406                )
407                .expect("seed writes");
408        }
409        (pool, path)
410    }
411
412    #[test]
413    fn checkpoint_probe_returns_a_well_formed_triple_on_a_file_backed_db() {
414        let dir = tempfile::tempdir().expect("tempdir");
415        let (pool, _path) = seeded_pool(&dir);
416        let conn = pool.open_standalone_writer().expect("standalone");
417
418        let probe = checkpoint_probe(&conn).expect("probe must succeed on a WAL database");
419
420        assert!(
421            probe.busy == 0 || probe.busy == 1,
422            "busy is a 0/1 flag, got {}",
423            probe.busy
424        );
425        assert!(
426            probe.log_frames >= 0,
427            "a WAL database must report a non-negative frame count, got {}",
428            probe.log_frames
429        );
430        assert!(
431            probe.checkpointed_frames >= 0,
432            "checkpointed frames must be non-negative, got {}",
433            probe.checkpointed_frames
434        );
435        assert!(
436            probe.checkpointed_frames <= probe.log_frames,
437            "a PASSIVE pass cannot checkpoint more frames than the WAL holds: {probe:?}"
438        );
439        assert!(probe.pin_depth() >= 0, "pin depth clamps at 0: {probe:?}");
440    }
441
442    /// The verb must not perturb the state it reports: the probe touches none
443    /// of the six ADR-091 counters.
444    #[test]
445    #[serial(checkpoint_skip_metrics)]
446    fn checkpoint_probe_does_not_perturb_the_adr091_counters() {
447        crate::checkpoint::reset_checkpoint_metrics_for_tests();
448        let dir = tempfile::tempdir().expect("tempdir");
449        let (pool, _path) = seeded_pool(&dir);
450        let conn = pool.open_standalone_writer().expect("standalone");
451
452        let before = checkpoint_counters();
453        for _ in 0..3 {
454            checkpoint_probe(&conn).expect("probe must succeed");
455        }
456        let after = checkpoint_counters();
457
458        assert_eq!(
459            before, after,
460            "checkpoint_probe must leave every ADR-091 counter untouched"
461        );
462    }
463
464    #[test]
465    fn wal_file_state_reports_the_sidecar_size_for_a_live_db() {
466        let dir = tempfile::tempdir().expect("tempdir");
467        let (_pool, path) = seeded_pool(&dir);
468
469        let state = wal_file_state(&path);
470        assert!(
471            state.wal_path.ends_with("diag.db-wal"),
472            "WAL path is the db path plus a -wal suffix, got {}",
473            state.wal_path
474        );
475        assert!(
476            state.wal_size_bytes.is_some(),
477            "a seeded WAL database must have a stat-able -wal file: {state:?}"
478        );
479        assert!(state.unavailable_reason.is_none(), "{state:?}");
480    }
481
482    #[test]
483    fn wal_file_state_degrades_with_a_reason_when_the_sidecar_is_absent() {
484        let dir = tempfile::tempdir().expect("tempdir");
485        let state = wal_file_state(&dir.path().join("never-created.db"));
486        assert!(state.wal_size_bytes.is_none());
487        assert!(
488            state.unavailable_reason.is_some(),
489            "an absent WAL file must carry a reason, not a silent zero: {state:?}"
490        );
491    }
492
493    #[test]
494    fn collect_on_a_file_backed_db_carries_build_identity_and_every_counter() {
495        let dir = tempfile::tempdir().expect("tempdir");
496        let (pool, _path) = seeded_pool(&dir);
497
498        let report = collect(
499            &pool,
500            BuildIdentity::from_env("9.9.9", Some("deadbeef")),
501            Duration::from_secs(30),
502        );
503
504        assert_eq!(report.build.version, "9.9.9");
505        assert_eq!(report.build.build_hash.as_deref(), Some("deadbeef"));
506        assert!(report.db_path.is_some());
507        assert!(
508            report.checkpoint_probe.is_some(),
509            "file-backed collect must land a probe; error was {:?}",
510            report.checkpoint_probe_error
511        );
512        assert!(
513            report.wal_file.as_ref().and_then(|w| w.wal_size_bytes) >= Some(0),
514            "wal_size_bytes must be a non-negative byte count when present"
515        );
516
517        let json = serde_json::to_value(&report).expect("report serializes");
518        let counters = json
519            .get("checkpoint_counters")
520            .expect("counters section present");
521        for key in [
522            "last_observed_wal_pages",
523            "truncate_attempts",
524            "truncate_consecutive_failures",
525            "checkpoint_skipped_ticks",
526            "checkpoint_consecutive_skips",
527            "checkpoint_last_skip_wal_pages",
528        ] {
529            assert!(counters.get(key).is_some(), "counter {key} must be present");
530        }
531    }
532
533    /// The `u64::MAX` never-observed sentinel must serialize as `null`, never
534    /// as a huge number an operator would read as a real page count.
535    #[test]
536    fn never_observed_sentinels_serialize_as_null() {
537        let counters = CheckpointCounters {
538            last_observed_wal_pages: None,
539            truncate_attempts: 0,
540            truncate_consecutive_failures: 0,
541            checkpoint_skipped_ticks: 0,
542            checkpoint_consecutive_skips: 0,
543            checkpoint_last_skip_wal_pages: None,
544        };
545        let json = serde_json::to_value(counters).expect("serializes");
546        assert!(json["last_observed_wal_pages"].is_null());
547        assert!(json["checkpoint_last_skip_wal_pages"].is_null());
548    }
549
550    /// A missing configured path must never be created by a diagnostic
551    /// request. `open_standalone_writer` opens without `SQLITE_OPEN_CREATE`,
552    /// so the probe degrades to an error and the file stays absent.
553    #[test]
554    fn probe_refuses_a_missing_configured_path_without_creating_it() {
555        let dir = tempfile::tempdir().expect("tempdir");
556        let (pool, path) = seeded_pool(&dir);
557
558        for suffix in ["", "-wal", "-shm"] {
559            let mut p = path.as_os_str().to_os_string();
560            p.push(suffix);
561            let _ = std::fs::remove_file(PathBuf::from(p));
562        }
563        assert!(!path.exists(), "precondition: the database file is gone");
564
565        let report = collect(
566            &pool,
567            BuildIdentity::from_env("0.0.0", None),
568            Duration::from_secs(30),
569        );
570
571        assert!(
572            report.checkpoint_probe.is_none(),
573            "a missing database must not yield a probe result: {report:?}"
574        );
575        assert!(
576            report.checkpoint_probe_error.is_some(),
577            "a missing database must say why there is no probe: {report:?}"
578        );
579        assert!(
580            !path.exists(),
581            "a diagnostics request must never create the database it was asked about"
582        );
583    }
584
585    /// In-memory backends have no WAL file and no census target: the report
586    /// still returns, with explicit reasons rather than missing sections.
587    #[test]
588    fn collect_degrades_gracefully_for_an_in_memory_backend() {
589        let pool = ConnectionPool::new(PoolConfig::default()).expect("in-memory pool");
590        let report = collect(
591            &pool,
592            BuildIdentity::from_env("0.0.0", None),
593            Duration::from_secs(30),
594        );
595
596        assert!(report.db_path.is_none());
597        assert!(report.wal_file.is_none());
598        assert!(report.checkpoint_probe.is_none());
599        assert!(
600            report.checkpoint_probe_error.is_some(),
601            "an in-memory report must say WHY there is no probe"
602        );
603        assert!(!report.wal_pin.available);
604        assert!(report.wal_pin.unavailable_reason.is_some());
605    }
606
607    /// This port's WAL-pin attribution never claims full reconciliation —
608    /// see the module docs on why sidecar enumeration is not wired here.
609    #[cfg(unix)]
610    #[test]
611    fn wal_pin_attribution_reports_census_but_never_claims_full_attribution() {
612        let dir = tempfile::tempdir().expect("tempdir");
613        let (pool, path) = seeded_pool(&dir);
614        let _ = &pool;
615
616        let pin = wal_pin_attribution(&path, Duration::from_secs(30));
617
618        assert!(
619            !pin.fully_attributed,
620            "sidecar reconciliation is not ported: this must never claim completeness"
621        );
622        assert!(
623            pin.unavailable_reason.is_some(),
624            "the gap must be explained, not silent: {pin:?}"
625        );
626        assert!(pin.sidecar_entries.is_empty());
627        assert!(pin.reporting.is_empty());
628    }
629}