cellos-core 0.7.3

CellOS domain types and ports — typed authority, formation DAG, CloudEvent envelopes, RBAC primitives. No I/O.
Documentation
use cellos_core::ExecutionCellDocument;

/// Ensures the checked-in minimal example deserializes and round-trips with serde.
#[test]
fn execution_cell_minimal_example() {
    let raw = include_str!("../../../contracts/examples/execution-cell-minimal.valid.json");
    let doc: ExecutionCellDocument = serde_json::from_str(raw).expect("parse");
    assert_eq!(doc.api_version, "cellos.io/v1");
    assert_eq!(doc.kind, "ExecutionCell");
    assert_eq!(doc.spec.id, "demo-cell-001");
    assert!(doc.spec.correlation.is_none());
    let again = serde_json::to_string(&doc).expect("serialize");
    let doc2: ExecutionCellDocument = serde_json::from_str(&again).expect("re-parse");
    assert_eq!(doc2.spec.id, doc.spec.id);
}

#[test]
fn execution_cell_with_command_example() {
    use cellos_core::validate_execution_cell_document;

    let raw = include_str!("../../../contracts/examples/execution-cell-with-command.valid.json");
    let doc: ExecutionCellDocument = serde_json::from_str(raw).expect("parse");
    validate_execution_cell_document(&doc).expect("valid spec");
    assert!(doc.spec.run.is_some());
}

#[test]
fn execution_cell_with_run_limits_example() {
    use cellos_core::validate_execution_cell_document;

    let raw = include_str!("../../../contracts/examples/execution-cell-with-run-limits.valid.json");
    let doc: ExecutionCellDocument = serde_json::from_str(raw).expect("parse");
    validate_execution_cell_document(&doc).expect("valid spec");
    let run = doc.spec.run.as_ref().expect("run");
    assert_eq!(run.timeout_ms, Some(30_000));
    let limits = run.limits.as_ref().expect("limits");
    assert_eq!(limits.memory_max_bytes, Some(268_435_456));
    let cpu = limits.cpu_max.as_ref().expect("cpu max");
    assert_eq!(cpu.quota_micros, 50_000);
    assert_eq!(cpu.period_micros, Some(100_000));
}

#[test]
fn execution_cell_ci_correlation_example() {
    let raw = include_str!("../../../contracts/examples/execution-cell-ci-correlation.valid.json");
    let doc: ExecutionCellDocument = serde_json::from_str(raw).expect("parse");
    let c = doc.spec.correlation.as_ref().expect("correlation");
    assert_eq!(c.platform.as_deref(), Some("github"));
    assert_eq!(c.external_run_id.as_deref(), Some("123456789"));
    assert!(c.labels.as_ref().unwrap().contains_key("repository"));
    let ingress = doc.spec.ingress.as_ref().expect("ingress");
    assert!(ingress.git.is_some());
    assert!(ingress
        .oci_image
        .as_ref()
        .unwrap()
        .reference
        .contains("ghcr.io"));
    let again = serde_json::to_string(&doc).expect("serialize");
    let doc2: ExecutionCellDocument = serde_json::from_str(&again).expect("re-parse");
    assert_eq!(doc2.spec.policy, doc.spec.policy);
}

#[test]
fn execution_cell_github_oidc_s3_example() {
    use cellos_core::{validate_execution_cell_document, ExportTarget, WorkloadIdentityKind};

    let raw = include_str!("../../../contracts/examples/execution-cell-github-oidc-s3.valid.json");
    let doc: ExecutionCellDocument = serde_json::from_str(raw).expect("parse");
    validate_execution_cell_document(&doc).expect("valid spec");

    let identity = doc.spec.identity.as_ref().expect("identity");
    assert_eq!(identity.kind, WorkloadIdentityKind::FederatedOidc);
    assert_eq!(identity.provider, "github-actions");
    assert_eq!(identity.secret_ref, "AWS_WEB_IDENTITY");

    let export = doc.spec.export.as_ref().expect("export");
    let targets = export.targets.as_ref().expect("targets");
    match &targets[0] {
        ExportTarget::S3(target) => {
            assert_eq!(target.name, "artifact-bucket");
            assert_eq!(target.bucket, "acme-cellos-artifacts");
        }
        other => panic!("unexpected target variant: {other:?}"),
    }
    assert_eq!(
        export.artifacts.as_ref().unwrap()[0].target.as_deref(),
        Some("artifact-bucket")
    );
}

