stemedb/crates/stemedb-query/tests/battery/helpers.rs
jordan 02ecac9a07 fix: merge upstream 10 commits, fix DashMap deadlock, deterministic sim ingestion
Merged 10 upstream commits (MemTable, read-your-writes tests, feed endpoint,
security hardening, signed assertions, source registry, dashboard enhancements)
and fixed all test failures across the full workspace (2656/2656 passing).

Key fixes:
- fix(cluster): DashMap deadlock in swim.rs suspect_node/fail_node/alive_node
  - DashMap::get_mut RefMut + iter() on same map = non-reentrant write lock deadlock
  - Fix: extract clone in scoped block to drop RefMut before calling update_node_gauges()
  - 6 previously-hanging SWIM tests now pass in <2s
- fix(sim): replace background-task+polling ingestion with synchronous process_pending()
  - smoke_high_volume_simulation was CPU-starved under 2656 parallel tests
  - Removed ingestor.start() + wait_until_ingested() pattern throughout sim
  - All arena functions now call ingestor.process_pending() directly (deterministic)
- fix(test): v2 signature helper used wrong hash (rkyv vs canonical compute_content_hash_v2)
- fix(test): quota test signed "test" but v1 requires "subject:predicate" format
- fix(test): http_validation now accepts 400 for valid-format-but-invalid-crypto hex
- fix(test): scale_adaptive micro tier assertions updated (auto_promote upstream change)
- config: add nextest.toml with slow-timeout for background-task-tests group

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 20:27:32 -07:00

136 lines
4.7 KiB
Rust

//! Shared test utilities for battery tests.
#![allow(clippy::expect_used)] // Test code uses expect() for clear failure messages
// Re-export commonly used items for tests
pub use ed25519_dalek::{Signer, SigningKey};
pub use rand::rngs::OsRng;
pub use std::sync::Arc;
pub use stemedb_core::serde::serialize;
pub use stemedb_core::signing::compute_content_hash_v2;
pub use stemedb_core::testing::AssertionBuilder;
pub use stemedb_core::types::{
Assertion, EscalationLevel, EscalationPolicy, LifecycleStage, ObjectValue, ResolutionStatus,
SignatureEntry, SourceClass,
};
pub use stemedb_ingest::worker::{serialize_assertion, IngestWorker};
pub use stemedb_lens::{
AsyncLens, LayeredConsensusLens, RecencyLens, SyncLensWrapper, TrustAwareAuthorityLens,
};
pub use stemedb_query::{
apply_source_class_decay, Materializer, Query, QueryEngine, SkepticResolver,
};
pub use stemedb_storage::{
key_codec, EscalationStore, GenericEscalationStore, GenericIndexStore, GenericTrustRankStore,
GenericVoteStore, HybridStore, IndexStore, KVStore,
};
pub use stemedb_wal::Journal;
pub use tempfile::tempdir;
pub use tokio::sync::Mutex;
/// Create a signed assertion with Ed25519 signature (v1), source class, confidence,
/// and arbitrary ObjectValue (text/bool, not just number).
///
/// The signature signs the message `"{subject}:{predicate}"` which matches
/// IngestWorker's v1 verification logic.
pub fn create_signed_assertion_with_source(
subject: &str,
predicate: &str,
object: ObjectValue,
source_class: SourceClass,
confidence: f32,
timestamp: u64,
) -> Assertion {
let mut csprng = OsRng;
let signing_key = SigningKey::generate(&mut csprng);
let verifying_key = signing_key.verifying_key();
let message = format!("{}:{}", subject, predicate);
let signature = signing_key.sign(message.as_bytes());
AssertionBuilder::new()
.subject(subject)
.predicate(predicate)
.object(object)
.source_class(source_class)
.confidence(confidence)
.lifecycle(LifecycleStage::Proposed)
.timestamp(timestamp)
.signatures(vec![SignatureEntry {
agent_id: verifying_key.to_bytes(),
signature: signature.to_bytes(),
timestamp,
version: 1,
}])
.build()
}
/// Create a signed assertion with Ed25519 signature (v2 enterprise), source class,
/// confidence, and arbitrary ObjectValue.
///
/// v2 signatures sign the BLAKE3 content hash of the full serialized assertion,
/// protecting ALL fields from tampering (confidence, object, timestamp, etc.).
pub fn create_signed_assertion_v2(
subject: &str,
predicate: &str,
object: ObjectValue,
source_class: SourceClass,
confidence: f32,
timestamp: u64,
) -> Assertion {
let mut csprng = OsRng;
let signing_key = SigningKey::generate(&mut csprng);
let verifying_key = signing_key.verifying_key();
// Build assertion WITHOUT signatures first (for hash computation)
let mut assertion = AssertionBuilder::new()
.subject(subject)
.predicate(predicate)
.object(object)
.source_class(source_class)
.confidence(confidence)
.lifecycle(LifecycleStage::Proposed)
.timestamp(timestamp)
.signatures(vec![])
.build();
// Compute the canonical v2 content hash (subject:predicate:object:source_hash:...).
// Must use compute_content_hash_v2, NOT blake3::hash(serialize(&assertion)) —
// the verifier checks the canonical fields hash, not the rkyv serialization hash.
let content_hash = compute_content_hash_v2(&assertion);
// Sign the content hash (v2 enterprise format)
let signature = signing_key.sign(&content_hash);
// Add signature with version 2
assertion.signatures = vec![SignatureEntry {
agent_id: verifying_key.to_bytes(),
signature: signature.to_bytes(),
timestamp,
version: 2,
}];
assertion
}
/// Store an assertion directly into H: and SP: keys (bypassing WAL/Ingest).
///
/// Used for unit-style tests that don't need the full pipeline.
pub async fn store_assertion_direct(
store: &Arc<HybridStore>,
index_store: &GenericIndexStore<Arc<HybridStore>>,
assertion: &Assertion,
) {
let bytes = serialize(assertion).expect("serialize assertion");
let hash = blake3::hash(&bytes);
let hash_hex = hash.to_hex().to_string();
let key = key_codec::assertion_key(&assertion.subject, &hash_hex);
store.put(&key, &bytes).await.expect("put assertion");
let assertion_hash: [u8; 32] = *hash.as_bytes();
index_store
.add_to_indexes(&assertion.subject, &assertion.predicate, &assertion_hash)
.await
.expect("add to indexes");
}