Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 27 additions & 21 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
21 changes: 7 additions & 14 deletions crates/verity-server/src/connector_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(_)));
}

Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(_)));
}

Expand Down
15 changes: 8 additions & 7 deletions crates/verity-server/src/connectors_admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2447,17 +2447,14 @@ 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);
}

#[test]
fn resolve_neither_present_is_no_credential_422() {
let err = resolve_backfill("gdrive", None, None)
.err()
.expect("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);
}
Expand All @@ -2466,8 +2463,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.
Expand Down Expand Up @@ -2686,6 +2682,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
Expand Down
5 changes: 2 additions & 3 deletions crates/verity-server/src/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1580,9 +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(jpeg.as_slice())
.take(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,
Expand Down
27 changes: 18 additions & 9 deletions crates/verity-server/src/folder_watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1948,15 +1948,24 @@ mod tests {
assert!(none_hits.is_empty(), "empty scope reads nothing");

// Source registered live in Sources & Freshness as folder:<name>.
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);
}
Expand Down
22 changes: 17 additions & 5 deletions crates/verity-server/src/seq_tail_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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";

Expand All @@ -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
Expand Down Expand Up @@ -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";

Expand Down
6 changes: 4 additions & 2 deletions crates/verity-storage-qdrant/tests/bitemporal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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(),
Expand Down
6 changes: 4 additions & 2 deletions crates/verity-storage/tests/bitemporal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(),
Expand Down
Loading