durable-actors 0.7.15

Standalone regional durable-actors control plane, host, and durability runtime
use super::*;

#[test]
fn secret_changes_update_host_configuration() {
    let mut deployment = spec("image-1");
    let initial = deployment.host_config_key();
    deployment.secret_refs = vec!["secrets-first".into()];
    let first = deployment.host_config_key();
    assert_ne!(initial, first);
    deployment.secret_refs = vec!["secrets-second".into()];
    assert_ne!(first, deployment.host_config_key());
    deployment.secret_refs.clear();
    assert_eq!(initial, deployment.host_config_key());
}

#[tokio::test]
async fn deployment_retains_secret_references() -> Result<()> {
    let registry = LocalAdminRegistry::default();
    let mut deployment = spec("im-1");
    deployment.secret_refs = vec!["project-secrets".into()];
    assert!(registry.register_test_deployment(&deployment).await?);
    assert_eq!(
        registry.launch_spec("default").await?,
        Some(deployment.clone())
    );
    deployment.secret_refs = vec!["replacement-secrets".into()];
    assert!(registry.register_test_deployment(&deployment).await?);
    Ok(())
}

#[tokio::test]
async fn registration_replaces_the_projects_deployment() -> Result<()> {
    let registry = LocalAdminRegistry::default();
    assert!(registry.register_test_deployment(&spec("im-1")).await?);
    assert!(!registry.register_test_deployment(&spec("im-1")).await?);
    let replacement = spec("im-2");

    assert!(registry.register_test_deployment(&replacement).await?);
    assert_eq!(registry.launch_spec("default").await?, Some(replacement));
    Ok(())
}

#[tokio::test]
async fn postgres_registration_replaces_the_single_deployment_atomically() -> Result<()> {
    crate::postgres::testing::with_postgres(async |fixture| {
        let database = PostgresDatabase::connect(&fixture.url).await?;
        let registry = PostgresAdminRegistry::from_database(database.clone());
        let mut deployment = spec("image-1");
        let initial_key = deployment.host_config_key();
        deployment.sandboxes.insert(
            "Counter".into(),
            serde_json::from_value(serde_json::json!({"cpu":2,"regions":["canada"]}))?,
        );
        assert_ne!(initial_key, deployment.host_config_key());
        deployment.secret_refs = vec!["project-secrets".into()];
        deployment.source = Some(DeploymentSource::Local(LocalSource {
            working_directory: "/project/a".into(),
            actor_entrypoint: Some("src/actors.ts".into()),
        }));

        assert!(registry.register_test_deployment(&deployment).await?);
        assert_eq!(
            registry.launch_spec("default").await?,
            Some(deployment.clone())
        );
        assert!(!registry.register_test_deployment(&deployment).await?);
        deployment.source = Some(DeploymentSource::Local(LocalSource {
            working_directory: "/project/b".into(),
            actor_entrypoint: Some("src/actors.ts".into()),
        }));
        assert!(registry.register_test_deployment(&deployment).await?);
        assert_eq!(registry.launch_spec("default").await?, Some(deployment));
        registry.remove_deployment("default").await?;
        assert_eq!(registry.launch_spec("default").await?, None);
        registry.remove_deployment("default").await?;
        Ok(())
    })
    .await
}

fn spec(image: &str) -> HostLaunchSpec {
    HostLaunchSpec {
        sandboxes: Default::default(),
        project_id: "default".into(),
        source: None,
        code_snapshot: None,
        image_ref: image.into(),
        working_directory: "/workspace".into(),
        actor_entrypoint: Some("src/actors.ts".into()),
        secret_refs: vec![],
    }
}

#[tokio::test]
async fn projects_keep_independent_deployments_and_host_identities() -> Result<()> {
    let registry = LocalAdminRegistry::default();
    let deployment = |project: &str| -> HostLaunchSpec {
        let mut value = serde_json::to_value(spec("same-image")).unwrap();
        value["projectId"] = project.into();
        serde_json::from_value(value).unwrap()
    };
    let first = deployment("team-a");
    let second = deployment("team-b");
    assert_ne!(first.host_config_key(), second.host_config_key());
    registry.register_test_deployment(&first).await?;
    registry.register_test_deployment(&second).await?;
    let mut deployments = registry.launch_specs().await?;
    deployments.sort_by(|a, b| a.project_id.cmp(&b.project_id));
    assert_eq!(deployments, vec![first.clone(), second.clone()]);
    assert_eq!(registry.launch_spec("team-a").await?, Some(first));
    assert_eq!(registry.launch_spec("team-b").await?, Some(second.clone()));
    registry.remove_deployment("team-a").await?;
    assert_eq!(registry.launch_spec("team-a").await?, None);
    assert_eq!(registry.launch_spec("team-b").await?, Some(second));
    Ok(())
}

