agent-workspace-contract 0.5.2

Transport-neutral contracts for Agent Infra workspace APIs
Documentation
use agent_workspace_contract::*;
use serde_json::json;

#[test]
fn create_space_request_requires_computer_id() {
    let err = serde_json::from_value::<CreateSpaceRequest>(json!({
        "persistence": "persistent"
    }))
    .expect_err("computerId is required");
    assert!(err.to_string().contains("computerId"));
}

#[test]
fn create_space_request_rejects_provider_field() {
    let err = serde_json::from_value::<CreateSpaceRequest>(json!({
        "computerId": "cmp-1",
        "provider": "local",
        "persistence": "persistent"
    }))
    .expect_err("provider field was removed");
    assert!(err.to_string().contains("unknown field"));
}

#[test]
fn space_record_includes_computer_id_and_owners() {
    let record = SpaceRecord {
        id: "spc-1".into(),
        computer_id: "cmp-1".into(),
        scope: ResourceScope {
            tenant_id: "t".into(),
            project_id: "p".into(),
        },
        persistence: SpacePersistence::Persistent,
        lifecycle: SpaceLifecycle::Ready,
        capabilities: ProviderCapabilities::default(),
        owners: vec![OwnerRef::runtime("rt-1")],
        source_snapshot_id: None,
        revision: 0,
        version: 1,
        created_at_ms: 0,
        updated_at_ms: 0,
    };
    let value = serde_json::to_value(&record).expect("serialize");
    assert_eq!(value["computerId"], "cmp-1");
    assert_eq!(value["owners"][0]["kind"], "runtime");
}

#[test]
fn normalize_rejects_empty_owners() {
    assert!(matches!(
        normalize_owners(vec![]),
        Err(OwnerValidationError::Empty)
    ));
}

#[test]
fn owners_subset_invariant() {
    let parent = vec![OwnerRef::user("u1"), OwnerRef::agent("agt-1")];
    let child = vec![OwnerRef::agent("agt-1")];
    assert!(owners_subset_of(&child, &parent));
    assert!(!owners_subset_of(&parent, &child));
}

#[test]
fn create_snapshot_request_accepts_optional_custom_build_but_not_a_freeze_source() {
    let parsed = serde_json::from_value::<CreateSnapshotResourceRequest>(json!({
        "provider": "e2b",
        "visibility": "public",
        "owners": [{"kind": "runtime", "id": "rti-1"}],
        "build": {
            "kind": "dockerfile",
            "dockerfile": "FROM python:3.13-slim"
        }
    }))
    .expect("catalog snapshot request");
    assert_eq!(parsed.provider.as_deref(), Some("e2b"));
    assert_eq!(parsed.visibility, SnapshotVisibility::Public);
    assert_eq!(parsed.owners, vec![OwnerRef::runtime("rti-1")]);
    assert_eq!(
        parsed.build,
        Some(SnapshotBuildSpec::Dockerfile {
            dockerfile: "FROM python:3.13-slim".into(),
        })
    );

    let err = serde_json::from_value::<CreateSnapshotResourceRequest>(json!({
        "source": {"kind": "computer", "computerId": "cmp-1"}
    }))
    .expect_err("freeze source is no longer a create field");
    assert!(err.to_string().contains("unknown field"));
}