#[test]
fn execution_cell_github_oidc_multi_export_example() {
    use cellos_core::{validate_execution_cell_document, ExportTarget, WorkloadIdentityKind};

    let raw = include_str!(
        "../../../contracts/examples/execution-cell-github-oidc-multi-export.valid.json"
    );
    let doc: ExecutionCellDocument = serde_json::from_str(raw).expect("parse");
    validate_execution_cell_document(&doc).expect("valid spec");

    let identity = doc.spec.identity.as_ref().expect("identity");
    assert_eq!(identity.kind, WorkloadIdentityKind::FederatedOidc);

    let export = doc.spec.export.as_ref().expect("export");
    let targets = export.targets.as_ref().expect("targets");
    assert_eq!(targets.len(), 2);

    match &targets[0] {
        ExportTarget::S3(target) => {
            assert_eq!(target.name, "artifact-bucket");
            assert_eq!(target.bucket, "acme-cellos-artifacts");
        }
        other => panic!("unexpected target variant: {other:?}"),
    }
    match &targets[1] {
        ExportTarget::Http(target) => {
            assert_eq!(target.name, "artifact-api");
            assert_eq!(target.base_url, "https://artifacts.acme.internal/upload");
            assert_eq!(target.secret_ref.as_deref(), Some("ARTIFACT_API_TOKEN"));
        }
        other => panic!("unexpected target variant: {other:?}"),
    }

    let artifacts = export.artifacts.as_ref().expect("artifacts");
    assert_eq!(artifacts[0].target.as_deref(), Some("artifact-bucket"));
    assert_eq!(artifacts[1].target.as_deref(), Some("artifact-api"));
}

#[test]
fn execution_cell_with_environment_example() {
    use cellos_core::validate_execution_cell_document;

    let raw =
        include_str!("../../../contracts/examples/execution-cell-with-environment.valid.json");
    let doc: ExecutionCellDocument = serde_json::from_str(raw).expect("parse");
    validate_execution_cell_document(&doc).expect("valid spec");

    let env = doc.spec.environment.as_ref().expect("environment");
    assert_eq!(env.image_reference, "ubuntu:24.04");
    assert!(env.image_digest.as_deref().unwrap().starts_with("sha256:"));
    assert_eq!(env.template_id.as_deref(), Some("ubuntu-24-04-build"));

    // round-trip
    let again = serde_json::to_string(&doc).expect("serialize");
    let doc2: ExecutionCellDocument = serde_json::from_str(&again).expect("re-parse");
    assert_eq!(doc2.spec.environment, doc.spec.environment);
}

#[test]
fn execution_cell_runtime_leased_secret_broker_example() {
    use cellos_core::{validate_execution_cell_document, SecretDeliveryMode};

    let raw = include_str!(
        "../../../contracts/examples/execution-cell-runtime-leased-secret-broker.valid.json"
    );
    let doc: ExecutionCellDocument = serde_json::from_str(raw).expect("parse");
    validate_execution_cell_document(&doc).expect("valid spec");
    let run = doc.spec.run.as_ref().expect("run");
    assert_eq!(run.secret_delivery, SecretDeliveryMode::RuntimeLeasedBroker);
}

#[test]
fn execution_cell_with_telemetry_example() {
    use cellos_core::{validate_execution_cell_document, TelemetryChannel};

    let raw = include_str!("../../../contracts/examples/execution-cell-with-telemetry.valid.json");
    let doc: ExecutionCellDocument = serde_json::from_str(raw).expect("parse");
    validate_execution_cell_document(&doc).expect("valid spec");

    let telemetry = doc.spec.telemetry.as_ref().expect("telemetry");
    assert_eq!(telemetry.channel, TelemetryChannel::VsockCbor);
    assert!(telemetry
        .events
        .iter()
        .any(|e| e.starts_with("net.connect")));
    assert_eq!(telemetry.agent_version, "1.0.0");
    let limits = telemetry.rate_limits.as_ref().expect("rateLimits");
    assert_eq!(limits.events_per_second, Some(64));
    let split = telemetry
        .host_vs_guest_fields
        .as_ref()
        .expect("hostVsGuestFields");
    assert!(split.host_fields.iter().any(|f| f == "runId"));
    assert!(split.guest_fields.iter().any(|f| f == "pid"));

    // round-trip
    let again = serde_json::to_string(&doc).expect("serialize");
    let doc2: ExecutionCellDocument = serde_json::from_str(&again).expect("re-parse");
    assert_eq!(doc2.spec.id, doc.spec.id);
    assert_eq!(
        doc2.spec.telemetry.as_ref().unwrap().agent_version,
        telemetry.agent_version
    );
}

