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>
191 lines
7.6 KiB
Rust
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()));
|
|
}
|