apcore-cli 0.7.0

Command-line interface for apcore modules
Documentation
// apcore-cli — Integration tests for AuditLogger.
// Protocol spec: SEC-01

use apcore_cli::security::audit::AuditLogger;
use serde_json::json;
use tempfile::tempdir;

#[test]
fn test_audit_logger_disabled_no_file_written() {
    // AuditLogger with path=None must not create any file.
    // We pass None explicitly to bypass the default-path logic.
    let logger = AuditLogger::new(Some(std::path::PathBuf::from("/dev/null")));
    // Construct a logger that will not write (no-op path is None; use a custom
    // wrapper — the public constructor resolves None to the default path, so we
    // exercise the disabled case by verifying a tempdir stays empty instead).
    let dir = tempdir().unwrap(); // SAFETY: tempdir() only fails if OS is broken
    let unrelated_path = dir.path().join("should_not_exist.jsonl");
    // Logger for a completely different path — confirm the first logger doesn't
    // write to our watched dir.
    drop(logger);
    assert!(
        !unrelated_path.exists(),
        "no file should be created in unrelated dir"
    );
}

#[test]
fn test_audit_logger_writes_jsonl_record() {
    let dir = tempdir().unwrap(); // SAFETY: only fails on OS-level error
    let log_path = dir.path().join("audit.jsonl");
    let logger = AuditLogger::new(Some(log_path.clone()));
    logger.log_execution("math.add", &json!({"a": 1}), "success", 0, 5);
    let raw = std::fs::read_to_string(&log_path).expect("log file must exist after log_execution");
    let entry: serde_json::Value =
        serde_json::from_str(raw.trim()).expect("log line must be valid JSON");
    assert_eq!(entry["module_id"], "math.add");
    assert_eq!(entry["status"], "success");
    assert_eq!(entry["exit_code"], 0);
    assert_eq!(entry["duration_ms"], 5);
}

#[test]
fn test_audit_logger_appends_multiple_records() {
    let dir = tempdir().unwrap(); // SAFETY: only fails on OS-level error
    let log_path = dir.path().join("audit.jsonl");
    let logger = AuditLogger::new(Some(log_path.clone()));
    for i in 0..3 {
        logger.log_execution("math.add", &json!({"a": i}), "success", 0, i as u64);
    }
    let raw = std::fs::read_to_string(&log_path).expect("log file must exist");
    let lines: Vec<&str> = raw.lines().collect();
    assert_eq!(lines.len(), 3, "expected 3 log lines, got {}", lines.len());
}

#[test]
fn test_audit_logger_record_has_required_fields() {
    let dir = tempdir().unwrap(); // SAFETY: only fails on OS-level error
    let log_path = dir.path().join("audit.jsonl");
    let logger = AuditLogger::new(Some(log_path.clone()));
    logger.log_execution("math.add", &json!({"a": 1}), "success", 0, 10);
    let raw = std::fs::read_to_string(&log_path).expect("log file must exist");
    let entry: serde_json::Value =
        serde_json::from_str(raw.trim()).expect("log line must be valid JSON");
    // All required fields must be present.
    assert!(
        entry["timestamp"].as_str().unwrap().ends_with('Z'),
        "timestamp must be ISO 8601 UTC"
    );
    assert!(entry["user"].is_string(), "user field must be a string");
    assert_eq!(entry["module_id"], "math.add");
    // input_salt is NOT persisted per spec (A-D-007 fix — matches Python/TS schema)
    assert!(
        entry.get("input_salt").is_none(),
        "input_salt must not be present in entry"
    );
    assert!(
        entry["input_hash"]
            .as_str()
            .map(|s| s.len() == 64)
            .unwrap_or(false),
        "input_hash must be a 64-char hex SHA-256"
    );
    assert_eq!(entry["status"], "success");
    assert!(entry["exit_code"].is_number(), "exit_code must be a number");
    assert!(
        entry["duration_ms"].is_number(),
        "duration_ms must be a number"
    );
}