#[test]
fn ci_runner_policy_example_parses_and_validates() {
    use cellos_core::{validate_policy_pack_document, PolicyPackDocument};

    let raw = include_str!("../../../contracts/examples/ci-runner-policy.valid.json");
    let doc: PolicyPackDocument = serde_json::from_str(raw).expect("parse");
    validate_policy_pack_document(&doc).expect("valid policy pack");

    assert_eq!(doc.api_version, "cellos.io/v1");
    assert_eq!(doc.kind, "PolicyPack");
    assert_eq!(doc.spec.id, "ci-runner-standard");
    assert_eq!(doc.spec.rules.max_lifetime_ttl_seconds, Some(3600));
    assert!(doc.spec.rules.require_egress_declared);
    assert!(doc.spec.rules.require_runtime_secret_delivery);
    assert!(doc.spec.rules.require_resource_limits);
    assert_eq!(doc.spec.rules.allowed_egress_hosts.len(), 4);

    // round-trip
    let again = serde_json::to_string(&doc).expect("serialize");
    let doc2: PolicyPackDocument = serde_json::from_str(&again).expect("re-parse");
    assert_eq!(doc2.spec.id, doc.spec.id);
    assert_eq!(
        doc2.spec.rules.max_lifetime_ttl_seconds,
        doc.spec.rules.max_lifetime_ttl_seconds
    );
}

// ---------------------------------------------------------------------------
// F1b — evidence_bundle + cell.observability.host.* per-phase tests.
//
// The Python `scripts/validate_contracts.py` runs full draft-2020-12 schema
// validation in CI; this Rust side asserts the structural invariants the
// builders must uphold even before the Python gate runs:
//
//   1. Valid evidence_bundle fixtures carry `specSignatureHash` and
//      `cellDestroyedEventRef` (D5).
//   2. The negative fixture `evidence-bundle.invalid.json` MUST be missing
//      `specSignatureHash` — that is the property the schema gate enforces
//      (per ADR-0006 §6 host-stamped fields are non-negotiable).
//   3. The Rust constructor never emits a bundle without `specSignatureHash`
//      or `cellDestroyedEventRef` — both are required arguments in the
//      type signature, so this is also a compile-time gate.
// ---------------------------------------------------------------------------

#[test]
fn evidence_bundle_valid_fixture_has_required_fields() {
    let raw = include_str!("../../../contracts/examples/evidence-bundle-data.valid.json");
    let v: serde_json::Value = serde_json::from_str(raw).expect("parse");
    let obj = v.as_object().expect("evidence bundle is object");
    assert!(
        obj.contains_key("specSignatureHash"),
        "valid fixture must carry specSignatureHash (ADR-0006 §6)"
    );
    assert!(
        obj.contains_key("cellDestroyedEventRef"),
        "valid fixture must carry cellDestroyedEventRef (D5 destruction gate)"
    );
    assert!(obj.contains_key("residueClass"));
}

#[test]
fn evidence_bundle_invalid_fixture_is_missing_spec_signature_hash() {
    // Per-phase test for F1b: the negative fixture MUST be missing
    // `specSignatureHash`. If a future edit accidentally adds it, the
    // negative fixture stops proving the schema gate works and this test
    // fails first — before the Python validator runs in CI.
    let raw = include_str!("../../../contracts/examples/evidence-bundle.invalid.json");
    let v: serde_json::Value = serde_json::from_str(raw).expect("parse");
    let obj = v.as_object().expect("evidence bundle is object");
    assert!(
        !obj.contains_key("specSignatureHash"),
        "negative fixture must NOT carry specSignatureHash — this is the \
         property the schema gate (and tests/residue.rs) rejects"
    );
}

#[test]
fn evidence_bundle_emitted_data_v1_emits_required_fields() {
    use cellos_core::{
        evidence_bundle_emitted_data_v1, EvidenceBundleRefs, ExecutionCellDocument, ResidueClass,
    };

    let raw = include_str!("../../../contracts/examples/execution-cell-minimal.valid.json");
    let doc: ExecutionCellDocument = serde_json::from_str(raw).expect("parse");
    let refs = EvidenceBundleRefs {
        started_event_ref: "urn:cellos:event:started",
        cell_destroyed_event_ref: "urn:cellos:event:destroyed",
        ..EvidenceBundleRefs::default()
    };
    let v = evidence_bundle_emitted_data_v1(
        &doc.spec,
        "cell-1",
        Some("run-1"),
        "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
        1_714_972_810_000,
        ResidueClass::None,
        &refs,
    )
    .expect("build evidence_bundle data");
    let obj = v.as_object().expect("data is object");
    assert!(obj.contains_key("specSignatureHash"));
    assert_eq!(
        obj.get("cellDestroyedEventRef").and_then(|x| x.as_str()),
        Some("urn:cellos:event:destroyed")
    );
    assert_eq!(
        obj.get("residueClass").and_then(|x| x.as_str()),
        Some("none")
    );
}