#[test]
fn build_builtin_request_selects_provider_and_default_flag() {
    let parsed = serde_json::from_value::<BuildBuiltinSnapshotRequest>(json!({
        "provider": "moulin",
        "setDefault": true,
        "gitUrl": "https://git.example/group/env.git",
        "gitRef": "master"
    }))
    .expect("builtin build request");
    assert_eq!(parsed.provider, "moulin");
    assert!(parsed.set_default);
    assert_eq!(
        parsed.git_url.as_deref(),
        Some("https://git.example/group/env.git")
    );
    assert_eq!(parsed.git_ref.as_deref(), Some("master"));

    let without_git = serde_json::from_value::<BuildBuiltinSnapshotRequest>(json!({
        "provider": "moulin"
    }))
    .expect("moulin builtin build may omit git");
    assert!(without_git.git_url.is_none());
    assert!(without_git.dockerfile.is_none());

    let with_dockerfile = serde_json::from_value::<BuildBuiltinSnapshotRequest>(json!({
        "provider": "moulin",
        "dockerfile": "FROM python:3.12-slim\nRUN pip install httpx\n"
    }))
    .expect("moulin builtin build may submit dockerfile");
    assert_eq!(
        with_dockerfile.dockerfile.as_deref(),
        Some("FROM python:3.12-slim\nRUN pip install httpx\n")
    );

    let e2b_dockerfile = serde_json::from_value::<BuildBuiltinSnapshotRequest>(json!({
        "provider": "e2b",
        "dockerfile": "FROM e2bdev/base\nRUN pip install httpx\n"
    }))
    .expect("e2b builtin build may submit dockerfile");
    assert_eq!(
        e2b_dockerfile.dockerfile.as_deref(),
        Some("FROM e2bdev/base\nRUN pip install httpx\n")
    );
}

#[test]
fn computer_and_space_resources_expose_provider_name_not_provider_ref() {
    let record = ComputerRecord {
        id: "computer_1".into(),
        scope: ResourceScope {
            tenant_id: "t".into(),
            project_id: "p".into(),
        },
        provider_ref: ProviderResourceRef {
            provider: "moulin".into(),
            external_id: "ext-secret".into(),
        },
        lifecycle: ComputerLifecycle::Ready,
        capabilities: ProviderCapabilities::default(),
        owners: vec![OwnerRef::user("u1")],
        version: 1,
        created_at_ms: 1,
        updated_at_ms: 1,
    };
    let computer = ComputerResource::from(record);
    let computer_json = serde_json::to_value(&computer).expect("serialize computer");
    assert_eq!(computer_json["provider"], "moulin");
    assert!(computer_json.get("providerRef").is_none());
    assert!(computer_json.get("externalId").is_none());

    let space = SpaceResource::from_record(
        SpaceRecord {
            id: "space_1".into(),
            computer_id: "computer_1".into(),
            scope: ResourceScope {
                tenant_id: "t".into(),
                project_id: "p".into(),
            },
            persistence: SpacePersistence::Persistent,
            lifecycle: SpaceLifecycle::Ready,
            capabilities: ProviderCapabilities::default(),
            owners: vec![OwnerRef::user("u1")],
            source_snapshot_id: None,
            revision: 0,
            version: 1,
            created_at_ms: 1,
            updated_at_ms: 1,
        },
        "moulin",
    );
    let space_json = serde_json::to_value(&space).expect("serialize space");
    assert_eq!(space_json["provider"], "moulin");
    assert_eq!(space_json["computerId"], "computer_1");
    assert!(space_json.get("providerRef").is_none());
}

#[test]
fn create_sandbox_request_rejects_source_snapshot_id() {
    let err = serde_json::from_value::<CreateSandboxRequest>(json!({
        "provider": "e2b",
        "placement": {"provider_pool": "default"},
        "ttlMs": 60000,
        "sourceSnapshotId": "snap-1"
    }))
    .expect_err("sandbox create no longer accepts sourceSnapshotId");
    assert!(err.to_string().contains("unknown field"));
}

#[test]
fn snapshot_build_spec_git_uses_repo_root_dockerfile() {
    let spec = serde_json::from_value::<SnapshotBuildSpec>(json!({
        "kind": "git",
        "gitUrl": "https://git.example/group/env.git",
        "gitRef": "master"
    }))
    .expect("git build spec");
    assert_eq!(
        spec,
        SnapshotBuildSpec::Git {
            git_url: "https://git.example/group/env.git".into(),
            git_ref: "master".into(),
        }
    );
}