#[tokio::test]
async fn postgres_projects_keep_deployments_contracts_and_deletions_separate() -> Result<()> {
    crate::postgres::testing::with_postgres(async |fixture| {
        let registry =
            PostgresAdminRegistry::from_database(PostgresDatabase::connect(&fixture.url).await?);
        let contract = PublicActorContract::new(serde_json::from_str(include_str!(
            "../../../sdk/tests/fixtures/public-contract.json"
        ))?)?;
        let mut first = spec("first-image");
        first.project_id = "team-a".into();
        let mut second = spec("second-image");
        second.project_id = "team-b".into();
        registry
            .register_deployment(&first, Some(&contract))
            .await?;
        registry
            .register_deployment(&second, Some(&contract))
            .await?;
        let mut deployments = registry.launch_specs().await?;
        deployments.sort_by(|a, b| a.project_id.cmp(&b.project_id));
        assert_eq!(deployments, vec![first.clone(), second.clone()]);
        assert_eq!(registry.launch_spec("team-a").await?, Some(first.clone()));
        assert_eq!(registry.launch_spec("team-b").await?, Some(second.clone()));

        first.secret_refs = vec!["team-a-secret".into()];
        registry
            .register_deployment(&first, Some(&contract))
            .await?;
        assert_eq!(registry.launch_spec("team-b").await?, Some(second.clone()));
        assert!(registry.deployment_contract("team-b").await?.is_some());
        registry.remove_deployment("team-a").await?;
        assert!(registry.deployment_contract("team-a").await?.is_none());
        assert!(registry.deployment_contract("team-b").await?.is_some());
        assert_eq!(registry.launch_spec("team-b").await?, Some(second));
        Ok(())
    })
    .await
}

#[test]
fn compiled_deployment_uses_an_immutable_gcs_manifest() -> Result<()> {
    let mut deployment = spec("registry/runtime@sha256:test");
    deployment.working_directory = "/customer".into();
    deployment.actor_entrypoint = Some("actors.mjs".into());
    deployment.code_snapshot = Some(crate::sandbox::testing::code_artifact(1));
    deployment.validate()?;
    deployment.actor_entrypoint = Some("different.mjs".into());
    assert!(deployment.validate().is_err());
    Ok(())
}

#[tokio::test]
async fn deployment_updates_serialize_across_instances_and_recover_when_the_lock_session_dies()
-> Result<()> {
    crate::postgres::testing::with_postgres(async |fixture| {
        let first = PostgresAdminRegistry::from_database(PostgresDatabase::connect(&fixture.url).await?);
        let second = PostgresAdminRegistry::from_database(PostgresDatabase::connect(&fixture.url).await?);
        let project = format!("deploy-{}", uuid::Uuid::new_v4().simple());
        let mut initial = spec("first");
        initial.project_id = project.clone();
        let mut update = first.lock_deployment(&project).await?;
        let waiting = second.lock_deployment(&project);
        tokio::pin!(waiting);
        assert!(tokio::time::timeout(std::time::Duration::from_millis(50), &mut waiting).await.is_err());
        let independent = second.lock_deployment("another-project").await?;
        drop(independent);
        update.register(&initial, None).await?;
        drop(update);
        let mut next = tokio::time::timeout(std::time::Duration::from_secs(2), waiting).await??;
        let client = fixture.pool.get().await?;
        let pid: i32 = client.query_one("SELECT pid FROM pg_locks WHERE locktype='advisory' AND granted AND classid=hashtext('durable-actors-deployment')::oid AND objid=hashtext($1)::oid", &[&project]).await?.get(0);
        client.query_one("SELECT pg_terminate_backend($1)", &[&pid]).await?;
        let mut replacement = initial.clone();
        replacement.image_ref = "replacement".into();
        assert!(next.register(&replacement, None).await.is_err());
        drop(next);
        assert_eq!(first.launch_spec(&project).await?, Some(initial));
        let mut recovered = tokio::time::timeout(std::time::Duration::from_secs(2), first.lock_deployment(&project)).await??;
        recovered.register(&replacement, None).await?;
        assert_eq!(second.launch_spec(&project).await?, Some(replacement));
        recovered.remove().await?;
        assert!(second.launch_spec(&project).await?.is_none());
        Ok(())
    }).await
}