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"));
}