stemedb/applications/aphoria/src/tests/conflict_detection.rs
jml 200b84751e feat: add claims search, promote, stats commands and convergence engine
Adds three new Aphoria CLI commands and supporting infrastructure for
org-pattern alignment and claim tier advancement:

- `aphoria claims search` — find claims by concept pattern, predicate,
  category, or max authority tier (works local and hosted mode)
- `aphoria claims promote` — raise a claim to a higher authority tier by
  creating a superseding claim (append-only; original marked Deprecated)
- `aphoria claims stats` — breakdown of claim counts by tier and status
  for a given concept_path + predicate pair

New modules:
- `convergence.rs` — pure engine comparing local scan observations to
  remote org claims, producing `ConvergenceSuggestion`s at read time
- `types/convergence.rs` — `ConvergenceSuggestion` type with severity
  derived from the driving claim's authority tier
- `types/promotion.rs` — `PromotionRequest` / `PromotionResult` types
- `handlers/promote.rs` — promotion handler; validates tier ordering

Remote client: adds `search_claims` and `claim_stats` methods to
`RemoteClaimStore`, wiring hosted mode for all three new commands.

API (`stemedb-api`): new `/v1/claims/search` and `/v1/claims/stats`
endpoints with DTOs, plus report formatters (JSON/Markdown/SARIF/table)
for search and stats output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 08:21:37 +00:00

292 lines
9.1 KiB
Rust

