From 7af43b7bf0d54025f4127d8572017a409c11d684 Mon Sep 17 00:00:00 2001 From: athenanewsapi <192553512+athenanewsapi@users.noreply.github.com> Date: Thu, 13 Aug 2026 12:01:31 -0400 Subject: [PATCH 1/5] tests: truncate timestamps to microseconds so DB read-back equals in-memory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five tests (three seq_tail_tests + bitemporal::supersession_lifecycle in both storage profiles) asserted a chunk/fact's valid_from/valid_to read back from Postgres equals an in-memory Utc::now() value. Postgres timestamptz stores microseconds; on Linux Utc::now() carries nanoseconds, so .913180133 never equals the .913180 that survives the round-trip and the equality assertions fail. It passed on macOS (this dev box's Utc::now() returns µs-granular values) which masked it, so CI has been red on Linux since these tests landed (2026-08-05) while local runs looked green. Fix truncates each test's reference timestamp to microseconds at the source (SubsecRound::trunc_subsecs(6)), so the value the test writes equals the value it reads back on every platform. The behavior under test was always correct (closes written, scoping right); only the assertions' precision was wrong. No production code touched. Reported by a community member who ran the full suite on Linux and read the failure diffs. Thank you. Co-Authored-By: Claude Fable 5 --- crates/verity-server/src/seq_tail_tests.rs | 22 ++++++++++++++----- .../verity-storage-qdrant/tests/bitemporal.rs | 6 +++-- crates/verity-storage/tests/bitemporal.rs | 6 +++-- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/crates/verity-server/src/seq_tail_tests.rs b/crates/verity-server/src/seq_tail_tests.rs index af24179..99bce56 100644 --- a/crates/verity-server/src/seq_tail_tests.rs +++ b/crates/verity-server/src/seq_tail_tests.rs @@ -9,7 +9,7 @@ //! these are enforcement-soundness tests — a missing database is a //! misconfiguration to surface loudly, never a class of test to silently no-op. -use chrono::{DateTime, Duration, Utc}; +use chrono::{DateTime, Duration, SubsecRound, Utc}; use serde_json::json; use sqlx::Row; @@ -119,7 +119,10 @@ fn scope(tenant: TenantId) -> Scope { #[tokio::test] async fn shrink_redelivery_closes_the_stale_tail() { let (storage, tenant) = tail_state().await; - let t0 = Utc::now() - Duration::seconds(60); + // Microsecond resolution: Postgres stores timestamptz at µs, so an + // in-memory nanosecond `Utc::now()` (Linux) would not equal its own + // read-back and the lineage assertions below would fail by sub-µs digits. + let t0 = Utc::now().trunc_subsecs(6) - Duration::seconds(60); let t1 = t0 + Duration::seconds(30); let tag = "account:shrink"; @@ -154,7 +157,10 @@ async fn shrink_redelivery_closes_the_stale_tail() { #[tokio::test] async fn shrink_replay_is_idempotent() { let (storage, tenant) = tail_state().await; - let t0 = Utc::now() - Duration::seconds(60); + // Microsecond resolution: Postgres stores timestamptz at µs, so an + // in-memory nanosecond `Utc::now()` (Linux) would not equal its own + // read-back and the lineage assertions below would fail by sub-µs digits. + let t0 = Utc::now().trunc_subsecs(6) - Duration::seconds(60); let t1 = t0 + Duration::seconds(30); let tag = "account:replay"; @@ -180,7 +186,10 @@ async fn shrink_replay_is_idempotent() { #[tokio::test] async fn tail_close_is_scoped_to_its_document_and_source() { let (storage, tenant) = tail_state().await; - let t0 = Utc::now() - Duration::seconds(60); + // Microsecond resolution: Postgres stores timestamptz at µs, so an + // in-memory nanosecond `Utc::now()` (Linux) would not equal its own + // read-back and the lineage assertions below would fail by sub-µs digits. + let t0 = Utc::now().trunc_subsecs(6) - Duration::seconds(60); let t1 = t0 + Duration::seconds(30); // The shrink target, a NEIGHBOR document in the same source, and the SAME @@ -251,7 +260,10 @@ async fn tail_close_is_scoped_to_its_document_and_source() { #[tokio::test] async fn growth_redelivery_is_unaffected() { let (storage, tenant) = tail_state().await; - let t0 = Utc::now() - Duration::seconds(60); + // Microsecond resolution: Postgres stores timestamptz at µs, so an + // in-memory nanosecond `Utc::now()` (Linux) would not equal its own + // read-back and the lineage assertions below would fail by sub-µs digits. + let t0 = Utc::now().trunc_subsecs(6) - Duration::seconds(60); let t1 = t0 + Duration::seconds(30); let tag = "account:growth"; diff --git a/crates/verity-storage-qdrant/tests/bitemporal.rs b/crates/verity-storage-qdrant/tests/bitemporal.rs index f032096..3f77379 100644 --- a/crates/verity-storage-qdrant/tests/bitemporal.rs +++ b/crates/verity-storage-qdrant/tests/bitemporal.rs @@ -4,7 +4,7 @@ //! leg), plus a retire_entity case for this profile's chunk-retirement //! superset. Requires VERITY_TEST_DSN + VERITY_QDRANT_URL; skips when absent. -use chrono::{Duration, Utc}; +use chrono::{Duration, SubsecRound, Utc}; use rand::Rng; use serde_json::json; @@ -70,7 +70,9 @@ async fn supersession_lifecycle() { eprintln!("VERITY_TEST_DSN / VERITY_QDRANT_URL not set; skipping"); return; }; - let t0 = Utc::now() - Duration::minutes(10); + // µs resolution: Postgres timestamptz stores µs, so the valid_to equality + // checks below must not carry sub-µs digits that never survive the round-trip. + let t0 = Utc::now().trunc_subsecs(6) - Duration::minutes(10); let write = |value: serde_json::Value, at| FactWrite { tenant_id: tenant, key: key(), diff --git a/crates/verity-storage/tests/bitemporal.rs b/crates/verity-storage/tests/bitemporal.rs index a0f01cf..c10ab49 100644 --- a/crates/verity-storage/tests/bitemporal.rs +++ b/crates/verity-storage/tests/bitemporal.rs @@ -5,7 +5,7 @@ //! and `recall_fails_closed` is a direct empty-principal fail-closed assertion — //! a soundness gate that silently skips is worse than no gate. -use chrono::{Duration, Utc}; +use chrono::{Duration, SubsecRound, Utc}; use serde_json::json; use verity_core::adapter::StorageAdapter; @@ -63,7 +63,9 @@ fn read_scope(tenant: TenantId) -> Scope { #[tokio::test] async fn supersession_lifecycle() { let (adapter, tenant, episode) = test_adapter().await; - let t0 = Utc::now() - Duration::minutes(10); + // µs resolution: Postgres timestamptz stores µs, so the valid_to equality + // checks below must not carry sub-µs digits that never survive the round-trip. + let t0 = Utc::now().trunc_subsecs(6) - Duration::minutes(10); let write = |value: serde_json::Value, at| FactWrite { tenant_id: tenant, key: key(), From 95e82237f36e0af80493a6ea778c0bbcc6bde9bb Mon Sep 17 00:00:00 2001 From: athenanewsapi <192553512+athenanewsapi@users.noreply.github.com> Date: Thu, 13 Aug 2026 12:08:56 -0400 Subject: [PATCH 2/5] ci: start SpiceDB with an explicit `serve` command (unbreak the rust job) The rust job ran SpiceDB as a service container and relied on the authzed/spicedb image defaulting to `serve`, passing the flags via env. GitHub Actions can't override a service image's command, and the unpinned `:latest` image drifted to one that no longer defaults to `serve`, so it printed help and the container died at "Initialize containers" before any test ran. That, not only the timestamp assertions, is why the job was red. SpiceDB now starts as an explicit `docker run ... serve --grpc-preshared-key verity-dev-key --http-enabled` step (mirroring deploy/docker-compose.yml, which always worked), with an HTTP-gateway readiness gate before the tests. This is also immune to future `:latest` command-default drift, since the command is now explicit rather than inherited. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 48 ++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b38c63e..cfa48a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,29 +22,20 @@ jobs: --health-interval 5s --health-timeout 3s --health-retries 20 - spicedb: - # ReBAC engine (SPEC §7a). Same image + preshared key as - # deploy/docker-compose.yml so VERITY_SPICEDB_KEY matches. The v0.1 - # Rust client speaks the HTTP gateway (8443); SPICEDB_HTTP_ENABLED turns - # it on. In-memory datastore (default) — ephemeral is correct for CI; - # every soundness test writes its own schema via ensure_schema(). - # GitHub Actions service containers cannot override the image command, - # so serve flags are passed as env: SPICEDB_GRPC_PRESHARED_KEY - # (== --grpc-preshared-key), SPICEDB_HTTP_ENABLED (== --http-enabled). - image: authzed/spicedb - env: - SPICEDB_GRPC_PRESHARED_KEY: verity-dev-key - SPICEDB_HTTP_ENABLED: "true" - ports: - - 8443:8443 - - 50051:50051 - options: >- - --health-cmd "grpc_health_probe -addr=localhost:50051" - --health-interval 5s - --health-timeout 3s - --health-retries 20 + # SpiceDB is NOT a service container: GitHub Actions can't override a + # service image's command, and the authzed/spicedb image no longer + # defaults to `serve` (a `:latest` drift that silently broke this job), + # so the env-var approach printed help and the container died at init. + # It's started as an explicit `docker run ... serve` step below instead, + # mirroring deploy/docker-compose.yml exactly. steps: - uses: actions/checkout@v4 + - name: Start SpiceDB (explicit serve; runs during the build below) + run: | + docker run -d --name spicedb \ + -p 8443:8443 -p 50051:50051 \ + authzed/spicedb serve \ + --grpc-preshared-key verity-dev-key --http-enabled - uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy @@ -58,6 +49,21 @@ jobs: # resolution_precision_gate (pure, ≥0.99 precision / 0.0 false-merge-rate) # and resolution_scope_fuzz (DSN; a mis-linked entity surfacing across # scope handles fails the build). + - name: Wait for SpiceDB HTTP gateway + run: | + for i in $(seq 1 40); do + code=$(curl -s -o /dev/null -w "%{http_code}" \ + -X POST http://localhost:8443/v1/schema/read \ + -H "authorization: bearer verity-dev-key" \ + -H "content-type: application/json" -d '{}' || true) + # Any HTTP status (200 or a 4xx "no schema written yet") means it is + # serving; only 000 (connection refused) means not up yet. + if [ -n "$code" ] && [ "$code" != "000" ]; then + echo "SpiceDB responding (HTTP $code)"; exit 0 + fi + sleep 2 + done + echo "SpiceDB did not become healthy:"; docker logs spicedb || true; exit 1 - run: cargo test --workspace env: VERITY_TEST_DSN: postgres://verity:verity@localhost:5433/verity From e11a9e54a9e8be566a368e8b6035164a4e2b5002 Mon Sep 17 00:00:00 2001 From: athenanewsapi <192553512+athenanewsapi@users.noreply.github.com> Date: Thu, 13 Aug 2026 12:17:15 -0400 Subject: [PATCH 3/5] ci: fix clippy --all-targets lints in test code (the third red cause) CI runs `cargo clippy --all-targets -- -D warnings`, which lints test code; local runs this session used default-target clippy, so 14 test-only lints went unseen and failed CI. All mechanical: 10x `.err().expect()` -> `.expect_err()`, one `repeat().take()` -> `repeat_n()` (clippy --fix), and three `assert!` over compile-time constants get a justified allow (the test deliberately pins constant relationships so it breaks if the floor/default change). Behavior-identical; test code only. Co-Authored-By: Claude Fable 5 --- crates/verity-server/src/connector_worker.rs | 21 +++++++------------- crates/verity-server/src/connectors_admin.rs | 14 +++++++------ crates/verity-server/src/extract.rs | 3 +-- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/crates/verity-server/src/connector_worker.rs b/crates/verity-server/src/connector_worker.rs index b48d23b..2fea394 100644 --- a/crates/verity-server/src/connector_worker.rs +++ b/crates/verity-server/src/connector_worker.rs @@ -1484,8 +1484,7 @@ mod tests { None, None, ) - .err() - .expect("gmail must fail without subject"); + .expect_err("gmail must fail without subject"); assert!(matches!(err, SpawnError::NoConfig(_))); // Blank/whitespace subject counts as absent (matches connector abort). assert!(matches!( @@ -1574,8 +1573,7 @@ mod tests { Some(cred), None, ) - .err() - .expect("no visibility must fail"); + .expect_err("no visibility must fail"); assert!(matches!(err, SpawnError::NoConfig(_))); // Empty visibility is treated as absent. let err = assemble_spec( @@ -1588,8 +1586,7 @@ mod tests { Some(cred), None, ) - .err() - .expect("empty visibility must fail"); + .expect_err("empty visibility must fail"); assert!(matches!(err, SpawnError::NoConfig(_))); // No credential-file path → NoConfig. let err = assemble_spec( @@ -1602,8 +1599,7 @@ mod tests { None, None, ) - .err() - .expect("no credential-file must fail"); + .expect_err("no credential-file must fail"); assert!(matches!(err, SpawnError::NoConfig(_))); } @@ -1865,8 +1861,7 @@ mod tests { Some(cred), Some(cursor), ) - .err() - .expect("no visibility must fail"); + .expect_err("no visibility must fail"); assert!(matches!(err, SpawnError::NoConfig(_))); // Empty visibility is treated as absent. let err = assemble_spec( @@ -1879,8 +1874,7 @@ mod tests { Some(cred), Some(cursor), ) - .err() - .expect("empty visibility must fail"); + .expect_err("empty visibility must fail"); assert!(matches!(err, SpawnError::NoConfig(_))); // No credential-file path → NoConfig. let err = assemble_spec( @@ -1893,8 +1887,7 @@ mod tests { None, Some(cursor), ) - .err() - .expect("no credential-file must fail"); + .expect_err("no credential-file must fail"); assert!(matches!(err, SpawnError::NoConfig(_))); } diff --git a/crates/verity-server/src/connectors_admin.rs b/crates/verity-server/src/connectors_admin.rs index dc9fdba..a039e9e 100644 --- a/crates/verity-server/src/connectors_admin.rs +++ b/crates/verity-server/src/connectors_admin.rs @@ -2447,8 +2447,7 @@ mod tests { fn resolve_both_present_is_ambiguous_409() { let cred = stored_path(None); let err = resolve_backfill("gdrive", Some(&cred), Some(Path::new("/env/sa.json"))) - .err() - .expect("both present must 409"); + .expect_err("both present must 409"); assert!(matches!(err, BackfillReject::Ambiguous(_))); assert_eq!(err.status(), StatusCode::CONFLICT); } @@ -2456,8 +2455,7 @@ mod tests { #[test] fn resolve_neither_present_is_no_credential_422() { let err = resolve_backfill("gdrive", None, None) - .err() - .expect("neither present must 422"); + .expect_err("neither present must 422"); assert!(matches!(err, BackfillReject::NoCredential(_))); assert_eq!(err.status(), StatusCode::UNPROCESSABLE_ENTITY); } @@ -2466,8 +2464,7 @@ mod tests { fn gmail_requires_a_stored_subject() { // Path present (env), but gmail has no stored subject → 422. let err = resolve_backfill("gmail", None, Some(Path::new("/env/sa.json"))) - .err() - .expect("gmail without subject must 422"); + .expect_err("gmail without subject must 422"); assert!(matches!(err, BackfillReject::SubjectMissing(_))); assert_eq!(err.status(), StatusCode::UNPROCESSABLE_ENTITY); // A blank stored subject is treated as absent. @@ -2686,6 +2683,11 @@ mod tests { } } + // These assertions deliberately pin relationships between COMPILE-TIME + // constants (the sync interval floor and default). clippy sees them as + // constant-valued, which is exactly the point: the test exists to fail if + // someone changes either constant out from under the handler's floor check. + #[allow(clippy::assertions_on_constants)] #[test] fn interval_floor_is_below_the_default() { // The DB floor (60) must be <= the default the toggle applies (300), so diff --git a/crates/verity-server/src/extract.rs b/crates/verity-server/src/extract.rs index a8fbbf7..0fb3428 100644 --- a/crates/verity-server/src/extract.rs +++ b/crates/verity-server/src/extract.rs @@ -1580,8 +1580,7 @@ mod tests { #[test] fn scanned_pdf_ocr_honors_the_page_cap() { let jpeg = fixtures::jpeg_bytes(8, 8); - let pages: Vec<&[u8]> = std::iter::repeat(jpeg.as_slice()) - .take(crate::ocr::MAX_OCR_PAGES + 2) + let pages: Vec<&[u8]> = std::iter::repeat_n(jpeg.as_slice(), crate::ocr::MAX_OCR_PAGES + 2) .collect(); let bytes = fixtures::scanned_pdf_with_jpegs(&pages); let ex = expect_extracted(extract_with_ocr( From 823a9017bdcbf33f254a7111681649bfeb066e63 Mon Sep 17 00:00:00 2001 From: athenanewsapi <192553512+athenanewsapi@users.noreply.github.com> Date: Thu, 13 Aug 2026 12:22:51 -0400 Subject: [PATCH 4/5] ci: match rustfmt to the stable toolchain CI uses The clippy-autofix rewrites left two test lines formatted the way the pinned 1.97.0 rustfmt accepts but CI's stable rustfmt (a different 1.9.0 build) reformats, so `cargo fmt --all --check` failed on CI while passing locally. Reformatted with the stable toolchain so both agree. Co-Authored-By: Claude Fable 5 --- crates/verity-server/src/connectors_admin.rs | 3 +-- crates/verity-server/src/extract.rs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/verity-server/src/connectors_admin.rs b/crates/verity-server/src/connectors_admin.rs index a039e9e..7cdac91 100644 --- a/crates/verity-server/src/connectors_admin.rs +++ b/crates/verity-server/src/connectors_admin.rs @@ -2454,8 +2454,7 @@ mod tests { #[test] fn resolve_neither_present_is_no_credential_422() { - let err = resolve_backfill("gdrive", None, None) - .expect_err("neither present must 422"); + let err = resolve_backfill("gdrive", None, None).expect_err("neither present must 422"); assert!(matches!(err, BackfillReject::NoCredential(_))); assert_eq!(err.status(), StatusCode::UNPROCESSABLE_ENTITY); } diff --git a/crates/verity-server/src/extract.rs b/crates/verity-server/src/extract.rs index 0fb3428..d6ad102 100644 --- a/crates/verity-server/src/extract.rs +++ b/crates/verity-server/src/extract.rs @@ -1580,8 +1580,8 @@ mod tests { #[test] fn scanned_pdf_ocr_honors_the_page_cap() { let jpeg = fixtures::jpeg_bytes(8, 8); - let pages: Vec<&[u8]> = std::iter::repeat_n(jpeg.as_slice(), crate::ocr::MAX_OCR_PAGES + 2) - .collect(); + let pages: Vec<&[u8]> = + std::iter::repeat_n(jpeg.as_slice(), crate::ocr::MAX_OCR_PAGES + 2).collect(); let bytes = fixtures::scanned_pdf_with_jpegs(&pages); let ex = expect_extracted(extract_with_ocr( &bytes, From f081ee5f25d13d0d5d990a11c56fe4981b569d1e Mon Sep 17 00:00:00 2001 From: athenanewsapi <192553512+athenanewsapi@users.noreply.github.com> Date: Thu, 13 Aug 2026 12:32:22 -0400 Subject: [PATCH 5/5] tests: poll for the folder-watch status row instead of a single-shot check drop_file_ingests_under_visibility_and_wrong_scope_is_blind asserted the folder: connector_status row exists immediately after recall succeeds, but that row is bumped per-file inside the async ingest task and can lag chunk visibility. On CI's timing the chunk was recallable before the status row landed, so the single-shot assertion failed while the behavior was correct. Polls for the row (10s deadline, 150ms interval), mirroring the recall_until the same test already uses. No production change. Co-Authored-By: Claude Fable 5 --- crates/verity-server/src/folder_watch.rs | 27 ++++++++++++++++-------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/crates/verity-server/src/folder_watch.rs b/crates/verity-server/src/folder_watch.rs index 04cf5f4..dde5570 100644 --- a/crates/verity-server/src/folder_watch.rs +++ b/crates/verity-server/src/folder_watch.rs @@ -1948,15 +1948,24 @@ mod tests { assert!(none_hits.is_empty(), "empty scope reads nothing"); // Source registered live in Sources & Freshness as folder:. - let sources = crate::connectors::list_status_rows(state.pool(), tenant) - .await - .expect("status"); - assert!( - sources - .iter() - .any(|s| s["source"] == format!("folder:{folder}")), - "the watch registers as a live source" - ); + // The status row is bumped per-file inside the async ingest task, which + // can lag chunk visibility, so poll for it (as the recall above does) + // rather than assume it lands synchronously with the recalled chunk. + let folder_src = format!("folder:{folder}"); + let deadline = std::time::Instant::now() + Duration::from_secs(10); + let registered = loop { + let sources = crate::connectors::list_status_rows(state.pool(), tenant) + .await + .expect("status"); + if sources.iter().any(|s| s["source"] == folder_src) { + break true; + } + if std::time::Instant::now() >= deadline { + break false; + } + tokio::time::sleep(Duration::from_millis(150)).await; + }; + assert!(registered, "the watch registers as a live source"); let _ = std::fs::remove_dir_all(&dir); }