stemedb/applications/aphoria/src/tests/golden_path.rs
jml e73bf3c4b7 feat(aphoria): add --show-claims flag to display all extracted claims
Implements the --show-claims feature requested by users who need to verify
extractors are working correctly and debug false negatives.

Changes:
- Add `claims: Option<Vec<ExtractedClaim>>` field to ScanResult
- Add `--show-claims` CLI flag to scan command
- Add `show_claims: bool` parameter to ScanArgs
- Populate claims in scanner when flag is set (sorted by file, then line)
- Display claims in all output formats:
  * Table: New "Extracted Claims" section with concept/value/file/line/confidence
  * JSON: Top-level `claims` array with full claim details
  * Markdown: "## Extracted Claims" section with table
  * SARIF: Informational-level results (level: "note") for IDE integration

User outcome:
- `aphoria scan . --show-claims` displays all claims (not just conflicts)
- Users can verify extractors detected their code patterns
- Users can debug false negatives by seeing what WAS extracted
- Builds trust through transparency

Quality:
- Zero breaking changes (opt-in flag, backward compatible)
- All tests passing (943 passed)
- Clippy clean (no warnings)
- Manual testing verified all 4 output formats

Addresses user feedback from /home/jml/Workspace/maxwell/.aphoria/.notes-for-aphoria-team

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 00:39:54 +00:00

191 lines
7.6 KiB
Rust

