Files
ironclaw/tests/e2e_trace_runtime_policy_serde.rs
Henry Park cae1a04f95 feat(sandbox): add explicit Docker and Railway user sandbox profiles (#7214)
* feat(sandbox): add Docker and Railway user sandbox profiles

* fix(sandbox): close local review gaps

* test(sandbox): cover production local profile wiring

* fix(ci): schedule sandbox paths and classify tests

* fix(sandbox): bound Railway user lifecycle state

* test(cli): cover sandbox profiles in profile list

* test(architecture): shrink composition support debt

* fix(sandbox): harden Railway lifecycle review gaps

* fix(sandbox): fail closed on Railway provider errors

* fix(sandbox): close remaining review gaps

* test(sandbox): remove Railway timing flake

* chore(composition): record sandbox assembly budget

* fix(sandbox): retain Railway parse causes

* fix(sandbox): address post-merge review findings

* fix(sandbox): close railway lifecycle review gaps

* fix(sandbox): retain failed railway cleanup state

* feat(sandbox): enable direct egress for sandbox profiles

* test(sandbox): keep public egress in live canary

* chore(sandbox): document bounded slice safety

* test(architecture): ratchet user sandbox contract growth

* test(sandbox): strengthen profile regression coverage

* test(sandbox): exercise harness validation at boundary

* test(sandbox): place setup check with harness contracts

* fix(sandbox): preserve nonzero Docker exit results

* fix(host-runtime): resolve hosted tenant workspace mounts (#7214)

* fix(sandbox): address review feedback on bounded startup (#7214)

* fix(ci): derive sandbox Docker paths from crate inventory (#7214)

* fix(sandbox): finalize Railway lifecycle and profile parity

* test(sandbox): update hosted process failure contract

* fix(sandbox): address CodeRabbit review feedback (#7214)
2026-08-08 00:18:56 +00:00

323 lines
10 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Wire-stable serde coverage for every runtime-policy enum + `EffectiveRuntimePolicy`.
//!
//! PR #3243 shipped serde derives on all eight backend enums (with
//! `rename_all = "snake_case"`) plus `EffectiveRuntimePolicy`, but only
//! round-trip-tested `EffectiveRuntimePolicy`/`OrgPolicyConstraints`/
//! `ResolveRequest` aggregate-style. Per-variant exhaustiveness is missing —
//! a regression that flipped a single rename or dropped a variant from
//! deserialize would not be caught by the existing tests.
//!
//! Each enum here is tested with an explicit array literal of every
//! variant, so adding a `#[non_exhaustive]` variant in the future fails
//! compilation of the test binary (forcing an explicit decision).
use ironclaw_host_api::runtime_policy::{
ApprovalPolicy, AuditMode, DeploymentMode, EffectiveRuntimePolicy, FilesystemBackendKind,
NetworkMode, ProcessBackendKind, RuntimeProfile, SecretMode,
};
use ironclaw_runtime_policy::{OrgPolicyConstraints, ResolveRequest, resolve};
/// JSON round-trip + serialized form matches `as_str()` (snake_case wire name).
fn assert_round_trip<T>(value: T, expected_wire: &str)
where
T: serde::Serialize + for<'de> serde::Deserialize<'de> + std::fmt::Debug + PartialEq + Clone,
{
let json = serde_json::to_string(&value).expect("serialize");
assert_eq!(
json,
format!("\"{expected_wire}\""),
"serialized form must match snake_case wire name for {value:?}",
);
let decoded: T = serde_json::from_str(&json).expect("deserialize");
assert_eq!(decoded, value);
}
#[test]
fn deployment_mode_round_trips_serde_for_every_variant() {
let variants = [
DeploymentMode::LocalSingleUser,
DeploymentMode::HostedMultiTenant,
DeploymentMode::EnterpriseDedicated,
];
assert_eq!(
variants.len(),
3,
"DeploymentMode variant count drift — update this test when adding variants",
);
for v in variants {
assert_round_trip(v, v.as_str());
// DeploymentMode also implements `FromStr` — round-trip via the
// wire name to lock in display/parse symmetry.
let parsed: DeploymentMode = v.as_str().parse().expect("FromStr");
assert_eq!(parsed, v);
}
}
#[test]
fn runtime_profile_round_trips_serde_for_every_variant() {
let variants = [
RuntimeProfile::SecureDefault,
RuntimeProfile::LocalSafe,
RuntimeProfile::LocalHost,
RuntimeProfile::LocalYolo,
RuntimeProfile::HostedSafe,
RuntimeProfile::HostedDev,
RuntimeProfile::HostedYoloTenantScoped,
RuntimeProfile::EnterpriseSafe,
RuntimeProfile::EnterpriseDev,
RuntimeProfile::EnterpriseYoloDedicated,
RuntimeProfile::Sandboxed,
RuntimeProfile::Experiment,
];
assert_eq!(
variants.len(),
12,
"RuntimeProfile variant count drift — update this test when adding variants",
);
for v in variants {
assert_round_trip(v, v.as_str());
let parsed: RuntimeProfile = v.as_str().parse().expect("FromStr");
assert_eq!(parsed, v);
}
}
#[test]
fn filesystem_backend_kind_round_trips_serde_for_every_variant() {
let variants = [
FilesystemBackendKind::ScopedVirtual,
FilesystemBackendKind::HostWorkspace,
FilesystemBackendKind::TenantWorkspace,
FilesystemBackendKind::OrgDedicatedWorkspace,
];
assert_eq!(
variants.len(),
4,
"FilesystemBackendKind variant count drift — update this test when adding variants",
);
for v in variants {
assert_round_trip(v, v.as_str());
}
}
#[test]
fn process_backend_kind_round_trips_serde_for_every_variant() {
let variants = [
ProcessBackendKind::None,
ProcessBackendKind::Docker,
ProcessBackendKind::Srt,
ProcessBackendKind::SmolVm,
ProcessBackendKind::LocalHost,
ProcessBackendKind::UserSandbox,
ProcessBackendKind::OrgDedicatedRunner,
];
assert_eq!(
variants.len(),
7,
"ProcessBackendKind variant count drift — update this test when adding variants",
);
for v in variants {
assert_round_trip(v, v.as_str());
}
}
#[test]
fn network_mode_round_trips_serde_for_every_variant() {
let variants = [
NetworkMode::Deny,
NetworkMode::Brokered,
NetworkMode::Allowlist,
NetworkMode::DirectLogged,
NetworkMode::Direct,
];
assert_eq!(
variants.len(),
5,
"NetworkMode variant count drift — update this test when adding variants",
);
for v in variants {
assert_round_trip(v, v.as_str());
}
}
#[test]
fn secret_mode_round_trips_serde_for_every_variant() {
let variants = [
SecretMode::Deny,
SecretMode::BrokeredHandles,
SecretMode::TenantBroker,
SecretMode::OrgBroker,
SecretMode::ScrubbedEnv,
SecretMode::InheritedEnv,
];
assert_eq!(
variants.len(),
6,
"SecretMode variant count drift — update this test when adding variants",
);
for v in variants {
assert_round_trip(v, v.as_str());
}
}
#[test]
fn approval_policy_round_trips_serde_for_every_variant() {
let variants = [
ApprovalPolicy::AskAlways,
ApprovalPolicy::AskWrites,
ApprovalPolicy::AskDestructive,
ApprovalPolicy::OrgPolicy,
ApprovalPolicy::Minimal,
];
assert_eq!(
variants.len(),
5,
"ApprovalPolicy variant count drift — update this test when adding variants",
);
for v in variants {
assert_round_trip(v, v.as_str());
}
}
#[test]
fn audit_mode_round_trips_serde_for_every_variant() {
let variants = [
AuditMode::LocalMinimal,
AuditMode::Standard,
AuditMode::OrgPolicy,
];
assert_eq!(
variants.len(),
3,
"AuditMode variant count drift — update this test when adding variants",
);
for v in variants {
assert_round_trip(v, v.as_str());
}
}
#[test]
fn effective_runtime_policy_round_trips_serde_for_a_representative_matrix() {
// Build at least nine representative policies via the resolver covering
// the deployment × profile combinations highlighted in PR #3243's body.
// Resolver-produced policies are the only sanctioned source per the
// ironclaw_runtime_policy guardrail; round-tripping these locks every
// backend enum's serde simultaneously.
let policies = [
// Local family
resolve(ResolveRequest::new(
DeploymentMode::LocalSingleUser,
RuntimeProfile::SecureDefault,
))
.unwrap(),
resolve(ResolveRequest::new(
DeploymentMode::LocalSingleUser,
RuntimeProfile::LocalSafe,
))
.unwrap(),
resolve(ResolveRequest::new(
DeploymentMode::LocalSingleUser,
RuntimeProfile::LocalHost,
))
.unwrap(),
resolve(ResolveRequest {
deployment: DeploymentMode::LocalSingleUser,
requested_profile: RuntimeProfile::LocalYolo,
org_policy: OrgPolicyConstraints::default(),
yolo_disclosure_acknowledged: true,
})
.unwrap(),
// Hosted family
resolve(ResolveRequest::new(
DeploymentMode::HostedMultiTenant,
RuntimeProfile::HostedSafe,
))
.unwrap(),
resolve(ResolveRequest::new(
DeploymentMode::HostedMultiTenant,
RuntimeProfile::HostedDev,
))
.unwrap(),
resolve(ResolveRequest {
deployment: DeploymentMode::HostedMultiTenant,
requested_profile: RuntimeProfile::HostedYoloTenantScoped,
org_policy: OrgPolicyConstraints::default(),
yolo_disclosure_acknowledged: true,
})
.unwrap(),
// Enterprise family
resolve(ResolveRequest::new(
DeploymentMode::EnterpriseDedicated,
RuntimeProfile::EnterpriseSafe,
))
.unwrap(),
resolve(ResolveRequest::new(
DeploymentMode::EnterpriseDedicated,
RuntimeProfile::EnterpriseDev,
))
.unwrap(),
resolve(ResolveRequest {
deployment: DeploymentMode::EnterpriseDedicated,
requested_profile: RuntimeProfile::EnterpriseYoloDedicated,
org_policy: OrgPolicyConstraints {
admin_approves_dedicated_yolo: true,
..OrgPolicyConstraints::default()
},
yolo_disclosure_acknowledged: true,
})
.unwrap(),
// Deployment-agnostic profiles
resolve(ResolveRequest::new(
DeploymentMode::LocalSingleUser,
RuntimeProfile::Sandboxed,
))
.unwrap(),
resolve(ResolveRequest::new(
DeploymentMode::HostedMultiTenant,
RuntimeProfile::Experiment,
))
.unwrap(),
];
for policy in policies {
let json = serde_json::to_string(&policy).expect("serialize policy");
let decoded: EffectiveRuntimePolicy =
serde_json::from_str(&json).expect("deserialize policy");
assert_eq!(
decoded, policy,
"policy must round-trip through serde unchanged",
);
}
}
#[test]
fn effective_runtime_policy_was_reduced_flag_round_trips_through_serde() {
// Build a narrowed policy: LocalYolo requested, LocalHost ceiling →
// resolver narrows to LocalHost. `was_reduced()` is `true`. Serialize,
// deserialize, and assert the flag survives the round-trip.
let narrowed = resolve(ResolveRequest {
deployment: DeploymentMode::LocalSingleUser,
requested_profile: RuntimeProfile::LocalYolo,
org_policy: OrgPolicyConstraints {
max_profile: Some(RuntimeProfile::LocalHost),
..OrgPolicyConstraints::default()
},
yolo_disclosure_acknowledged: true,
})
.expect("LocalYolo with LocalHost ceiling resolves");
assert!(
narrowed.was_reduced(),
"ceiling narrowing must set requested != resolved",
);
assert_eq!(narrowed.requested_profile, RuntimeProfile::LocalYolo);
assert_eq!(narrowed.resolved_profile, RuntimeProfile::LocalHost);
let json = serde_json::to_string(&narrowed).expect("serialize narrowed");
let decoded: EffectiveRuntimePolicy =
serde_json::from_str(&json).expect("deserialize narrowed");
assert_eq!(decoded, narrowed);
assert!(
decoded.was_reduced(),
"was_reduced() must return true after serde round-trip on narrowed policy",
);
}