//! Integration tests for conflict detection (Phase 2A).
use crate::*;
use ed25519_dalek::SigningKey;
use stemedb_core::types::{Assertion, ObjectValue, SourceClass};
/// Create a minimal test corpus for integration tests.
#[allow(clippy::vec_init_then_push)]
#[allow(dead_code)]
fn create_test_corpus(signing_key: &SigningKey) -> Vec<Assertion> {
let timestamp = crate::episteme::current_timestamp();
let mut assertions = Vec::new();
// TLS verification (RFC 5246 + OWASP)
assertions.push(crate::episteme::create_authoritative_assertion(
signing_key,
"rfc://5246/tls/cert_verification",
"enabled",
ObjectValue::Boolean(true),
SourceClass::Regulatory,
"TLS certificate verification MUST be enabled (RFC 5246)",
timestamp,
));
assertions.push(crate::episteme::create_authoritative_assertion(
signing_key,
"owasp://transport_layer/tls/cert_verification",
"enabled",
ObjectValue::Boolean(true),
SourceClass::Clinical,
"OWASP: Always verify TLS certificates",
timestamp,
));
// JWT validation (RFC 7519)
assertions.push(crate::episteme::create_authoritative_assertion(
signing_key,
"rfc://7519/jwt/audience_validation",
"enabled",
ObjectValue::Boolean(true),
SourceClass::Regulatory,
"JWT audience claim MUST be validated (RFC 7519 Section 4.1.3)",
timestamp,
));
assertions.push(crate::episteme::create_authoritative_assertion(
signing_key,
"rfc://7519/jwt/expiry_validation",
"enabled",
ObjectValue::Boolean(true),
SourceClass::Regulatory,
"JWT expiry claim MUST be validated (RFC 7519 Section 4.1.4)",
timestamp,
));
// CORS security (OWASP)
assertions.push(crate::episteme::create_authoritative_assertion(
signing_key,
"owasp://cors/allow_origin",
"config_value",
ObjectValue::Text("explicit_list".to_string()),
SourceClass::Clinical,
"OWASP: Never use wildcard (*) for CORS Allow-Origin in production",
timestamp,
));
// Secrets management (OWASP)
assertions.push(crate::episteme::create_authoritative_assertion(
signing_key,
"owasp://secrets/api_key",
"storage_method",
ObjectValue::Text("environment_or_vault".to_string()),
SourceClass::Clinical,
"OWASP: Never hardcode API keys in source code",
timestamp,
));
assertions
}
#[ignore = "Needs corpus refactor after hardcoded deletion"]
#[tokio::test]
async fn test_conflict_detection_tls_disabled() {
// Create temp project with danger_accept_invalid_certs(true)
let temp_dir =
tempfile::Builder::new().prefix("aphoria_tls_conflict").tempdir().expect("create temp dir");
let src_dir = temp_dir.path().join("src");
std::fs::create_dir_all(&src_dir).expect("create src dir");
// Write a Rust file with TLS verification disabled
std::fs::write(
src_dir.join("client.rs"),
r#"
fn create_client() -> Result<Client, Error> {
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.build()?;
Ok(client)
}
"#,
)
.expect("write file");
// Create Cargo.toml so it's detected as a Rust project
std::fs::write(
temp_dir.path().join("Cargo.toml"),
r#"
[package]
name = "testproject"
version = "0.1.0"
"#,
)
.expect("write cargo.toml");
let args = ScanArgs {
path: temp_dir.path().to_path_buf(),
format: "table".to_string(),
exit_code_enabled: true,
mode: ScanMode::Ephemeral,
debug: false,
sync: false,
file_source: FileSource::All,
benchmark: false,
show_claims: false,
strict: false,
show_observations: false,
explain_authority: false,
suggest_convergence: false,
};
let mut config = AphoriaConfig::default();
config.episteme.data_dir = temp_dir.path().join(".aphoria").join("db");
let result = run_scan(args, &config).await.expect("scan should succeed");
// Assert: conflicts not empty, has_blocks() == true
assert!(
!result.conflicts.is_empty(),
"Should detect conflicts for TLS verification disabled. \
Claims extracted: {}, Files scanned: {}",
result.claims_extracted,
result.files_scanned
);
assert!(
result.has_blocks(),
"TLS verification disabled should be a BLOCK verdict. \
Conflicts: {:?}",
result.conflicts.iter().map(|c| (&c.claim.concept_path, &c.verdict)).collect::<Vec<_>>()
);
}
#[ignore = "Needs corpus refactor after hardcoded deletion"]
#[tokio::test]
async fn test_conflict_detection_jwt_audience_disabled() {
// Create temp project with JWT audience validation disabled
let temp_dir =
tempfile::Builder::new().prefix("aphoria_jwt_conflict").tempdir().expect("create temp dir");
let src_dir = temp_dir.path().join("src");
std::fs::create_dir_all(&src_dir).expect("create src dir");
// Write a Rust file with JWT audience validation disabled
std::fs::write(
src_dir.join("auth.rs"),
r#"
fn validate_token(token: &str) -> Result<Claims, Error> {
let mut validation = Validation::default();
validation.validate_aud = false; // Disabled!
let token_data = decode::<Claims>(token, &key, &validation)?;
Ok(token_data.claims)
}
"#,
)
.expect("write file");
// Create Cargo.toml
std::fs::write(
temp_dir.path().join("Cargo.toml"),
r#"
[package]
name = "testproject"
version = "0.1.0"
"#,
)
.expect("write cargo.toml");
let args = ScanArgs {
path: temp_dir.path().to_path_buf(),
format: "table".to_string(),
exit_code_enabled: true,
mode: ScanMode::Ephemeral,
debug: false,
sync: false,
file_source: FileSource::All,
benchmark: false,
show_claims: false,
strict: false,
show_observations: false,
explain_authority: false,
suggest_convergence: false,
};
let mut config = AphoriaConfig::default();
config.episteme.data_dir = temp_dir.path().join(".aphoria").join("db");
let result = run_scan(args, &config).await.expect("scan should succeed");
// Assert: conflicts not empty for JWT audience validation
assert!(
!result.conflicts.is_empty(),
"Should detect conflicts for JWT audience validation disabled. \
Claims extracted: {}, Files scanned: {}",
result.claims_extracted,
result.files_scanned
);
// Check that at least one conflict is about JWT audience
let has_jwt_conflict = result
.conflicts
.iter()
.any(|c| c.claim.concept_path.contains("jwt") && c.claim.concept_path.contains("audience"));
assert!(
has_jwt_conflict,
"Should have a conflict about JWT audience validation. \
Conflicts: {:?}",
result.conflicts.iter().map(|c| &c.claim.concept_path).collect::<Vec<_>>()
);
}
#[ignore = "Needs corpus refactor after hardcoded deletion"]
#[tokio::test]
async fn test_no_conflicts_when_compliant() {
// Create temp project with compliant code (no dangerous patterns)
let temp_dir =
tempfile::Builder::new().prefix("aphoria_compliant").tempdir().expect("create temp dir");
let src_dir = temp_dir.path().join("src");
std::fs::create_dir_all(&src_dir).expect("create src dir");
// Write a Rust file with compliant code
std::fs::write(
src_dir.join("main.rs"),
r#"
fn main() {
println!("Hello, world!");
}
"#,
)
.expect("write file");
// Create Cargo.toml
std::fs::write(
temp_dir.path().join("Cargo.toml"),
r#"
[package]
name = "testproject"
version = "0.1.0"
"#,
)
.expect("write cargo.toml");
let args = ScanArgs {
path: temp_dir.path().to_path_buf(),
format: "table".to_string(),
exit_code_enabled: true,
mode: ScanMode::Ephemeral,
debug: false,
sync: false,
file_source: FileSource::All,
benchmark: false,
show_claims: false,
strict: false,
show_observations: false,
explain_authority: false,
suggest_convergence: false,
};
let mut config = AphoriaConfig::default();
config.episteme.data_dir = temp_dir.path().join(".aphoria").join("db");
let result = run_scan(args, &config).await.expect("scan should succeed");
// No dangerous patterns = no claims = no conflicts
assert!(
result.conflicts.is_empty(),
"Compliant code should have no conflicts. Found: {:?}",
result.conflicts.iter().map(|c| &c.claim.concept_path).collect::<Vec<_>>()
);
}