use std::sync::Arc;
use cellos_core::ports::{CellBackend, CellHandle};
use cellos_core::{CellosError, ExecutionCellDocument};
use cellos_host_cellos::ProprietaryCellBackend;
fn doc(id: &str) -> ExecutionCellDocument {
serde_json::from_value(serde_json::json!({
"apiVersion": "cellos.io/v1",
"kind": "ExecutionCell",
"spec": {
"id": id,
"authority": { "secretRefs": [] },
"lifetime": { "ttlSeconds": 60 }
}
}))
.expect("contract: spec must parse")
}
#[tokio::test]
async fn contract_create_then_destroy_lifecycle_ordering() {
let host = ProprietaryCellBackend::new();
let spec = doc("contract-lifecycle-1");
assert_eq!(host.tracked_cell_count().await, 0);
assert!(!host.has_tracked_state("contract-lifecycle-1").await);
let handle = host.create(&spec).await.expect("create must succeed");
assert_eq!(handle.cell_id, "contract-lifecycle-1");
assert_eq!(host.tracked_cell_count().await, 1);
assert!(host.has_tracked_state("contract-lifecycle-1").await);
assert!(
handle.nft_rules_applied.is_none(),
"host-cellos must not claim nft enforcement"
);
let report = host.destroy(&handle).await.expect("destroy must succeed");
assert_eq!(report.cell_id, "contract-lifecycle-1");
assert!(
report.destroyed,
"destroy of a live cell reports destroyed=true"
);
assert_eq!(
report.peers_tracked_after, 0,
"single-cell host: no peer residue after destroy"
);
assert_eq!(host.tracked_cell_count().await, 0);
assert!(!host.has_tracked_state("contract-lifecycle-1").await);
}
#[tokio::test]
async fn contract_wait_for_in_vm_exit_is_none() {
let host = ProprietaryCellBackend::new();
assert!(
host.wait_for_in_vm_exit("contract-no-vm").await.is_none(),
"host-cellos must not claim ownership of in-VM execution"
);
}
#[tokio::test]
async fn contract_repeat_destroy_is_typed_error_not_panic() {
let host = ProprietaryCellBackend::new();
let handle = host
.create(&doc("contract-idempotent-1"))
.await
.expect("create must succeed");
let r1 = host
.destroy(&handle)
.await
.expect("first destroy must succeed");
assert!(r1.destroyed);
assert_eq!(r1.peers_tracked_after, 0);
let err = host
.destroy(&handle)
.await
.expect_err("second destroy must error, not panic");
assert!(
matches!(err, CellosError::Host(_)),
"expected CellosError::Host on repeat-destroy, got: {err:?}"
);
assert_eq!(host.tracked_cell_count().await, 0);
assert!(!host.has_tracked_state("contract-idempotent-1").await);
}
#[tokio::test]
async fn contract_destroy_unknown_handle_is_typed_error() {
let host = ProprietaryCellBackend::new();
let err = host
.destroy(&CellHandle {
cell_id: "contract-never-created".into(),
cgroup_path: None,
nft_rules_applied: None,
kernel_digest_sha256: None,
rootfs_digest_sha256: None,
firecracker_digest_sha256: None,
})
.await
.expect_err("destroy of unknown handle must error on host-cellos");
match err {
CellosError::Host(ref msg) => assert!(
msg.contains("contract-never-created"),
"Host error must name the unknown cell id, got: {msg}"
),
other => panic!("expected CellosError::Host, got: {other:?}"),
}
assert_eq!(host.tracked_cell_count().await, 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn contract_concurrent_destroy_does_not_double_fail() {
let host = Arc::new(ProprietaryCellBackend::new());
let handle = host
.create(&doc("contract-concurrent-1"))
.await
.expect("create must succeed");
let h1 = handle.clone();
let h2 = handle.clone();
let host1 = Arc::clone(&host);
let host2 = Arc::clone(&host);
let t1 = tokio::spawn(async move { host1.destroy(&h1).await });
let t2 = tokio::spawn(async move { host2.destroy(&h2).await });
let r1 = t1.await.expect("task1 join must not panic");
let r2 = t2.await.expect("task2 join must not panic");
let oks = [&r1, &r2].iter().filter(|r| r.is_ok()).count();
let errs = [&r1, &r2].iter().filter(|r| r.is_err()).count();
assert_eq!(
oks, 1,
"exactly one racer must win destroy: r1={r1:?} r2={r2:?}"
);
assert_eq!(
errs, 1,
"exactly one racer must lose destroy: r1={r1:?} r2={r2:?}"
);
let losing = if r1.is_err() { r1 } else { r2 };
let losing_err = losing.expect_err("loser must be Err by construction");
assert!(
matches!(losing_err, CellosError::Host(_)),
"loser must surface CellosError::Host, got: {losing_err:?}"
);
assert_eq!(host.tracked_cell_count().await, 0);
assert!(!host.has_tracked_state("contract-concurrent-1").await);
}
#[tokio::test]
async fn contract_create_empty_id_returns_invalid_spec() {
let host = ProprietaryCellBackend::new();
let err = host
.create(&doc(""))
.await
.expect_err("empty id must be rejected");
assert!(
matches!(err, CellosError::InvalidSpec(_)),
"expected CellosError::InvalidSpec, got: {err:?}"
);
assert_eq!(host.tracked_cell_count().await, 0);
}
#[tokio::test]
async fn contract_duplicate_create_rejected_then_id_reusable_after_destroy() {
let host = ProprietaryCellBackend::new();
let h1 = host.create(&doc("contract-dup-1")).await.expect("create 1");
let err = host
.create(&doc("contract-dup-1"))
.await
.expect_err("duplicate live id must be rejected");
assert!(
matches!(err, CellosError::Host(_)),
"expected CellosError::Host on duplicate live id, got: {err:?}"
);
host.destroy(&h1).await.expect("destroy clears the slot");
let h2 = host
.create(&doc("contract-dup-1"))
.await
.expect("id is reusable after destroy");
assert_eq!(h2.cell_id, "contract-dup-1");
host.destroy(&h2).await.expect("teardown");
}