Break monolith source files into focused modules: - stemedb-core/types.rs → types/ directory (assertion, source, gold_standard, etc.) - stemedb-storage: audit_store, quota_store, trust_rank_store, vector_index, vote_store → module directories - stemedb-ingest/worker.rs → worker/ with separate test modules - stemedb-query: engine, materializer, query → module directories - stemedb-lens: epoch_aware, skeptic → module directories - stemedb-sim/lib.rs → agent, arenas/, helpers, runner, strategy, types - stemedb-api/tests: integration_tests → http_basic, http_validation, http_epoch, http_pipeline - stemedb-api/tests: e2e_flow_test → e2e_full_pipeline, e2e_lens_resolution - stemedb-query/tests: e2e_pipeline → e2e_pipeline + e2e_decay Also adds new features: gold standard verification, escalation handlers, admin endpoints, concept hierarchy spec, arena roadmap, and Go SDK. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
274 lines
9.6 KiB
Rust
274 lines
9.6 KiB
Rust
//! HTTP integration tests for Epoch endpoint operations.
|
|
//!
|
|
//! Coverage:
|
|
//! - POST /v1/epoch - Epoch creation with validation
|
|
//! - Epoch name validation (empty, whitespace)
|
|
//! - Supersession validation (supersedes hash + supersession_type)
|
|
//! - Hex validation for supersedes field
|
|
//! - Deterministic ID generation (content-addressed)
|
|
//! - Default timestamp handling
|
|
|
|
#![allow(clippy::expect_used)]
|
|
|
|
mod common;
|
|
|
|
use axum::{
|
|
body::Body,
|
|
http::{Request, StatusCode},
|
|
};
|
|
use serde_json::json;
|
|
use tower::ServiceExt;
|
|
|
|
use stemedb_api::create_router;
|
|
|
|
// ============================================================================
|
|
// Epoch Creation Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_create_epoch_success() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let epoch = json!({
|
|
"name": "GAAP-2024",
|
|
"start_timestamp": 1704067200
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/epoch")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
let status = response.status();
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
|
|
assert_eq!(status, StatusCode::CREATED, "Body: {:?}", json);
|
|
|
|
assert_eq!(json["status"], "created");
|
|
// BLAKE3 hash = 32 bytes = 64 hex characters
|
|
assert_eq!(json["hash"].as_str().expect("hash").len(), 64);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Epoch Name Validation Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_create_epoch_empty_name_fails() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let epoch = json!({
|
|
"name": "",
|
|
"start_timestamp": 1704067200
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/epoch")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
assert!(json["error"].as_str().expect("error").contains("cannot be empty"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_create_epoch_whitespace_name_fails() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let epoch = json!({
|
|
"name": " ",
|
|
"start_timestamp": 1704067200
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/epoch")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
assert!(json["error"].as_str().expect("error").contains("cannot be empty"));
|
|
}
|
|
|
|
// ============================================================================
|
|
// Epoch Supersession Validation Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_create_epoch_with_supersedes() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
// Supersession requires both supersedes hash AND supersession_type
|
|
let epoch = json!({
|
|
"name": "GAAP-2025",
|
|
"supersedes": "a".repeat(64),
|
|
"supersession_type": "Temporal",
|
|
"start_timestamp": 1735689600
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/epoch")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::CREATED);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
assert_eq!(json["status"], "created");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_create_epoch_supersedes_without_type_fails() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
// Providing supersedes but NOT supersession_type should fail
|
|
let epoch = json!({
|
|
"name": "GAAP-2025",
|
|
"supersedes": "a".repeat(64),
|
|
// Missing supersession_type!
|
|
"start_timestamp": 1735689600
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/epoch")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
assert!(json["error"].as_str().expect("error").contains("supersession_type is required"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_create_epoch_invalid_supersedes_hex() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
let epoch = json!({
|
|
"name": "GAAP-2025",
|
|
"supersedes": "invalid", // Too short, should be 64 hex chars
|
|
"supersession_type": "Invalidate",
|
|
"start_timestamp": 1735689600
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/epoch")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
|
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
// Should complain about hex length
|
|
assert!(json["error"].as_str().expect("error").contains("Expected 64 hex characters"));
|
|
}
|
|
|
|
// ============================================================================
|
|
// Epoch Content-Addressed ID Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_create_epoch_deterministic_id() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
// Create the same epoch twice - should get the same ID
|
|
let epoch = json!({
|
|
"name": "Deterministic-Test",
|
|
"start_timestamp": 1000
|
|
});
|
|
|
|
let request1 = Request::builder()
|
|
.uri("/v1/epoch")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response1 = app.clone().oneshot(request1).await.expect("Request");
|
|
assert_eq!(response1.status(), StatusCode::CREATED);
|
|
let body1 = axum::body::to_bytes(response1.into_body(), usize::MAX).await.expect("Body");
|
|
let json1: serde_json::Value = serde_json::from_slice(&body1).expect("JSON");
|
|
let hash1 = json1["hash"].as_str().expect("hash");
|
|
|
|
let request2 = Request::builder()
|
|
.uri("/v1/epoch")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response2 = app.oneshot(request2).await.expect("Request");
|
|
assert_eq!(response2.status(), StatusCode::CREATED);
|
|
let body2 = axum::body::to_bytes(response2.into_body(), usize::MAX).await.expect("Body");
|
|
let json2: serde_json::Value = serde_json::from_slice(&body2).expect("JSON");
|
|
let hash2 = json2["hash"].as_str().expect("hash");
|
|
|
|
// Same input should produce same ID
|
|
assert_eq!(hash1, hash2, "Same epoch input should produce deterministic ID");
|
|
}
|
|
|
|
// ============================================================================
|
|
// Epoch Timestamp Default Tests
|
|
// ============================================================================
|
|
|
|
#[tokio::test]
|
|
async fn test_create_epoch_defaults_timestamp() {
|
|
let env = common::create_test_env().await;
|
|
let app = create_router(env.state);
|
|
|
|
// Omit start_timestamp - should default to current time
|
|
let epoch = json!({
|
|
"name": "Default-Timestamp-Test"
|
|
});
|
|
|
|
let request = Request::builder()
|
|
.uri("/v1/epoch")
|
|
.method("POST")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(serde_json::to_vec(&epoch).expect("JSON")))
|
|
.expect("Request");
|
|
|
|
let response = app.oneshot(request).await.expect("Request");
|
|
assert_eq!(response.status(), StatusCode::CREATED);
|
|
|
|
// If we got 201, the timestamp defaulted successfully
|
|
let body = axum::body::to_bytes(response.into_body(), usize::MAX).await.expect("Body");
|
|
let json: serde_json::Value = serde_json::from_slice(&body).expect("JSON");
|
|
assert_eq!(json["status"], "created");
|
|
}
|