//! Golden Path Loop Tests (Bless → Export → Import → Scan with Policy Source).
use crate::*;
#[tokio::test]
async fn test_golden_path_bless_export_import_scan() {
// This tests the full "Golden Path" loop:
// 1. Project A: Bless a pattern as the authoritative standard
// 2. Export as Trust Pack
// 3. Project B: Import the Trust Pack
// 4. Scan shows policy source attribution
let temp_dir_a =
tempfile::Builder::new().prefix("aphoria_golden_a").tempdir().expect("create temp dir A");
let temp_dir_b =
tempfile::Builder::new().prefix("aphoria_golden_b").tempdir().expect("create temp dir B");
// ========== Project A: Bless a pattern ==========
let mut config_a = AphoriaConfig::default();
config_a.episteme.data_dir = temp_dir_a.path().join(".aphoria").join("db");
// Create .aphoria directory for the agent key
let aphoria_dir_a = temp_dir_a.path().join(".aphoria");
std::fs::create_dir_all(&aphoria_dir_a).expect("create .aphoria dir A");
// Open LocalEpisteme and bless a pattern
{
let mut episteme = crate::episteme::LocalEpisteme::open(&config_a, temp_dir_a.path())
.await
.expect("open A");
// Create blessed assertion (not "acknowledged", but the actual predicate "enabled")
let claim = ExtractedClaim {
concept_path: "code://rust/acme/grpc/tls".to_string(),
predicate: "enabled".to_string(),
value: stemedb_core::types::ObjectValue::Boolean(true),
file: "aphoria_bless".to_string(),
line: 0,
matched_text: "Blessed: enabled = true".to_string(),
confidence: 1.0,
description: "All services MUST use mTLS".to_string(),
};
episteme.ingest_claims(&[claim]).await.expect("ingest blessed claim");
episteme.shutdown().await;
}
// ========== Export as Trust Pack ==========
let pack_path = temp_dir_a.path().join("acme-standard.pack");
// We need to directly create a pack since export_policy uses current_dir()
let signing_key = crate::bridge::load_or_generate_key(temp_dir_a.path()).expect("load key A");
// Create a blessed assertion for the pack using the bridge helper
let blessed_claim = ExtractedClaim {
concept_path: "code://rust/acme/grpc/tls".to_string(),
predicate: "enabled".to_string(),
value: stemedb_core::types::ObjectValue::Boolean(true),
file: "aphoria_bless".to_string(),
line: 0,
matched_text: "Blessed: enabled = true".to_string(),
confidence: 1.0,
description: "All services MUST use mTLS".to_string(),
};
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let blessed_assertion =
crate::bridge::claim_to_assertion(&blessed_claim, &signing_key, timestamp);
let pack = crate::policy::TrustPack::new(
"Acme Security Standard".to_string(),
"1.0.0".to_string(),
vec![blessed_assertion],
vec![], // No aliases
&signing_key,
)
.expect("create pack");
pack.save(&pack_path).expect("save pack");
// ========== Project B: Import and scan ==========
let mut config_b = AphoriaConfig::default();
config_b.episteme.data_dir = temp_dir_b.path().join(".aphoria").join("db");
// Add the policy to config for scanning
config_b.policies = vec![pack_path.to_string_lossy().to_string()];
// Create project B with code that DISABLES TLS (conflicts with blessed pattern)
let src_dir_b = temp_dir_b.path().join("src");
std::fs::create_dir_all(&src_dir_b).expect("create src dir B");
std::fs::write(
src_dir_b.join("server.rs"),
r#"
fn create_server() -> Result<Server, Error> {
// Disabling TLS - should conflict with blessed pattern
let server = tonic::transport::Server::builder()
.tls_config(None) // TLS disabled
.build()?;
Ok(server)
}
"#,
)
.expect("write file B");
std::fs::write(
temp_dir_b.path().join("Cargo.toml"),
r#"[package]
name = "projectb"
version = "0.1.0"
"#,
)
.expect("write cargo.toml B");
// Create .aphoria directory for project B
let aphoria_dir_b = temp_dir_b.path().join(".aphoria");
std::fs::create_dir_all(&aphoria_dir_b).expect("create .aphoria dir B");
// Run ephemeral scan with the imported policy
let args = ScanArgs {
path: temp_dir_b.path().to_path_buf(),
format: "table".to_string(),
exit_code_enabled: false,
mode: ScanMode::Ephemeral,
debug: false,
sync: false,
file_source: FileSource::All,
benchmark: false,
show_claims: false,
};
let result = run_scan(args, &config_b).await.expect("scan should succeed");
// Verify the pack was loaded and policy source is tracked
// The scan should show conflicts where policy_source is populated
// Note: The current extractors may not extract the exact pattern we blessed,
// so we mainly verify the policy ingestion worked
// The key assertion: policies are loaded and can be queried
// Let's verify the policy manager loaded the pack correctly
let policy_manager = crate::policy::PolicyManager::new(&config_b.corpus.cache_dir);
let policies = policy_manager.load_policies(&config_b.policies).expect("load policies");
assert_eq!(policies.len(), 1, "Should have loaded 1 policy pack");
assert_eq!(policies[0].header.name, "Acme Security Standard");
assert_eq!(policies[0].header.version, "1.0.0");
assert_eq!(policies[0].assertions.len(), 1);
assert_eq!(policies[0].assertions[0].subject, "code://rust/acme/grpc/tls");
assert_eq!(policies[0].assertions[0].predicate, "enabled");
// Verify the scan completed (even if no specific conflicts match our blessed pattern)
assert!(result.files_scanned > 0, "Should have scanned files");
}
#[tokio::test]
async fn test_bless_args_value_parsing() {
// Test the parse_value function for different value types
use crate::policy_ops::parse_value;
// Boolean values
assert_eq!(parse_value("true"), stemedb_core::types::ObjectValue::Boolean(true));
assert_eq!(parse_value("false"), stemedb_core::types::ObjectValue::Boolean(false));
assert_eq!(parse_value("TRUE"), stemedb_core::types::ObjectValue::Boolean(true));
assert_eq!(parse_value("False"), stemedb_core::types::ObjectValue::Boolean(false));
// Numeric values
assert_eq!(parse_value("42"), stemedb_core::types::ObjectValue::Number(42.0));
assert_eq!(parse_value("2.71"), stemedb_core::types::ObjectValue::Number(2.71));
assert_eq!(parse_value("-1.5"), stemedb_core::types::ObjectValue::Number(-1.5));
// Text values (anything that doesn't parse as bool or number)
assert_eq!(parse_value("TLS1.3"), stemedb_core::types::ObjectValue::Text("TLS1.3".to_string()));
assert_eq!(
parse_value("enabled"),
stemedb_core::types::ObjectValue::Text("enabled".to_string())
);
// Scientific notation should work
assert_eq!(parse_value("1e10"), stemedb_core::types::ObjectValue::Number(1e10));
// NaN and Infinity should be treated as text (defensive behavior)
assert_eq!(parse_value("nan"), stemedb_core::types::ObjectValue::Text("nan".to_string()));
assert_eq!(
parse_value("infinity"),
stemedb_core::types::ObjectValue::Text("infinity".to_string())
);
assert_eq!(parse_value("inf"), stemedb_core::types::ObjectValue::Text("inf".to_string()));
}