#[test]
fn install_sandbox_skill_request_defaults_provider_version_and_installs_under_skills() {
    let parsed = serde_json::from_value::<InstallSandboxSkillRequest>(json!({
        "group": "customer-service",
        "name": "customer-greeter"
    }))
    .expect("install request");
    assert_eq!(parsed.group, "customer-service");
    assert_eq!(parsed.name, "customer-greeter");
    assert_eq!(parsed.version, None);
    assert_eq!(parsed.provider, "local");
    let e2b = serde_json::from_value::<InstallSandboxSkillRequest>(json!({
        "group": "customer-service",
        "name": "customer-greeter",
        "provider": "e2b"
    }))
    .expect("e2b install request");
    assert_eq!(e2b.provider, "e2b");
    assert_eq!(SANDBOX_SKILL_INSTALL_ROOT, "skills");
    assert_eq!(SANDBOX_SKILL_DEFAULT_VERSION, "latest");
    assert_eq!(SANDBOX_SKILL_DEFAULT_PROVIDER, "local");
    assert_eq!(
        SANDBOX_SKILLS_PATH,
        "/internal/v1/workspace/sandboxes/{sandboxId}/skills"
    );
    assert_eq!(SKILLS_SEARCH_PATH, "/internal/v1/workspace/skills:search");
}

#[test]
fn install_space_skill_contract_targets_runtime_workspace_root() {
    let parsed = serde_json::from_value::<InstallSpaceSkillRequest>(json!({
        "group": "customer-service",
        "name": "customer-greeter",
        "provider": "moulin"
    }))
    .expect("Space install request");
    assert_eq!(parsed.provider, "moulin");
    assert_eq!(parsed.version, None);
    assert_eq!(SPACE_SKILL_INSTALL_ROOT, "skills");
    assert_eq!(
        SPACE_SKILLS_PATH,
        "/internal/v1/workspace/spaces/{spaceId}/skills"
    );
}

#[test]
fn install_space_skill_contract_round_trips_all_fields_and_rejects_unknown_fields() {
    let request = serde_json::from_value::<InstallSpaceSkillRequest>(json!({
        "group": "customer-service",
        "name": "customer-greeter",
        "version": "1.2.3",
        "provider": "moulin"
    }))
    .expect("full install request");
    assert_eq!(
        request,
        InstallSpaceSkillRequest {
            group: "customer-service".into(),
            name: "customer-greeter".into(),
            version: Some("1.2.3".into()),
            provider: "moulin".into(),
        }
    );
    let request_json = serde_json::to_value(&request).expect("serialize install request");
    assert_eq!(
        request_json,
        json!({
            "group": "customer-service",
            "name": "customer-greeter",
            "version": "1.2.3",
            "provider": "moulin"
        })
    );

    let resource = InstallSpaceSkillResource {
        space_id: "spc_1".into(),
        computer_id: "cmp_1".into(),
        provider: "moulin".into(),
        group: "customer-service".into(),
        name: "customer-greeter".into(),
        version: "1.2.3".into(),
        install_path: "/skills".into(),
        files: vec!["skills/customer-service/customer-greeter/SKILL.md".into()],
        revision: 7,
    };
    let resource_json = serde_json::to_value(&resource).expect("serialize install resource");
    assert_eq!(
        resource_json,
        json!({
            "spaceId": "spc_1",
            "computerId": "cmp_1",
            "provider": "moulin",
            "group": "customer-service",
            "name": "customer-greeter",
            "version": "1.2.3",
            "installPath": "/skills",
            "files": ["skills/customer-service/customer-greeter/SKILL.md"],
            "revision": 7
        })
    );
    assert_eq!(
        serde_json::from_value::<InstallSpaceSkillResource>(resource_json)
            .expect("resource round trip"),
        resource
    );

    let request_error = serde_json::from_value::<InstallSpaceSkillRequest>(json!({
        "group": "customer-service",
        "name": "customer-greeter",
        "sourceSnapshotId": "snap_1"
    }))
    .expect_err("unknown fields are rejected");
    assert!(request_error.to_string().contains("unknown field"));
    let resource_error = serde_json::from_value::<InstallSpaceSkillResource>(json!({
        "spaceId": "spc_1",
        "computerId": "cmp_1",
        "provider": "moulin",
        "group": "customer-service",
        "name": "customer-greeter",
        "version": "1.2.3",
        "installPath": "/skills",
        "files": [],
        "revision": 7,
        "providerRef": "moulin"
    }))
    .expect_err("internal provider fields are rejected");
    assert!(resource_error.to_string().contains("unknown field"));
}