oxidized-state 0.3.1

SurrealDB backend for AIVCS state persistence (Layer 0)
Documentation
//! Integration tests for SurrealDB schema initialization
//!
//! These tests verify that the migration functions properly initialize
//! all tables with correct constraints and indexes.

use oxidized_state::{
    DbRunEventRecord as RunEventRecord, DbRunRecord as RunRecord, ReleaseRecordSchema,
};
use serde_json::json;

// Note: Full schema initialization test requires a running SurrealDB instance.
// To test locally:
//   surreal start --log debug
//   cargo test --test schema_init -- --ignored
//
// For now, schema validation is covered by:
// - Serialization tests below (verify schema correctness)
// - Trait contract tests in crates/oxidized-state/tests/trait_contracts.rs
// - SurrealDB implementation tests (coming in M1-PR4)

#[test]
fn test_run_record_serialization() {
    // Verify RunRecord can be serialized to JSON (needed for SurrealDB)
    let run = RunRecord::new(
        "run-123".to_string(),
        "spec-abc".to_string(),
        Some("git-sha".to_string()),
        "agent-1".to_string(),
        json!({"env": "test"}),
    );

    let json = serde_json::to_string(&run).expect("Failed to serialize");
    assert!(json.contains("run-123"));
    assert!(json.contains("RUNNING"));
    assert!(json.contains("\"success\":false"));
}

#[test]
fn test_run_event_record_serialization() {
    // Verify RunEventRecord can be serialized to JSON
    let event = RunEventRecord::new(
        "run-123".to_string(),
        1,
        "node_entered".to_string(),
        json!({"node_id": "n1"}),
    );

    let json = serde_json::to_string(&event).expect("Failed to serialize");
    assert!(json.contains("run-123"));
    assert!(json.contains("\"seq\":1"));
    assert!(json.contains("node_entered"));
}

#[test]
fn test_release_record_serialization() {
    // Verify ReleaseRecordSchema can be serialized to JSON
    let release = ReleaseRecordSchema::new(
        "my-agent".to_string(),
        "spec-digest".to_string(),
        Some("v1.0.0".to_string()),
        "alice".to_string(),
        Some("Initial release".to_string()),
    );

    let json = serde_json::to_string(&release).expect("Failed to serialize");
    assert!(json.contains("my-agent"));
    assert!(json.contains("v1.0.0"));
    assert!(json.contains("alice"));
}

#[test]
fn test_run_record_state_transitions() {
    // Verify state transitions work correctly
    let mut run = RunRecord::new(
        "run-123".to_string(),
        "spec-abc".to_string(),
        None,
        "agent-1".to_string(),
        json!({}),
    );

    // Start in "RUNNING" state
    assert_eq!(run.status, "RUNNING");
    assert_eq!(run.total_events, 0);
    assert!(!run.success);
    assert!(run.completed_at.is_none());

    // Complete the run
    run = run.complete(10, Some("final-digest".to_string()), 5000);
    assert_eq!(run.status, "COMPLETED");
    assert_eq!(run.total_events, 10);
    assert!(run.success);
    assert!(run.completed_at.is_some());
    assert_eq!(run.final_state_digest, Some("final-digest".to_string()));
    assert_eq!(run.duration_ms, 5000);
}

#[test]
fn test_run_record_fail_transition() {
    // Verify fail transition works correctly
    let mut run = RunRecord::new(
        "run-456".to_string(),
        "spec-xyz".to_string(),
        None,
        "agent-2".to_string(),
        json!({}),
    );

    // Start in "RUNNING" state
    assert_eq!(run.status, "RUNNING");

    // Fail the run
    run = run.fail(3, 1500);
    assert_eq!(run.status, "FAILED");
    assert_eq!(run.total_events, 3);
    assert!(!run.success);
    assert!(run.completed_at.is_some());
    assert_eq!(run.duration_ms, 1500);
}

#[test]
fn test_unique_constraint_run_id_concept() {
    // Document the uniqueness constraint for run_id
    // (In-memory test; actual DB constraint is in SurrealDB)
    let run1 = RunRecord::new(
        "run-123".to_string(),
        "spec-a".to_string(),
        None,
        "agent".to_string(),
        json!({}),
    );

    let run2 = RunRecord::new(
        "run-456".to_string(),
        "spec-a".to_string(),
        None,
        "agent".to_string(),
        json!({}),
    );

    // Both have same spec_digest but different run_ids
    assert_eq!(run1.spec_digest, run2.spec_digest);
    assert_ne!(run1.run_id, run2.run_id);
}

#[test]
fn test_monotonic_seq_constraint_concept() {
    // Document the monotonic seq constraint
    // (In-memory test; actual DB constraint is in SurrealDB)
    let event1 = RunEventRecord::new("run-123".to_string(), 1, "Started".to_string(), json!({}));

    let event2 = RunEventRecord::new(
        "run-123".to_string(),
        2,
        "Processing".to_string(),
        json!({}),
    );

    let event3 = RunEventRecord::new("run-123".to_string(), 3, "Completed".to_string(), json!({}));

    // Events have monotonically increasing seq
    assert_eq!(event1.seq, 1);
    assert_eq!(event2.seq, 2);
    assert_eq!(event3.seq, 3);
    assert!(event1.seq < event2.seq && event2.seq < event3.seq);
}

#[test]
fn test_index_keys_present_in_records() {
    // Verify that all fields used in indexes are present
    let run = RunRecord::new(
        "run-123".to_string(),
        "spec-abc".to_string(),
        Some("abc123".to_string()),
        "agent-1".to_string(),
        json!({"env": "test"}),
    );

    // Index fields: run_id, spec_digest, git_sha, agent_name, created_at
    assert!(!run.run_id.is_empty());
    assert!(!run.spec_digest.is_empty());
    assert!(run.git_sha.is_some());
    assert!(!run.agent_name.is_empty());
    // created_at is set automatically

    // Composite index fields: (spec_digest, created_at)
    assert!(!run.spec_digest.is_empty());
    // created_at is set

    // Composite index fields: (run_id, status)
    assert!(!run.run_id.is_empty());
    assert!(!run.status.is_empty());
}