clusterflux-public/crates/clusterflux-coordinator/src/service/tests.rs
Clusterflux Release a9f6f8b7a9 Publish release-5edbadb22804 filtered source
Private source: 5edbadb2280419cf2cb42b5237835304ad7040e0

Public tree identity: sha256:e0d4d5e1c8693959e69448684a71582edfc13e9b18d7f1e733034fea5ce62cda
2026-07-29 18:13:15 +02:00

9114 lines
310 KiB
Rust

use std::collections::{BTreeMap, BTreeSet};
use std::io::{BufRead, BufReader, Write};
use std::net::TcpStream;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
use clusterflux_core::{
admin_request_proof, agent_ed25519_public_key_from_private_key, coordinator_wire_request,
derive_ed25519_private_key_from_seed, node_ed25519_public_key_from_private_key,
sign_agent_workflow_request, sign_node_request, signed_request_payload_digest,
AgentSignedRequest, AgentWorkflowScope, ArtifactFlush, ArtifactHandle, ArtifactId, Capability,
DataPlaneObject, DataPlaneScope, Digest, EnvironmentBackend, EnvironmentRequirements,
LimitKind, NodeCapabilities, NodeEndpoint, NodeSignedRequest, Os, ResourceLimits,
SourceProviderKind, TaskBoundaryValue, TaskDefinitionId, TaskDispatch, TaskInstanceId,
TaskJoinState, TaskSpec, VfsPath, WasmExportAbi, WasmTaskResult,
};
use serde_json::json;
use crate::FallibleDurableStore;
use super::keys::{process_control_key, task_control_key};
use super::*;
fn test_admin_request(
token: &str,
operation: &str,
tenant: &str,
actor_user: &str,
target_tenant: &str,
nonce: &str,
) -> (Digest, String, u64) {
let issued_at_epoch_seconds = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or_default();
(
admin_request_proof(
token,
operation,
tenant,
actor_user,
target_tenant,
nonce,
issued_at_epoch_seconds,
),
nonce.to_owned(),
issued_at_epoch_seconds,
)
}
fn enroll_test_node(
service: &mut CoordinatorService,
tenant: &str,
project: &str,
node: &str,
public_key: &str,
) {
let response = service
.handle_request(CoordinatorRequest::CreateNodeEnrollmentGrant {
tenant: tenant.to_owned(),
project: project.to_owned(),
actor_user: "test-user".to_owned(),
ttl_seconds: 900,
})
.unwrap();
let CoordinatorResponse::NodeEnrollmentGrantCreated { grant, .. } = response else {
panic!("expected node enrollment grant");
};
service
.handle_request(CoordinatorRequest::ExchangeNodeEnrollmentGrant {
tenant: tenant.to_owned(),
project: project.to_owned(),
node: node.to_owned(),
public_key: public_key.to_owned(),
enrollment_grant: grant,
})
.unwrap();
}
#[test]
fn runtime_service_uses_memory_only_when_database_url_is_absent() {
let service = CoordinatorService::new_with_database_url(1, None).unwrap();
assert_eq!(service.durable_store_kind(), "in_memory");
let error = CoordinatorService::new_with_database_url(1, Some("not-a-postgres-url"))
.err()
.expect("an invalid configured DATABASE_URL must fail closed");
assert!(error
.to_string()
.contains("durable coordinator state failed"));
}
#[test]
fn postgres_persists_duplicate_scoped_node_ids_and_credentials_across_restart() {
let Ok(database_url) = std::env::var("CLUSTERFLUX_TEST_POSTGRES_SERVICE") else {
return;
};
let mut clean_store = crate::PostgresDurableStore::connect(&database_url).unwrap();
clean_store
.save_state(&crate::DurableState::default())
.unwrap();
let session_secret = "postgres-runtime-service-session";
let mut first = CoordinatorService::new_with_database_url(41, Some(&database_url)).unwrap();
assert_eq!(first.durable_store_kind(), "postgres");
first
.issue_cli_session(
TenantId::from("tenant-pg"),
ProjectId::from("project-pg"),
UserId::from("user-pg"),
session_secret,
None,
)
.unwrap();
enroll_test_node(
&mut first,
"tenant-pg",
"project-pg",
"node-pg",
&test_node_public_key("node-pg"),
);
let duplicate_private_key = test_node_private_key("node-pg-other-scope");
enroll_test_node(
&mut first,
"tenant-pg-other",
"project-pg-other",
"node-pg",
&node_ed25519_public_key_from_private_key(&duplicate_private_key).unwrap(),
);
first
.handle_request(CoordinatorRequest::Authenticated {
session_secret: session_secret.to_owned(),
request: AuthenticatedCoordinatorRequest::StartProcess {
launch_attempt: None,
process: "process-ephemeral".to_owned(),
restart: false,
},
})
.unwrap();
drop(first);
let mut restarted = CoordinatorService::new_with_database_url(42, Some(&database_url)).unwrap();
assert_eq!(restarted.durable_store_kind(), "postgres");
let auth = restarted
.handle_request(CoordinatorRequest::Authenticated {
session_secret: session_secret.to_owned(),
request: AuthenticatedCoordinatorRequest::AuthStatus,
})
.unwrap();
assert!(matches!(
auth,
CoordinatorResponse::AuthStatus {
authenticated: true,
..
}
));
let heartbeat = restarted
.handle_request(CoordinatorRequest::NodeHeartbeat {
tenant: "tenant-pg".to_owned(),
project: "project-pg".to_owned(),
node: "node-pg".to_owned(),
node_signature: Some(signed_node_heartbeat_in_scope(
"tenant-pg",
"project-pg",
"node-pg",
"postgres-restart-heartbeat",
)),
})
.unwrap();
assert!(matches!(
heartbeat,
CoordinatorResponse::NodeHeartbeat { epoch: 42, .. }
));
let duplicate_heartbeat = restarted
.handle_request(CoordinatorRequest::NodeHeartbeat {
tenant: "tenant-pg-other".to_owned(),
project: "project-pg-other".to_owned(),
node: "node-pg".to_owned(),
node_signature: Some(signed_node_heartbeat_in_scope_with_private_key(
"tenant-pg-other",
"project-pg-other",
"node-pg",
&duplicate_private_key,
"postgres-restart-heartbeat",
)),
})
.unwrap();
assert!(matches!(
duplicate_heartbeat,
CoordinatorResponse::NodeHeartbeat { epoch: 42, .. }
));
let CoordinatorResponse::ProcessStatuses { processes, .. } = restarted
.handle_request(CoordinatorRequest::Authenticated {
session_secret: session_secret.to_owned(),
request: AuthenticatedCoordinatorRequest::ListProcesses,
})
.unwrap()
else {
panic!("expected process list");
};
assert!(processes.is_empty());
restarted
.handle_request(CoordinatorRequest::RevokeNodeCredential {
tenant: "tenant-pg".to_owned(),
project: "project-pg".to_owned(),
actor_user: "user-pg".to_owned(),
node: "node-pg".to_owned(),
})
.unwrap();
drop(restarted);
let mut after_revocation =
CoordinatorService::new_with_database_url(43, Some(&database_url)).unwrap();
assert!(after_revocation
.coordinator
.node_identity(
&TenantId::from("tenant-pg"),
&ProjectId::from("project-pg"),
&NodeId::from("node-pg"),
)
.is_none());
assert!(after_revocation
.coordinator
.node_identity(
&TenantId::from("tenant-pg-other"),
&ProjectId::from("project-pg-other"),
&NodeId::from("node-pg"),
)
.is_some());
after_revocation
.handle_request(CoordinatorRequest::NodeHeartbeat {
tenant: "tenant-pg-other".to_owned(),
project: "project-pg-other".to_owned(),
node: "node-pg".to_owned(),
node_signature: Some(signed_node_heartbeat_in_scope_with_private_key(
"tenant-pg-other",
"project-pg-other",
"node-pg",
&duplicate_private_key,
"post-revocation-restart",
)),
})
.expect("the duplicate node in the other scope must remain authenticated");
}
fn linux_capabilities() -> NodeCapabilities {
NodeCapabilities {
os: Os::Linux,
arch: "x86_64".to_owned(),
capabilities: BTreeSet::from([
Capability::Command,
Capability::Containers,
Capability::RootlessPodman,
Capability::VfsArtifacts,
]),
environment_backends: BTreeSet::from([EnvironmentBackend::Container]),
source_providers: BTreeSet::from(["filesystem".to_owned()]),
}
}
fn windows_capabilities() -> NodeCapabilities {
NodeCapabilities {
os: Os::Windows,
arch: "x86_64".to_owned(),
capabilities: BTreeSet::from([
Capability::Command,
Capability::WindowsCommandDev,
Capability::VfsArtifacts,
]),
environment_backends: BTreeSet::from([EnvironmentBackend::WindowsCommandDev]),
source_providers: BTreeSet::from(["filesystem".to_owned()]),
}
}
fn endpoint(name: &str) -> NodeEndpoint {
NodeEndpoint {
node: NodeId::from(name),
advertised_addr: format!("{name}.mesh.invalid:4433"),
public_key_fingerprint: Digest::sha256(format!("{name}-public-key")),
}
}
fn data_plane_scope(project: &str) -> DataPlaneScope {
DataPlaneScope {
tenant: TenantId::from("tenant"),
project: ProjectId::from(project),
process: ProcessId::from("process"),
object: DataPlaneObject::Artifact(ArtifactId::from("artifact")),
authorization_subject: "node-a-to-node-b".to_owned(),
}
}
fn assert_agent_workflow_actor(actor: &WorkflowActor, fingerprint: &Digest) {
assert_eq!(actor.kind, "agent");
assert_eq!(actor.user, Some(UserId::from("user")));
assert_eq!(actor.agent, Some(AgentId::from("agent-ci")));
assert_eq!(actor.credential_kind, CredentialKind::PublicKey);
assert_eq!(actor.public_key_fingerprint.as_ref(), Some(fingerprint));
assert!(actor.authenticated_without_browser);
assert!(actor.scopes.iter().any(|scope| scope == "project:run"));
}
const TEST_AGENT_PRIVATE_KEY: &str = "ed25519:BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=";
static TEST_NODE_REQUEST_NONCE: AtomicU64 = AtomicU64::new(1);
const TEST_WASM_MODULE: &[u8] = b"clusterflux-test-wasm-module";
fn test_wasm_module_base64() -> String {
BASE64_STANDARD.encode(TEST_WASM_MODULE)
}
fn append_test_custom_section(module: &mut Vec<u8>, name: &str, data: &[u8]) {
fn leb(mut value: usize, output: &mut Vec<u8>) {
loop {
let mut byte = (value & 0x7f) as u8;
value >>= 7;
if value != 0 {
byte |= 0x80;
}
output.push(byte);
if value == 0 {
break;
}
}
}
let mut payload = Vec::new();
leb(name.len(), &mut payload);
payload.extend_from_slice(name.as_bytes());
payload.extend_from_slice(data);
module.push(0);
leb(payload.len(), module);
module.extend_from_slice(&payload);
}
fn test_edited_task_bundle(compatibility: &Digest, edit_marker: &str) -> (String, Digest) {
let descriptor = serde_json::to_vec(&serde_json::json!({
"kind": "task",
"name": "compile",
"function": "compile",
"export": "compile_export",
"stable_id": Digest::sha256("compile-stable"),
"argument_schema": "u32",
"result_schema": "u32",
"required_capabilities": [],
"restart_compatibility_hash": compatibility,
"abi_version": clusterflux_core::WASM_TASK_ABI_VERSION,
"probe_symbol": "clusterflux.probe.compile",
}))
.unwrap();
let mut module = b"\0asm\x01\0\0\0".to_vec();
append_test_custom_section(&mut module, "clusterflux.tasks", &descriptor);
append_test_custom_section(&mut module, "clusterflux.edit", edit_marker.as_bytes());
let digest = Digest::sha256(&module);
(BASE64_STANDARD.encode(module), digest)
}
fn test_task_spec(
tenant: &str,
project: &str,
process: &str,
task: &str,
epoch: u64,
required_capabilities: impl IntoIterator<Item = Capability>,
) -> TaskSpec {
test_task_spec_instance(
tenant,
project,
process,
task,
task,
epoch,
required_capabilities,
)
}
fn test_task_spec_instance(
tenant: &str,
project: &str,
process: &str,
task_definition: &str,
task_instance: &str,
epoch: u64,
required_capabilities: impl IntoIterator<Item = Capability>,
) -> TaskSpec {
TaskSpec {
tenant: TenantId::from(tenant),
project: ProjectId::from(project),
process: ProcessId::from(process),
task_definition: clusterflux_core::TaskDefinitionId::from(task_definition),
task_instance: clusterflux_core::TaskInstanceId::from(task_instance),
dispatch: TaskDispatch::CoordinatorNodeWasm {
export: Some(task_definition.to_owned()),
abi: WasmExportAbi::TaskV1,
},
environment_id: None,
environment: None,
environment_digest: None,
required_capabilities: required_capabilities.into_iter().collect(),
dependency_cache: None,
source_snapshot: None,
required_artifacts: Vec::new(),
args: Vec::new(),
vfs_epoch: epoch,
failure_policy: Default::default(),
bundle_digest: Some(Digest::sha256(TEST_WASM_MODULE)),
}
}
trait AuthorizedTestTaskLaunch {
fn handle_authorized_test_task_launch(
&mut self,
request: CoordinatorRequest,
) -> Result<CoordinatorResponse, CoordinatorServiceError>;
}
impl AuthorizedTestTaskLaunch for CoordinatorService {
fn handle_authorized_test_task_launch(
&mut self,
request: CoordinatorRequest,
) -> Result<CoordinatorResponse, CoordinatorServiceError> {
let CoordinatorRequest::LaunchTask {
task_spec,
wait_for_node,
artifact_path,
wasm_module_base64,
..
} = request
else {
panic!("authorized test task launch requires LaunchTask");
};
let tenant = task_spec.tenant.clone();
let project = task_spec.project.clone();
self.handle_launch_task_with_actor(
tenant,
project,
WorkflowActor {
kind: "task".to_owned(),
user: None,
agent: None,
credential_kind: CredentialKind::TaskCredential,
public_key_fingerprint: None,
authenticated_without_browser: true,
scopes: vec!["process:spawn-child".to_owned()],
},
task_spec,
wait_for_node,
artifact_path,
wasm_module_base64,
)
}
}
fn register_test_task_assignment(
service: &mut CoordinatorService,
tenant: &str,
project: &str,
process: &str,
node: &str,
task_definition: &str,
task_instance: &str,
epoch: u64,
) {
let task_spec = test_task_spec_instance(
tenant,
project,
process,
task_definition,
task_instance,
epoch,
[],
);
let assignment = TaskAssignment {
tenant: TenantId::from(tenant),
project: ProjectId::from(project),
process: ProcessId::from(process),
task: TaskInstanceId::from(task_instance),
node: NodeId::from(node),
epoch,
artifact_path: format!("/vfs/artifacts/{task_instance}.bin"),
task_spec,
wasm_module_base64: test_wasm_module_base64(),
};
service
.capture_task_restart_checkpoint(&assignment)
.unwrap();
service.active_tasks.insert(super::keys::task_control_key(
&assignment.tenant,
&assignment.project,
&assignment.process,
&assignment.node,
&assignment.task,
));
}
fn service_with_completed_main_and_final_child(
failure_policy: clusterflux_core::TaskFailurePolicy,
) -> CoordinatorService {
let mut service = CoordinatorService::new(83);
let tenant = TenantId::from("tenant");
let project = ProjectId::from("project");
let process = ProcessId::from("terminal-matrix");
let node = NodeId::from("worker");
let task = TaskInstanceId::from("final-child");
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: tenant.to_string(),
project: project.to_string(),
node: node.to_string(),
public_key: test_node_public_key(node.as_str()),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: tenant.to_string(),
project: project.to_string(),
node: node.to_string(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: true,
online: true,
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: tenant.to_string(),
project: project.to_string(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: process.to_string(),
restart: false,
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReconnectNode {
tenant: tenant.to_string(),
project: project.to_string(),
node: node.to_string(),
process: process.to_string(),
epoch: 83,
})
.unwrap();
let mut task_spec = test_task_spec_instance(
tenant.as_str(),
project.as_str(),
process.as_str(),
"child-definition",
task.as_str(),
83,
[],
);
task_spec.failure_policy = failure_policy;
let assignment = TaskAssignment {
tenant: tenant.clone(),
project: project.clone(),
process: process.clone(),
task: task.clone(),
node: node.clone(),
epoch: 83,
artifact_path: "/vfs/artifacts/final-child.bin".to_owned(),
task_spec: task_spec.clone(),
wasm_module_base64: test_wasm_module_base64(),
};
service
.capture_task_restart_checkpoint(&assignment)
.unwrap();
service
.begin_task_attempt(
&task_spec,
Some(node.clone()),
Some(&assignment.artifact_path),
false,
)
.unwrap();
service
.active_tasks
.insert(task_control_key(&tenant, &project, &process, &node, &task));
service
.task_assignments
.entry((tenant.clone(), project.clone(), node.clone()))
.or_default()
.push_back(assignment);
service
.debug_epochs
.insert(process_control_key(&tenant, &project, &process), 11);
service.debug_breakpoints.insert(
process_control_key(&tenant, &project, &process),
super::debug::DebugBreakpointPlan {
actor: UserId::from("user"),
revision: 1,
probe_symbols: BTreeSet::from(["child-probe".to_owned()]),
hit_epoch: None,
hit_task: None,
hit_probe_symbol: None,
},
);
service.debug_commands.insert(
task_control_key(&tenant, &project, &process, &node, &task),
super::debug::DebugPendingCommand {
epoch: 11,
command: "continue".to_owned(),
},
);
let panel_key = super::keys::panel_stop_key(&tenant, &project, &process);
service.panel_snapshots.insert(
panel_key.clone(),
PanelState {
tenant: tenant.clone(),
project: project.clone(),
process: process.clone(),
widgets: BTreeMap::new(),
program_ui_events_enabled: false,
control_plane_actions: Vec::new(),
},
);
service.stopped_panels.insert(panel_key);
service.record_task_completion_event(TaskCompletionEvent {
tenant: tenant.clone(),
project: project.clone(),
process: process.clone(),
node: NodeId::from("coordinator-main"),
executor: TaskExecutor::CoordinatorMain,
task_definition: TaskDefinitionId::from("build"),
task: TaskInstanceId::from("main"),
attempt_id: None,
placement: None,
terminal_state: TaskTerminalState::Completed,
status_code: Some(0),
stdout_bytes: 0,
stderr_bytes: 0,
stdout_tail: String::new(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
artifact_digest: None,
artifact_size_bytes: None,
result: None,
});
service
.coordinator
.grant_project_debug(tenant, project, UserId::from("user"));
service
}
fn complete_terminal_matrix_child(
service: &mut CoordinatorService,
terminal_state: TaskTerminalState,
) {
service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "terminal-matrix".to_owned(),
node: "worker".to_owned(),
task: "final-child".to_owned(),
terminal_state: Some(terminal_state),
status_code: Some(1),
stdout_bytes: 0,
stderr_bytes: 4,
stdout_tail: String::new(),
stderr_tail: "boom".to_owned(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
artifact_digest: None,
artifact_size_bytes: None,
result: None,
})
.unwrap();
}
fn test_agent_public_key() -> String {
agent_ed25519_public_key_from_private_key(TEST_AGENT_PRIVATE_KEY).unwrap()
}
fn signed_agent_workflow_request(
request: &CoordinatorRequest,
request_kind: &str,
process: &str,
task: Option<&str>,
nonce: &str,
) -> AgentSignedRequest {
let task = task.map(TaskInstanceId::from);
let payload = serde_json::to_value(request).unwrap();
let payload_digest = signed_request_payload_digest(&payload);
sign_agent_workflow_request(
TEST_AGENT_PRIVATE_KEY,
AgentWorkflowScope {
tenant: &TenantId::from("tenant"),
project: &ProjectId::from("project"),
agent: &AgentId::from("agent-ci"),
request_kind,
process: &ProcessId::from(process),
task: task.as_ref(),
},
&payload_digest,
nonce.to_owned(),
unix_timestamp_seconds_for_tests(),
)
.unwrap()
}
fn with_signed_agent_workflow(
mut request: CoordinatorRequest,
request_kind: &str,
process: &str,
task: Option<&str>,
nonce: &str,
) -> CoordinatorRequest {
let signature = signed_agent_workflow_request(&request, request_kind, process, task, nonce);
match &mut request {
CoordinatorRequest::StartProcess {
launch_attempt: None,
agent_signature,
..
}
| CoordinatorRequest::LaunchTask {
agent_signature, ..
} => *agent_signature = Some(signature),
_ => panic!("agent signing helper only accepts agent workflow requests"),
}
request
}
fn test_node_private_key(node: &str) -> String {
derive_ed25519_private_key_from_seed(&format!("test-node-key:{node}"))
}
fn test_node_public_key(node: &str) -> String {
node_ed25519_public_key_from_private_key(&test_node_private_key(node)).unwrap()
}
fn signed_node_heartbeat(node: &str, nonce: &str) -> NodeSignedRequest {
signed_node_heartbeat_in_scope("tenant", "project", node, nonce)
}
fn signed_node_heartbeat_in_scope(
tenant: &str,
project: &str,
node: &str,
nonce: &str,
) -> NodeSignedRequest {
let payload = json!({
"type": "node_heartbeat",
"tenant": tenant,
"project": project,
"node": node
});
signed_node_request_with_private_key(
node,
&test_node_private_key(node),
"node_heartbeat",
&signed_request_payload_digest(&payload),
nonce,
)
}
fn signed_node_heartbeat_with_private_key(
node: &str,
private_key: &str,
nonce: &str,
) -> NodeSignedRequest {
signed_node_heartbeat_in_scope_with_private_key("tenant", "project", node, private_key, nonce)
}
fn signed_node_heartbeat_in_scope_with_private_key(
tenant: &str,
project: &str,
node: &str,
private_key: &str,
nonce: &str,
) -> NodeSignedRequest {
let payload = json!({
"type": "node_heartbeat",
"tenant": tenant,
"project": project,
"node": node
});
signed_node_request_with_private_key(
node,
private_key,
"node_heartbeat",
&signed_request_payload_digest(&payload),
nonce,
)
}
fn signed_node_request_with_private_key(
node: &str,
private_key: &str,
request_kind: &str,
payload_digest: &Digest,
nonce: &str,
) -> NodeSignedRequest {
sign_node_request(
private_key,
&NodeId::from(node),
request_kind,
payload_digest,
nonce.to_owned(),
unix_timestamp_seconds_for_tests(),
)
.unwrap()
}
fn signed_node_request_auto(request: CoordinatorRequest) -> CoordinatorRequest {
let node = match &request {
CoordinatorRequest::ReportNodeCapabilities { node, .. }
| CoordinatorRequest::PollTaskAssignment { node, .. }
| CoordinatorRequest::PollArtifactTransfer { node, .. }
| CoordinatorRequest::UploadArtifactTransferChunk { node, .. }
| CoordinatorRequest::FailArtifactTransfer { node, .. }
| CoordinatorRequest::LaunchChildTask { node, .. }
| CoordinatorRequest::JoinChildTask { node, .. }
| CoordinatorRequest::CompleteSourcePreparation { node, .. }
| CoordinatorRequest::ReconnectNode { node, .. }
| CoordinatorRequest::PollTaskControl { node, .. }
| CoordinatorRequest::PollDebugCommand { node, .. }
| CoordinatorRequest::ReportDebugState { node, .. }
| CoordinatorRequest::ReportDebugProbeHit { node, .. }
| CoordinatorRequest::ReportTaskLog { node, .. }
| CoordinatorRequest::ReportTaskLogChunk { node, .. }
| CoordinatorRequest::ReportVfsMetadata { node, .. }
| CoordinatorRequest::TaskCompleted { node, .. } => node.clone(),
CoordinatorRequest::RequestRendezvous { source, .. } => source.node.to_string(),
_ => panic!("test helper only signs node-originated requests"),
};
signed_node_request_auto_with_private_key(request, &test_node_private_key(&node))
}
fn signed_node_request_auto_with_private_key(
request: CoordinatorRequest,
private_key: &str,
) -> CoordinatorRequest {
let payload_digest = signed_request_payload_digest(&serde_json::to_value(&request).unwrap());
let (node, request_kind) = match &request {
CoordinatorRequest::ReportNodeCapabilities { node, .. } => {
(node.clone(), "report_node_capabilities")
}
CoordinatorRequest::PollTaskAssignment { node, .. } => {
(node.clone(), "poll_task_assignment")
}
CoordinatorRequest::PollArtifactTransfer { node, .. } => {
(node.clone(), "poll_artifact_transfer")
}
CoordinatorRequest::UploadArtifactTransferChunk { node, .. } => {
(node.clone(), "upload_artifact_transfer_chunk")
}
CoordinatorRequest::FailArtifactTransfer { node, .. } => {
(node.clone(), "fail_artifact_transfer")
}
CoordinatorRequest::LaunchChildTask { node, .. } => (node.clone(), "launch_child_task"),
CoordinatorRequest::JoinChildTask { node, .. } => (node.clone(), "join_child_task"),
CoordinatorRequest::CompleteSourcePreparation { node, .. } => {
(node.clone(), "complete_source_preparation")
}
CoordinatorRequest::RequestRendezvous { source, .. } => {
(source.node.to_string(), "request_rendezvous")
}
CoordinatorRequest::ReconnectNode { node, .. } => (node.clone(), "reconnect_node"),
CoordinatorRequest::PollTaskControl { node, .. } => (node.clone(), "poll_task_control"),
CoordinatorRequest::PollDebugCommand { node, .. } => (node.clone(), "poll_debug_command"),
CoordinatorRequest::ReportDebugState { node, .. } => (node.clone(), "report_debug_state"),
CoordinatorRequest::ReportDebugProbeHit { node, .. } => {
(node.clone(), "report_debug_probe_hit")
}
CoordinatorRequest::ReportTaskLog { node, .. } => (node.clone(), "report_task_log"),
CoordinatorRequest::ReportTaskLogChunk { node, .. } => {
(node.clone(), "report_task_log_chunk")
}
CoordinatorRequest::ReportVfsMetadata { node, .. } => (node.clone(), "report_vfs_metadata"),
CoordinatorRequest::TaskCompleted { node, .. } => (node.clone(), "task_completed"),
_ => panic!("test helper only signs node-originated requests"),
};
let nonce = TEST_NODE_REQUEST_NONCE
.fetch_add(1, Ordering::Relaxed)
.to_string();
CoordinatorRequest::SignedNode {
node: node.clone(),
node_signature: signed_node_request_with_private_key(
&node,
private_key,
request_kind,
&payload_digest,
&format!("node-request-{nonce}"),
),
request: Box::new(request),
}
}
trait SignedNodeRequestTestExt {
fn handle_signed_node_request_auto(
&mut self,
request: CoordinatorRequest,
) -> Result<CoordinatorResponse, CoordinatorServiceError>;
}
impl SignedNodeRequestTestExt for CoordinatorService {
fn handle_signed_node_request_auto(
&mut self,
request: CoordinatorRequest,
) -> Result<CoordinatorResponse, CoordinatorServiceError> {
self.handle_request(signed_node_request_auto(request))
}
}
fn upload_pending_artifact_transfer(
service: &mut CoordinatorService,
node: &str,
bytes: &[u8],
) -> ArtifactTransferAssignment {
let mut first = None;
loop {
let CoordinatorResponse::ArtifactTransferAssignment {
transfer: Some(transfer),
} = service
.handle_signed_node_request_auto(CoordinatorRequest::PollArtifactTransfer {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: node.to_owned(),
})
.unwrap()
else {
panic!("expected pending artifact reverse transfer");
};
assert_eq!(transfer.expected_digest, Digest::sha256(bytes));
assert_eq!(transfer.expected_size_bytes, bytes.len() as u64);
let start = transfer.offset as usize;
let end = start
.saturating_add(transfer.max_chunk_bytes as usize)
.min(bytes.len());
let content = &bytes[start..end];
let response = service
.handle_signed_node_request_auto(CoordinatorRequest::UploadArtifactTransferChunk {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: node.to_owned(),
transfer_id: transfer.transfer_id.clone(),
artifact: transfer.artifact.as_str().to_owned(),
offset: transfer.offset,
content_base64: BASE64_STANDARD.encode(content),
chunk_digest: Digest::sha256(content),
eof: end == bytes.len(),
})
.unwrap();
let complete = matches!(
response,
CoordinatorResponse::ArtifactTransferChunkAccepted { complete: true, .. }
);
first.get_or_insert_with(|| transfer.clone());
if complete {
return first.unwrap();
}
}
}
fn unix_timestamp_seconds_for_tests() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or(0)
}
fn write_coordinator_wire_request(
stream: &mut TcpStream,
request: &CoordinatorRequest,
request_id: &str,
) {
let payload = serde_json::to_value(request).unwrap();
let wire_request = coordinator_wire_request(request_id, payload);
serde_json::to_writer(&mut *stream, &wire_request).unwrap();
stream.write_all(b"\n").unwrap();
stream.flush().unwrap();
}
#[test]
fn service_creates_selects_and_lists_signed_in_user_projects() {
let mut service = CoordinatorService::new(7);
let CoordinatorResponse::ProjectCreated { project, actor } = service
.handle_request(CoordinatorRequest::CreateProject {
tenant: "tenant-a".to_owned(),
actor_user: "user-a".to_owned(),
project: "project-a".to_owned(),
name: "Demo".to_owned(),
})
.unwrap()
else {
panic!("expected project creation");
};
assert_eq!(actor, UserId::from("user-a"));
assert_eq!(project.tenant, TenantId::from("tenant-a"));
assert_eq!(project.id, ProjectId::from("project-a"));
assert_eq!(project.name, "Demo");
let CoordinatorResponse::ProjectSelected { project, actor } = service
.handle_request(CoordinatorRequest::SelectProject {
tenant: "tenant-a".to_owned(),
actor_user: "user-a".to_owned(),
project: "project-a".to_owned(),
})
.unwrap()
else {
panic!("expected project selection");
};
assert_eq!(actor, UserId::from("user-a"));
assert_eq!(project.id, ProjectId::from("project-a"));
service
.handle_request(CoordinatorRequest::CreateProject {
tenant: "tenant-b".to_owned(),
actor_user: "user-b".to_owned(),
project: "project-b".to_owned(),
name: "Other".to_owned(),
})
.unwrap();
let CoordinatorResponse::Projects { projects, actor } = service
.handle_request(CoordinatorRequest::ListProjects {
tenant: "tenant-a".to_owned(),
actor_user: "user-a".to_owned(),
})
.unwrap()
else {
panic!("expected project list");
};
assert_eq!(actor, UserId::from("user-a"));
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].id, ProjectId::from("project-a"));
let cross_tenant = service
.handle_request(CoordinatorRequest::SelectProject {
tenant: "tenant-a".to_owned(),
actor_user: "user-a".to_owned(),
project: "project-b".to_owned(),
})
.unwrap_err();
assert!(cross_tenant.to_string().contains("tenant scope"));
}
#[test]
fn authenticated_envelope_derives_user_scope_from_cli_session() {
let mut service = CoordinatorService::new(7);
service
.issue_cli_session(
TenantId::from("tenant-a"),
ProjectId::from("project-a"),
UserId::from("user-a"),
"cli-session-secret",
None,
)
.unwrap();
let CoordinatorResponse::AuthStatus {
tenant,
project,
actor,
authenticated,
..
} = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::AuthStatus,
})
.unwrap()
else {
panic!("expected authenticated auth status");
};
assert_eq!(tenant, TenantId::from("tenant-a"));
assert_eq!(project, ProjectId::from("project-a"));
assert_eq!(actor, UserId::from("user-a"));
assert!(authenticated);
let CoordinatorResponse::ProjectCreated { project, actor } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::CreateProject {
project: "project-b".to_owned(),
name: "From Session".to_owned(),
},
})
.unwrap()
else {
panic!("expected authenticated project creation");
};
assert_eq!(project.tenant, TenantId::from("tenant-a"));
assert_eq!(project.id, ProjectId::from("project-b"));
assert_eq!(actor, UserId::from("user-a"));
let CoordinatorResponse::Projects { projects, actor } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::ListProjects,
})
.unwrap()
else {
panic!("expected authenticated project list");
};
assert_eq!(actor, UserId::from("user-a"));
assert_eq!(projects.len(), 2);
assert!(projects
.iter()
.any(|project| project.id == ProjectId::from("project-a")));
assert!(projects
.iter()
.any(|project| project.id == ProjectId::from("project-b")));
let CoordinatorResponse::AgentPublicKey { record, actor } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::RegisterAgentPublicKey {
agent: "agent-session".to_owned(),
public_key: "agent-session-key-v1".to_owned(),
},
})
.unwrap()
else {
panic!("expected authenticated agent key registration");
};
assert_eq!(actor, UserId::from("user-a"));
assert_eq!(record.tenant, TenantId::from("tenant-a"));
assert_eq!(record.project, ProjectId::from("project-a"));
assert_eq!(record.user, UserId::from("user-a"));
assert_eq!(record.agent, AgentId::from("agent-session"));
let CoordinatorResponse::AgentPublicKeys { records, actor } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::ListAgentPublicKeys,
})
.unwrap()
else {
panic!("expected authenticated agent key list");
};
assert_eq!(actor, UserId::from("user-a"));
assert_eq!(records.len(), 1);
assert_eq!(records[0].project, ProjectId::from("project-a"));
let CoordinatorResponse::AgentPublicKey { record, actor } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::RevokeAgentPublicKey {
agent: "agent-session".to_owned(),
},
})
.unwrap()
else {
panic!("expected authenticated agent key revocation");
};
assert_eq!(actor, UserId::from("user-a"));
assert!(record.revoked);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant-a".to_owned(),
project: "project-a".to_owned(),
node: "session-node".to_owned(),
public_key: test_node_public_key("session-node"),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant-a".to_owned(),
project: "project-a".to_owned(),
node: "session-node".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: vec![],
dependency_cache_digests: vec![],
source_snapshots: vec![],
artifact_locations: vec![],
direct_connectivity: true,
online: true,
})
.unwrap();
let CoordinatorResponse::NodeDescriptors { descriptors, actor } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::ListNodeDescriptors,
})
.unwrap()
else {
panic!("expected authenticated node descriptor list");
};
assert_eq!(actor, UserId::from("user-a"));
assert_eq!(descriptors.len(), 1);
assert_eq!(descriptors[0].id, NodeId::from("session-node"));
let CoordinatorResponse::TaskPlacement { placement } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::ScheduleTask {
environment: None,
environment_digest: None,
required_capabilities: vec![Capability::Command],
dependency_cache: None,
source_snapshot: None,
required_artifacts: vec![],
prefer_node: Some("session-node".to_owned()),
},
})
.unwrap()
else {
panic!("expected authenticated task placement");
};
assert_eq!(placement.node, NodeId::from("session-node"));
let CoordinatorResponse::ProcessStarted {
launch_attempt: None,
process,
actor,
..
} = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::StartProcess {
launch_attempt: None,
process: "vp-session".to_owned(),
restart: false,
},
})
.unwrap()
else {
panic!("expected authenticated process start");
};
assert_eq!(process, ProcessId::from("vp-session"));
assert_eq!(actor.user, Some(UserId::from("user-a")));
let denied_external_task = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::LaunchTask {
task_spec: Box::new(test_task_spec(
"tenant-a",
"project-a",
"vp-session",
"task-session",
7,
[Capability::Command],
)),
wait_for_node: false,
artifact_path: "/vfs/artifacts/session.txt".to_owned(),
wasm_module_base64: test_wasm_module_base64(),
},
})
.unwrap_err();
assert!(denied_external_task
.to_string()
.contains("external callers may launch only EntrypointV1"));
let CoordinatorResponse::TaskLaunched {
process,
task,
actor,
assignment,
..
} = service
.handle_authorized_test_task_launch(CoordinatorRequest::LaunchTask {
task_spec: test_task_spec(
"tenant-a",
"project-a",
"vp-session",
"task-session",
7,
[Capability::Command],
),
tenant: "tenant-a".to_owned(),
project: "project-a".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
wait_for_node: false,
artifact_path: "/vfs/artifacts/session.txt".to_owned(),
wasm_module_base64: test_wasm_module_base64(),
})
.unwrap()
else {
panic!("expected authenticated task launch");
};
assert_eq!(process, ProcessId::from("vp-session"));
assert_eq!(task, TaskInstanceId::from("task-session"));
assert_eq!(actor.kind, "task");
assert_eq!(assignment.tenant, TenantId::from("tenant-a"));
assert_eq!(assignment.project, ProjectId::from("project-a"));
service
.handle_signed_node_request_auto(CoordinatorRequest::ReconnectNode {
tenant: "tenant-a".to_owned(),
project: "project-a".to_owned(),
node: "session-node".to_owned(),
process: "vp-session".to_owned(),
epoch: 7,
})
.unwrap();
let artifact_bytes = "session artifact";
service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant-a".to_owned(),
project: "project-a".to_owned(),
process: "vp-session".to_owned(),
node: "session-node".to_owned(),
task: "task-session".to_owned(),
terminal_state: Some(TaskTerminalState::Completed),
status_code: Some(0),
stdout_bytes: artifact_bytes.len() as u64,
stderr_bytes: 0,
stdout_tail: artifact_bytes.to_owned(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: Some("/vfs/artifacts/session.txt".to_owned()),
artifact_digest: Some(Digest::sha256(artifact_bytes)),
artifact_size_bytes: Some(artifact_bytes.len() as u64),
result: None,
})
.unwrap();
let CoordinatorResponse::TaskEvents { events } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::ListTaskEvents {
process: Some("vp-session".to_owned()),
},
})
.unwrap()
else {
panic!("expected authenticated task event list");
};
assert_eq!(events.len(), 1);
assert_eq!(events[0].tenant, TenantId::from("tenant-a"));
assert_eq!(events[0].project, ProjectId::from("project-a"));
service
.issue_cli_session(
TenantId::from("tenant-b"),
ProjectId::from("project-victim"),
UserId::from("user-b"),
"victim-cli-session-secret",
None,
)
.unwrap();
service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "victim-cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::StartProcess {
launch_attempt: None,
process: "vp-victim".to_owned(),
restart: false,
},
})
.unwrap();
let cross_tenant_events = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::ListTaskEvents {
process: Some("vp-victim".to_owned()),
},
})
.unwrap_err();
assert!(cross_tenant_events
.to_string()
.contains("outside the virtual process tenant/project scope"));
let CoordinatorResponse::ArtifactDownloadLink { link } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::CreateArtifactDownloadLink {
artifact: "session.txt".to_owned(),
max_bytes: 1024,
ttl_seconds: 60,
},
})
.unwrap()
else {
panic!("expected authenticated artifact link");
};
assert_eq!(link.tenant, TenantId::from("tenant-a"));
assert_eq!(link.project, ProjectId::from("project-a"));
assert_eq!(link.actor, Actor::User(UserId::from("user-a")));
let CoordinatorResponse::ProcessCancellationRequested { process, .. } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::CancelProcess {
process: "vp-session".to_owned(),
},
})
.unwrap()
else {
panic!("expected authenticated process cancellation");
};
assert_eq!(process, ProcessId::from("vp-session"));
let CoordinatorResponse::DebugAttach { actor, .. } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::DebugAttach {
process: "vp-session".to_owned(),
},
})
.unwrap()
else {
panic!("expected authenticated debug attach");
};
assert_eq!(actor, UserId::from("user-a"));
let CoordinatorResponse::NodeCredentialRevoked {
node,
tenant,
project,
actor,
descriptor_removed,
..
} = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::RevokeNodeCredential {
node: "session-node".to_owned(),
},
})
.unwrap()
else {
panic!("expected authenticated node credential revocation");
};
assert_eq!(node, NodeId::from("session-node"));
assert_eq!(tenant, TenantId::from("tenant-a"));
assert_eq!(project, ProjectId::from("project-a"));
assert_eq!(actor, UserId::from("user-a"));
assert!(descriptor_removed);
let rejected = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "wrong-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::AuthStatus,
})
.unwrap_err();
assert!(rejected.to_string().contains("not recognized"));
let expired = service
.issue_cli_session(
TenantId::from("tenant-a"),
ProjectId::from("project-b"),
UserId::from("user-a"),
"expired-cli-session-secret",
Some(1),
)
.unwrap();
assert_eq!(expired.expires_at_epoch_seconds, Some(1));
let expired_rejected = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "expired-cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::AuthStatus,
})
.unwrap_err();
assert!(expired_rejected.to_string().contains("expired"));
let CoordinatorResponse::CliSessionRevoked {
tenant,
project,
actor,
} = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::RevokeCliSession,
})
.unwrap()
else {
panic!("expected CLI session revocation");
};
assert_eq!(tenant, TenantId::from("tenant-a"));
assert_eq!(project, ProjectId::from("project-a"));
assert_eq!(actor, UserId::from("user-a"));
let revoked_rejected = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "cli-session-secret".to_owned(),
request: AuthenticatedCoordinatorRequest::AuthStatus,
})
.unwrap_err();
assert!(revoked_rejected.to_string().contains("revoked"));
}
#[test]
fn service_reports_and_enforces_public_admin_tenant_suspension() {
let mut service = CoordinatorService::new_with_admin_token(7, "admin-token");
let CoordinatorResponse::AuthStatus {
tenant,
project,
actor,
authenticated,
account_status,
suspended,
disabled,
sanitized_reason,
sensitive_moderation_details_exposed,
signup_failure_details_exposed,
..
} = service
.handle_request(CoordinatorRequest::AuthStatus {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
})
.unwrap()
else {
panic!("expected auth status");
};
assert_eq!(tenant, TenantId::from("tenant"));
assert_eq!(project, ProjectId::from("project"));
assert_eq!(actor, UserId::from("user"));
assert!(authenticated);
assert_eq!(account_status, "active");
assert!(!suspended);
assert!(!disabled);
assert!(sanitized_reason.is_none());
assert!(!sensitive_moderation_details_exposed);
assert!(!signup_failure_details_exposed);
for (tenant, policy_name, expected_status, expected_reason) in [
(
"manual-tenant",
"tenant:manual_review",
"manual_review",
"account or tenant is pending hosted review",
),
(
"disabled-tenant",
"tenant:disabled",
"disabled",
"account or tenant is disabled by hosted policy",
),
(
"deleted-tenant",
"tenant:deleted",
"deleted",
"account or tenant is no longer active",
),
] {
service.coordinator.upsert_service_policy_record(
TenantId::from(tenant),
policy_name,
Digest::from_parts([tenant.as_bytes(), policy_name.as_bytes()]),
);
let CoordinatorResponse::AuthStatus {
account_status,
suspended,
disabled,
deleted,
manual_review,
sanitized_reason,
next_actions,
sensitive_moderation_details_exposed,
signup_failure_details_exposed,
..
} = service
.handle_request(CoordinatorRequest::AuthStatus {
tenant: tenant.to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
})
.unwrap()
else {
panic!("expected inactive auth status");
};
assert_eq!(account_status, expected_status);
assert_eq!(suspended, expected_status == "suspended");
assert_eq!(disabled, expected_status == "disabled");
assert_eq!(deleted, expected_status == "deleted");
assert_eq!(manual_review, expected_status == "manual_review");
assert_eq!(sanitized_reason.as_deref(), Some(expected_reason));
assert!(next_actions
.iter()
.any(|action| action.contains("hosted operator")));
assert!(!sensitive_moderation_details_exposed);
assert!(!signup_failure_details_exposed);
}
let (admin_proof, admin_nonce, issued_at_epoch_seconds) = test_admin_request(
"admin-token",
"admin_status",
"tenant",
"admin",
"tenant",
"admin-status-1",
);
let admin_status_request = CoordinatorRequest::AdminStatus {
tenant: "tenant".to_owned(),
actor_user: "admin".to_owned(),
admin_proof,
admin_nonce,
issued_at_epoch_seconds,
};
let CoordinatorResponse::AdminStatus {
tenant,
actor,
suspended,
safe_default,
} = service
.handle_request(admin_status_request.clone())
.unwrap()
else {
panic!("expected admin status");
};
assert_eq!(tenant, TenantId::from("tenant"));
assert_eq!(actor, UserId::from("admin"));
assert!(!suspended);
assert_eq!(safe_default, "read_only");
let replayed_admin_status = service.handle_request(admin_status_request).unwrap_err();
assert!(replayed_admin_status
.to_string()
.contains("nonce was already used"));
let (admin_proof, admin_nonce, issued_at_epoch_seconds) = test_admin_request(
"admin-token",
"suspend_tenant",
"admin-tenant",
"admin",
"tenant",
"admin-suspend-1",
);
let CoordinatorResponse::TenantSuspended {
tenant,
actor,
policy,
} = service
.handle_request(CoordinatorRequest::SuspendTenant {
tenant: "admin-tenant".to_owned(),
actor_user: "admin".to_owned(),
target_tenant: "tenant".to_owned(),
admin_proof,
admin_nonce,
issued_at_epoch_seconds,
})
.unwrap()
else {
panic!("expected tenant suspension");
};
assert_eq!(tenant, TenantId::from("tenant"));
assert_eq!(actor, UserId::from("admin"));
assert_eq!(policy.name, "tenant:suspended");
let (admin_proof, admin_nonce, issued_at_epoch_seconds) = test_admin_request(
"admin-token",
"admin_status",
"tenant",
"admin",
"tenant",
"admin-status-2",
);
let CoordinatorResponse::AdminStatus { suspended, .. } = service
.handle_request(CoordinatorRequest::AdminStatus {
tenant: "tenant".to_owned(),
actor_user: "admin".to_owned(),
admin_proof,
admin_nonce,
issued_at_epoch_seconds,
})
.unwrap()
else {
panic!("expected suspended admin status");
};
assert!(suspended);
let (admin_proof, admin_nonce, issued_at_epoch_seconds) = test_admin_request(
"admin-token",
"admin_status",
"tenant",
"admin",
"tenant",
"admin-status-unconfigured",
);
let missing_admin_credential = CoordinatorService::new(7)
.handle_request(CoordinatorRequest::AdminStatus {
tenant: "tenant".to_owned(),
actor_user: "admin".to_owned(),
admin_proof,
admin_nonce,
issued_at_epoch_seconds,
})
.unwrap_err();
assert!(missing_admin_credential
.to_string()
.contains("admin credential is not configured"));
let (admin_proof, admin_nonce, issued_at_epoch_seconds) = test_admin_request(
"wrong-token",
"suspend_tenant",
"admin-tenant",
"admin",
"other-tenant",
"admin-suspend-wrong",
);
let invalid_admin_credential = service
.handle_request(CoordinatorRequest::SuspendTenant {
tenant: "admin-tenant".to_owned(),
actor_user: "admin".to_owned(),
target_tenant: "other-tenant".to_owned(),
admin_proof,
admin_nonce,
issued_at_epoch_seconds,
})
.unwrap_err();
assert!(invalid_admin_credential
.to_string()
.contains("admin request proof is invalid"));
let CoordinatorResponse::AuthStatus {
account_status,
suspended,
disabled,
sanitized_reason,
next_actions,
sensitive_moderation_details_exposed,
signup_failure_details_exposed,
..
} = service
.handle_request(CoordinatorRequest::AuthStatus {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
})
.unwrap()
else {
panic!("expected suspended auth status");
};
assert_eq!(account_status, "suspended");
assert!(suspended);
assert!(!disabled);
assert_eq!(
sanitized_reason.as_deref(),
Some("account or tenant is suspended by hosted policy")
);
assert!(next_actions
.iter()
.any(|action| action.contains("hosted operator")));
assert!(!sensitive_moderation_details_exposed);
assert!(!signup_failure_details_exposed);
let create = service
.handle_request(CoordinatorRequest::CreateProject {
tenant: "tenant".to_owned(),
actor_user: "user".to_owned(),
project: "project".to_owned(),
name: "Demo".to_owned(),
})
.unwrap_err();
assert!(create.to_string().contains("tenant is suspended"));
let start = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: Some("attempt-a".to_owned()),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process".to_owned(),
restart: false,
})
.unwrap_err();
assert!(start.to_string().contains("tenant is suspended"));
}
#[test]
fn service_manages_project_scoped_agent_public_keys() {
let mut service = CoordinatorService::new(7);
let CoordinatorResponse::AgentPublicKey { record, actor } = service
.handle_request(CoordinatorRequest::RegisterAgentPublicKey {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
agent: "agent-ci".to_owned(),
public_key: "agent-key-v1".to_owned(),
})
.unwrap()
else {
panic!("expected agent public key registration");
};
assert_eq!(actor, UserId::from("user"));
assert_eq!(record.tenant, TenantId::from("tenant"));
assert_eq!(record.project, ProjectId::from("project"));
assert_eq!(record.user, UserId::from("user"));
assert_eq!(record.agent, AgentId::from("agent-ci"));
assert_eq!(record.version, 1);
assert!(!record.revoked);
assert_eq!(record.scopes, vec!["project:read", "project:run"]);
assert!(!record.human_account_creation_privilege);
assert!(!record.browser_interaction_required_each_run);
let CoordinatorResponse::AgentPublicKey { record, .. } = service
.handle_request(CoordinatorRequest::RegisterAgentPublicKey {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
agent: "agent-ci".to_owned(),
public_key: "agent-key-v2".to_owned(),
})
.unwrap()
else {
panic!("expected agent public key rotation by re-add");
};
assert_eq!(record.version, 2);
assert_eq!(record.public_key, "agent-key-v2");
let CoordinatorResponse::AgentPublicKeys { records, actor } = service
.handle_request(CoordinatorRequest::ListAgentPublicKeys {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
})
.unwrap()
else {
panic!("expected agent public key list");
};
assert_eq!(actor, UserId::from("user"));
assert_eq!(records.len(), 1);
assert_eq!(records[0].agent, AgentId::from("agent-ci"));
let CoordinatorResponse::AgentPublicKeys { records, .. } = service
.handle_request(CoordinatorRequest::ListAgentPublicKeys {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "other-user".to_owned(),
})
.unwrap()
else {
panic!("expected foreign user agent public key list");
};
assert!(records.is_empty());
let CoordinatorResponse::AgentPublicKeys { records, .. } = service
.handle_request(CoordinatorRequest::ListAgentPublicKeys {
tenant: "other-tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
})
.unwrap()
else {
panic!("expected foreign tenant agent public key list");
};
assert!(records.is_empty());
let foreign_user_revoke = service
.handle_request(CoordinatorRequest::RevokeAgentPublicKey {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "other-user".to_owned(),
agent: "agent-ci".to_owned(),
})
.unwrap_err();
assert!(foreign_user_revoke
.to_string()
.contains("signed-in user scope"));
let CoordinatorResponse::AgentPublicKey { record, actor } = service
.handle_request(CoordinatorRequest::RevokeAgentPublicKey {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
agent: "agent-ci".to_owned(),
})
.unwrap()
else {
panic!("expected agent public key revocation");
};
assert_eq!(actor, UserId::from("user"));
assert!(record.revoked);
}
#[test]
fn service_runs_agent_workflows_with_scoped_key_attribution() {
let mut service = CoordinatorService::new(7);
let agent_public_key = test_agent_public_key();
let agent_fingerprint = Digest::sha256(&agent_public_key);
service
.handle_request(CoordinatorRequest::RegisterAgentPublicKey {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
agent: "agent-ci".to_owned(),
public_key: agent_public_key,
})
.unwrap();
let fingerprint_only = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: Some("agent-ci".to_owned()),
agent_public_key_fingerprint: Some(agent_fingerprint.clone()),
agent_signature: None,
process: "vp-agent-fingerprint-only".to_owned(),
restart: false,
})
.unwrap_err();
assert!(fingerprint_only.to_string().contains("signed request"));
let wrong_fingerprint = service
.handle_request(with_signed_agent_workflow(
CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: Some("agent-ci".to_owned()),
agent_public_key_fingerprint: Some(Digest::sha256("other-key")),
agent_signature: None,
process: "vp-agent-bad-key".to_owned(),
restart: false,
},
"start_process",
"vp-agent-bad-key",
None,
"bad-fingerprint-nonce",
))
.unwrap_err();
assert!(wrong_fingerprint.to_string().contains("fingerprint"));
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker-linux".to_owned(),
public_key: test_node_public_key("worker-linux"),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker-linux".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: true,
online: true,
})
.unwrap();
let CoordinatorResponse::ProcessStarted {
launch_attempt: None,
actor: start_actor,
..
} = service
.handle_request(with_signed_agent_workflow(
CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: Some("agent-ci".to_owned()),
agent_public_key_fingerprint: Some(agent_fingerprint.clone()),
agent_signature: None,
process: "vp-agent".to_owned(),
restart: false,
},
"start_process",
"vp-agent",
None,
"start-nonce",
))
.unwrap()
else {
panic!("expected agent-authenticated process start");
};
assert_agent_workflow_actor(&start_actor, &agent_fingerprint);
let replay = service
.handle_request(with_signed_agent_workflow(
CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: Some("agent-ci".to_owned()),
agent_public_key_fingerprint: Some(agent_fingerprint.clone()),
agent_signature: None,
process: "vp-agent".to_owned(),
restart: true,
},
"start_process",
"vp-agent",
None,
"start-nonce",
))
.unwrap_err();
assert!(replay.to_string().contains("nonce"));
let denied_external_task = service
.handle_request(with_signed_agent_workflow(
CoordinatorRequest::LaunchTask {
task_spec: test_task_spec(
"tenant",
"project",
"vp-agent",
"compile-linux",
7,
[Capability::Command],
),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: Some("agent-ci".to_owned()),
agent_public_key_fingerprint: Some(agent_fingerprint.clone()),
agent_signature: None,
wait_for_node: false,
artifact_path: "/vfs/artifacts/dap-output.txt".to_owned(),
wasm_module_base64: test_wasm_module_base64(),
},
"launch_task",
"vp-agent",
Some("compile-linux"),
"launch-nonce",
))
.unwrap_err();
assert!(denied_external_task
.to_string()
.contains("external callers may launch only EntrypointV1"));
}
#[test]
fn signed_node_and_agent_requests_reject_body_modification() {
let mut service = CoordinatorService::new(7);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker-bound-body".to_owned(),
public_key: test_node_public_key("worker-bound-body"),
})
.unwrap();
let original_node_request = CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker-bound-body".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: true,
online: true,
};
let original_node_digest =
signed_request_payload_digest(&serde_json::to_value(&original_node_request).unwrap());
let node_signature = signed_node_request_with_private_key(
"worker-bound-body",
&test_node_private_key("worker-bound-body"),
"report_node_capabilities",
&original_node_digest,
"node-body-modification",
);
let mut modified_node_request = original_node_request;
let CoordinatorRequest::ReportNodeCapabilities { online, .. } = &mut modified_node_request
else {
unreachable!();
};
*online = false;
let modified_node = service
.handle_request(CoordinatorRequest::SignedNode {
node: "worker-bound-body".to_owned(),
node_signature,
request: Box::new(modified_node_request),
})
.unwrap_err();
assert!(modified_node.to_string().contains("signature"));
let agent_public_key = test_agent_public_key();
let agent_fingerprint = Digest::sha256(&agent_public_key);
service
.handle_request(CoordinatorRequest::RegisterAgentPublicKey {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
agent: "agent-ci".to_owned(),
public_key: agent_public_key,
})
.unwrap();
let original_agent_request = CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: Some("agent-ci".to_owned()),
agent_public_key_fingerprint: Some(agent_fingerprint),
agent_signature: None,
process: "vp-agent-bound-body".to_owned(),
restart: false,
};
let agent_signature = signed_agent_workflow_request(
&original_agent_request,
"start_process",
"vp-agent-bound-body",
None,
"agent-body-modification",
);
let mut modified_agent_request = original_agent_request;
let CoordinatorRequest::StartProcess {
launch_attempt: None,
restart,
agent_signature: request_signature,
..
} = &mut modified_agent_request
else {
unreachable!();
};
*restart = true;
*request_signature = Some(agent_signature);
let modified_agent = service.handle_request(modified_agent_request).unwrap_err();
assert!(modified_agent.to_string().contains("signature"));
}
#[test]
fn service_checks_spawn_quota_before_process_or_task_work_starts() {
let mut service = CoordinatorService::new(7);
service.quota.set_workflow_limits(ResourceLimits {
limits: BTreeMap::from([(LimitKind::Spawn, 2)]),
});
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker-linux".to_owned(),
public_key: test_node_public_key("worker-linux"),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker-linux".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: true,
online: true,
})
.unwrap();
let CoordinatorResponse::ProcessStarted {
launch_attempt: None,
charged_spawns,
..
} = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "vp-quota".to_owned(),
restart: false,
})
.unwrap()
else {
panic!("expected process start within spawn quota");
};
assert_eq!(charged_spawns, 1);
let CoordinatorResponse::TaskLaunched { charged_spawns, .. } = service
.handle_authorized_test_task_launch(CoordinatorRequest::LaunchTask {
task_spec: test_task_spec(
"tenant",
"project",
"vp-quota",
"compile-linux",
7,
[Capability::Command],
),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: Some("user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
wait_for_node: false,
artifact_path: "/vfs/artifacts/dap-output.txt".to_owned(),
wasm_module_base64: test_wasm_module_base64(),
})
.unwrap()
else {
panic!("expected task launch within spawn quota");
};
assert_eq!(charged_spawns, 2);
let denied_task = service
.handle_authorized_test_task_launch(CoordinatorRequest::LaunchTask {
task_spec: test_task_spec(
"tenant",
"project",
"vp-quota",
"compile-linux-denied",
7,
[Capability::Command],
),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: Some("user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
wait_for_node: false,
artifact_path: "/vfs/artifacts/denied.txt".to_owned(),
wasm_module_base64: test_wasm_module_base64(),
})
.unwrap_err();
assert!(denied_task.to_string().contains("Spawn"));
let now_epoch_seconds = service.current_epoch_seconds().unwrap();
assert_eq!(
service.quota.used_workflow_spawns(
&TenantId::from("tenant"),
&ProjectId::from("project"),
now_epoch_seconds,
),
2
);
let denied_task_key = task_control_key(
&TenantId::from("tenant"),
&ProjectId::from("project"),
&ProcessId::from("vp-quota"),
&NodeId::from("worker-linux"),
&TaskInstanceId::from("compile-linux-denied"),
);
assert!(!service.active_tasks.contains(&denied_task_key));
assert!(!service.task_placements.contains_key(&denied_task_key));
assert_eq!(
service
.task_assignments
.get(&(
TenantId::from("tenant"),
ProjectId::from("project"),
NodeId::from("worker-linux"),
))
.map(VecDeque::len),
Some(1)
);
let other_project_process = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "other-project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "vp-denied".to_owned(),
restart: false,
})
.unwrap();
assert!(matches!(
other_project_process,
CoordinatorResponse::ProcessStarted {
launch_attempt: None,
..
}
));
assert_eq!(
service.quota.used_workflow_spawns(
&TenantId::from("tenant"),
&ProjectId::from("other-project"),
now_epoch_seconds,
),
1
);
assert_eq!(
service.quota.used_workflow_spawns(
&TenantId::from("tenant"),
&ProjectId::from("project"),
now_epoch_seconds,
),
2
);
}
#[test]
fn project_quota_resets_at_the_configured_window_boundary() {
let mut limits = ResourceLimits::unlimited();
limits.limits.insert(LimitKind::Spawn, 1);
let quota = CoordinatorQuotaConfiguration::new(limits, [(LimitKind::Spawn, 60)]).unwrap();
let mut service = CoordinatorService::new_with_admin_token_database_url_and_quota(
7,
"test-admin-token",
None,
quota,
)
.unwrap();
service.set_server_time(59);
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: Some("attempt-b".to_owned()),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "vp-window-one".to_owned(),
restart: false,
})
.unwrap();
service
.handle_request(CoordinatorRequest::AbortProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "vp-window-one".to_owned(),
})
.unwrap();
let exhausted = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: Some("attempt-b".to_owned()),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "vp-still-window-one".to_owned(),
restart: false,
})
.unwrap_err();
assert!(exhausted.to_string().contains("Spawn"));
service.set_server_time(60);
let started = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "vp-window-two".to_owned(),
restart: false,
})
.unwrap();
assert!(matches!(
started,
CoordinatorResponse::ProcessStarted {
launch_attempt: None,
charged_spawns: 1,
..
}
));
assert_eq!(service.quota.active_meter_count(), 1);
}
#[test]
fn authenticated_api_calls_are_metered_per_tenant_and_project_before_dispatch() {
let mut limits = ResourceLimits::unlimited();
limits.limits.insert(LimitKind::ApiCall, 1);
let quota = CoordinatorQuotaConfiguration::new(limits, [(LimitKind::ApiCall, 60)]).unwrap();
let mut service = CoordinatorService::new_with_admin_token_database_url_and_quota(
7,
"test-admin-token",
None,
quota,
)
.unwrap();
service.set_server_time(30);
for (project, secret) in [("project-a", "session-a"), ("project-b", "session-b")] {
service
.issue_cli_session(
TenantId::from("tenant"),
ProjectId::from(project),
UserId::from("user"),
secret,
None,
)
.unwrap();
}
service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::AuthStatus,
})
.unwrap();
let denied = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::AuthStatus,
})
.unwrap_err();
assert!(denied.to_string().contains("ApiCall"));
service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-b".to_owned(),
request: AuthenticatedCoordinatorRequest::AuthStatus,
})
.unwrap();
assert_eq!(
service
.quota
.used_api_calls(&TenantId::from("tenant"), &ProjectId::from("project-a"), 30,),
1
);
assert_eq!(
service
.quota
.used_api_calls(&TenantId::from("tenant"), &ProjectId::from("project-b"), 30,),
1
);
}
#[test]
fn signed_node_log_ingestion_truncates_at_scoped_quota_without_failing_reports() {
let mut limits = ResourceLimits::unlimited();
limits.limits.insert(LimitKind::LogBytes, 4);
let quota = CoordinatorQuotaConfiguration::new(limits, [(LimitKind::LogBytes, 60)]).unwrap();
let mut service = CoordinatorService::new_with_admin_token_database_url_and_quota(
7,
"test-admin-token",
None,
quota,
)
.unwrap();
service.set_server_time(30);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
public_key: test_node_public_key("node"),
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process".to_owned(),
restart: false,
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReconnectNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
process: "process".to_owned(),
epoch: 7,
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportTaskLog {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "node".to_owned(),
task: "task".to_owned(),
stdout_bytes: 3,
stderr_bytes: 1,
stdout_tail: "out".to_owned(),
stderr_tail: "e".to_owned(),
stdout_truncated: false,
stderr_truncated: false,
backpressured: false,
})
.unwrap();
let truncated = service
.handle_signed_node_request_auto(CoordinatorRequest::ReportTaskLog {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "node".to_owned(),
task: "task".to_owned(),
stdout_bytes: 4,
stderr_bytes: 1,
stdout_tail: "outx".to_owned(),
stderr_tail: "e".to_owned(),
stdout_truncated: false,
stderr_truncated: false,
backpressured: true,
})
.unwrap();
let CoordinatorResponse::TaskLogRecorded {
stdout_tail,
stdout_bytes: 4,
..
} = truncated
else {
panic!("expected a successful truncated task-log report");
};
assert_eq!(stdout_tail, "[log output truncated at project log quota]");
assert!(service
.recent_logs
.get(&(TenantId::from("tenant"), ProjectId::from("project")))
.unwrap()
.iter()
.any(|entry| entry.text.contains("project log quota") && entry.truncated));
assert_eq!(
service
.quota
.used_log_bytes(&TenantId::from("tenant"), &ProjectId::from("project"), 30,),
4
);
}
#[test]
fn log_quota_exhaustion_cannot_strand_task_completion_or_artifact_publication() {
let mut limits = ResourceLimits::unlimited();
limits.limits.insert(LimitKind::LogBytes, 4);
let quota = CoordinatorQuotaConfiguration::new(limits, [(LimitKind::LogBytes, 60)]).unwrap();
let mut service = CoordinatorService::new_with_admin_token_database_url_and_quota(
7,
"test-admin-token",
None,
quota,
)
.unwrap();
service.set_server_time(30);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
public_key: test_node_public_key("node"),
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process".to_owned(),
restart: false,
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReconnectNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
process: "process".to_owned(),
epoch: 7,
})
.unwrap();
register_test_task_assignment(
&mut service,
"tenant",
"project",
"process",
"node",
"compile",
"compile-one",
7,
);
service.record_task_completion_event(TaskCompletionEvent {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
process: ProcessId::from("process"),
node: NodeId::from("coordinator-main"),
executor: TaskExecutor::CoordinatorMain,
task_definition: TaskDefinitionId::from("build"),
task: TaskInstanceId::from("main"),
attempt_id: None,
placement: None,
terminal_state: TaskTerminalState::Completed,
status_code: Some(0),
stdout_bytes: 0,
stderr_bytes: 0,
stdout_tail: String::new(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
artifact_digest: None,
artifact_size_bytes: None,
result: None,
});
service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "node".to_owned(),
task: "compile-one".to_owned(),
terminal_state: Some(TaskTerminalState::Completed),
status_code: Some(0),
stdout_bytes: 64,
stderr_bytes: 0,
stdout_tail: "the-real-final-tail".to_owned(),
stderr_tail: String::new(),
stdout_truncated: true,
stderr_truncated: false,
artifact_path: Some("/vfs/artifacts/result.bin".to_owned()),
artifact_digest: Some(Digest::sha256("artifact bytes")),
artifact_size_bytes: Some(14),
result: None,
})
.unwrap();
let event = service
.task_events
.iter()
.find(|event| event.task == TaskInstanceId::from("compile-one"))
.unwrap();
assert_eq!(
event.stdout_tail,
"[log output truncated at project log quota]"
);
assert!(event.stdout_truncated);
assert!(!service
.active_tasks
.iter()
.any(|key| key.4 == TaskInstanceId::from("compile-one")));
assert!(service
.coordinator
.active_process(
&TenantId::from("tenant"),
&ProjectId::from("project"),
&ProcessId::from("process"),
)
.is_none());
assert!(matches!(
service
.handle_request(CoordinatorRequest::GetArtifact {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "result.bin".to_owned(),
})
.unwrap(),
CoordinatorResponse::Artifact { .. }
));
}
#[test]
fn service_attaches_node_starts_process_and_records_scoped_task_event() {
let mut service = CoordinatorService::new(7);
let attached = service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
public_key: test_node_public_key("node"),
})
.unwrap();
assert!(matches!(attached, CoordinatorResponse::NodeAttached { .. }));
let started = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process".to_owned(),
restart: false,
})
.unwrap();
assert_eq!(
started,
CoordinatorResponse::ProcessStarted {
launch_attempt: None,
process: ProcessId::from("process"),
epoch: 7,
actor: WorkflowActor {
kind: "user".to_owned(),
user: Some(UserId::from("user")),
agent: None,
credential_kind: CredentialKind::BrowserSession,
public_key_fingerprint: None,
authenticated_without_browser: false,
scopes: vec!["project:read".to_owned(), "project:run".to_owned()],
},
charged_spawns: 1,
}
);
let heartbeat = service
.handle_request(CoordinatorRequest::NodeHeartbeat {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
node_signature: Some(signed_node_heartbeat("node", "task-event-heartbeat")),
})
.unwrap();
assert_eq!(
heartbeat,
CoordinatorResponse::NodeHeartbeat {
node: NodeId::from("node"),
epoch: 7,
}
);
service
.handle_signed_node_request_auto(CoordinatorRequest::ReconnectNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
process: "process".to_owned(),
epoch: 7,
})
.unwrap();
register_test_task_assignment(
&mut service,
"tenant",
"project",
"process",
"node",
"compile-linux",
"compile-linux",
7,
);
let recorded = service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "node".to_owned(),
task: "compile-linux".to_owned(),
terminal_state: None,
status_code: Some(0),
stdout_bytes: 12,
stderr_bytes: 0,
stdout_tail: "build ok".to_owned(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: Some("/vfs/artifacts/app.txt".to_owned()),
artifact_digest: Some(Digest::sha256("artifact")),
artifact_size_bytes: Some(12),
result: None,
})
.unwrap();
assert_eq!(
recorded,
CoordinatorResponse::TaskRecorded {
process: ProcessId::from("process"),
task: TaskInstanceId::from("compile-linux"),
events_recorded: 1
}
);
let CoordinatorResponse::TaskEvents { events } = service
.handle_request(CoordinatorRequest::ListTaskEvents {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: Some("process".to_owned()),
})
.unwrap()
else {
panic!("expected task events");
};
assert_eq!(events.len(), 1);
assert_eq!(events[0].node, NodeId::from("node"));
assert_eq!(events[0].stdout_tail, "build ok");
assert_eq!(events[0].stderr_tail, "");
assert!(!events[0].stdout_truncated);
let oversized_tail = "x".repeat(MAX_TASK_LOG_TAIL_BYTES + 1);
let oversized_log = service
.handle_signed_node_request_auto(CoordinatorRequest::ReportTaskLog {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "node".to_owned(),
task: "compile-linux".to_owned(),
stdout_bytes: oversized_tail.len() as u64,
stderr_bytes: 0,
stdout_tail: oversized_tail.clone(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
backpressured: false,
})
.unwrap_err();
assert!(oversized_log.to_string().contains("stdout_tail"));
let oversized_completion = service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "node".to_owned(),
task: "compile-linux".to_owned(),
terminal_state: None,
status_code: Some(0),
stdout_bytes: oversized_tail.len() as u64,
stderr_bytes: 0,
stdout_tail: oversized_tail,
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
artifact_digest: None,
artifact_size_bytes: None,
result: None,
})
.unwrap_err();
assert!(oversized_completion.to_string().contains("stdout_tail"));
let cross_tenant = service
.handle_request(CoordinatorRequest::ListTaskEvents {
tenant: "other".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: Some("process".to_owned()),
})
.unwrap_err();
assert!(cross_tenant
.to_string()
.contains("outside the virtual process tenant/project scope"));
let CoordinatorResponse::TaskEvents { events } = service
.handle_request(CoordinatorRequest::ListTaskEvents {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: Some("other-process".to_owned()),
})
.unwrap()
else {
panic!("expected task events");
};
assert!(events.is_empty());
}
#[test]
fn service_revokes_node_credentials_and_live_descriptors() {
let mut service = CoordinatorService::new(7);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
public_key: test_node_public_key("node"),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: vec![],
dependency_cache_digests: vec![],
source_snapshots: vec![],
artifact_locations: vec![],
direct_connectivity: true,
online: true,
})
.unwrap();
let CoordinatorResponse::NodeCredentialRevoked {
node,
tenant,
project,
actor,
descriptor_removed,
queued_assignments_removed,
} = service
.handle_request(CoordinatorRequest::RevokeNodeCredential {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
node: "node".to_owned(),
})
.unwrap()
else {
panic!("expected node credential revocation");
};
assert_eq!(node, NodeId::from("node"));
assert_eq!(tenant, TenantId::from("tenant"));
assert_eq!(project, ProjectId::from("project"));
assert_eq!(actor, UserId::from("user"));
assert!(descriptor_removed);
assert_eq!(queued_assignments_removed, 0);
let heartbeat = service
.handle_request(CoordinatorRequest::NodeHeartbeat {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
node_signature: Some(signed_node_heartbeat("node", "revoked-heartbeat")),
})
.unwrap_err();
assert!(heartbeat.to_string().contains("not enrolled"));
let CoordinatorResponse::NodeDescriptors { descriptors, .. } = service
.handle_request(CoordinatorRequest::ListNodeDescriptors {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
})
.unwrap()
else {
panic!("expected node descriptors");
};
assert!(descriptors.is_empty());
}
#[test]
fn duplicate_node_ids_are_isolated_across_identity_replay_liveness_and_revocation() {
let mut service = CoordinatorService::new(19);
let node = "shared-node";
let private_a = test_node_private_key("tenant-a-shared-node");
let private_b = test_node_private_key("tenant-b-shared-node");
let private_c = test_node_private_key("tenant-a-other-project-shared-node");
let public_a = node_ed25519_public_key_from_private_key(&private_a).unwrap();
let public_b = node_ed25519_public_key_from_private_key(&private_b).unwrap();
let public_c = node_ed25519_public_key_from_private_key(&private_c).unwrap();
for (tenant, project, public_key) in [
("tenant-a", "project-a", public_a),
("tenant-b", "project-b", public_b),
("tenant-a", "project-c", public_c),
] {
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: tenant.to_owned(),
project: project.to_owned(),
node: node.to_owned(),
public_key,
})
.unwrap();
}
assert_eq!(
service
.coordinator
.node_identity_count_for_tenant(&TenantId::from("tenant-a")),
2
);
assert_eq!(
service
.coordinator
.node_identity_count_for_tenant(&TenantId::from("tenant-b")),
1
);
for (index, (tenant, project, private_key)) in [
("tenant-a", "project-a", private_a.as_str()),
("tenant-b", "project-b", private_b.as_str()),
("tenant-a", "project-c", private_c.as_str()),
]
.into_iter()
.enumerate()
{
service.set_server_time(100 + index as u64);
service
.handle_request(CoordinatorRequest::NodeHeartbeat {
tenant: tenant.to_owned(),
project: project.to_owned(),
node: node.to_owned(),
node_signature: Some(signed_node_heartbeat_in_scope_with_private_key(
tenant,
project,
node,
private_key,
"same-nonce",
)),
})
.unwrap();
service
.handle_request(signed_node_request_auto_with_private_key(
CoordinatorRequest::ReportNodeCapabilities {
tenant: tenant.to_owned(),
project: project.to_owned(),
node: node.to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: vec![Digest::sha256(format!(
"cache-{tenant}-{project}"
))],
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: index != 1,
online: true,
},
private_key,
))
.unwrap();
}
let replay = service
.handle_request(CoordinatorRequest::NodeHeartbeat {
tenant: "tenant-a".to_owned(),
project: "project-a".to_owned(),
node: node.to_owned(),
node_signature: Some(signed_node_heartbeat_in_scope_with_private_key(
"tenant-a",
"project-a",
node,
&private_a,
"same-nonce",
)),
})
.unwrap_err();
assert!(replay.to_string().contains("nonce"));
let forged = service
.handle_request(CoordinatorRequest::NodeHeartbeat {
tenant: "tenant-b".to_owned(),
project: "project-b".to_owned(),
node: node.to_owned(),
node_signature: Some(signed_node_heartbeat_in_scope_with_private_key(
"tenant-b",
"project-b",
node,
&private_a,
"forged-cross-scope",
)),
})
.unwrap_err();
assert!(forged.to_string().contains("signature"));
let scope_a = crate::NodeScopeKey::new(
TenantId::from("tenant-a"),
ProjectId::from("project-a"),
NodeId::from(node),
);
let scope_b = crate::NodeScopeKey::new(
TenantId::from("tenant-b"),
ProjectId::from("project-b"),
NodeId::from(node),
);
let scope_c = crate::NodeScopeKey::new(
TenantId::from("tenant-a"),
ProjectId::from("project-c"),
NodeId::from(node),
);
assert!(service.node_descriptors.contains_key(&scope_a));
assert!(service.node_descriptors.contains_key(&scope_b));
assert!(service.node_descriptors.contains_key(&scope_c));
assert!(service.node_last_seen_epoch_seconds.contains_key(&scope_a));
assert!(service.node_last_seen_epoch_seconds.contains_key(&scope_b));
assert!(service.node_last_seen_epoch_seconds.contains_key(&scope_c));
assert_eq!(service.node_last_seen_epoch_seconds[&scope_a], 100);
assert_eq!(service.node_last_seen_epoch_seconds[&scope_b], 101);
assert_eq!(service.node_last_seen_epoch_seconds[&scope_c], 102);
assert!(service.node_descriptors[&scope_a].direct_connectivity);
assert!(!service.node_descriptors[&scope_b].direct_connectivity);
assert!(service.node_descriptors[&scope_c].direct_connectivity);
assert!(service
.node_replay_nonces
.contains_key(&(scope_a.clone(), "same-nonce".to_owned())));
assert!(service
.node_replay_nonces
.contains_key(&(scope_b.clone(), "same-nonce".to_owned())));
assert!(service
.node_replay_nonces
.contains_key(&(scope_c.clone(), "same-nonce".to_owned())));
service
.handle_request(CoordinatorRequest::RevokeNodeCredential {
tenant: "tenant-a".to_owned(),
project: "project-a".to_owned(),
actor_user: "user-a".to_owned(),
node: node.to_owned(),
})
.unwrap();
assert!(service
.coordinator
.node_identity(
&TenantId::from("tenant-a"),
&ProjectId::from("project-a"),
&NodeId::from(node),
)
.is_none());
assert!(service
.coordinator
.node_identity(
&TenantId::from("tenant-b"),
&ProjectId::from("project-b"),
&NodeId::from(node),
)
.is_some());
assert!(!service.node_descriptors.contains_key(&scope_a));
assert!(service.node_descriptors.contains_key(&scope_b));
assert!(service.node_descriptors.contains_key(&scope_c));
service
.handle_request(CoordinatorRequest::NodeHeartbeat {
tenant: "tenant-b".to_owned(),
project: "project-b".to_owned(),
node: node.to_owned(),
node_signature: Some(signed_node_heartbeat_in_scope_with_private_key(
"tenant-b",
"project-b",
node,
&private_b,
"post-other-scope-revocation",
)),
})
.expect("revoking tenant A's duplicate node must not affect tenant B");
}
#[test]
fn service_delivers_cancellation_to_connected_node_and_records_terminal_state() {
let mut service = CoordinatorService::new(7);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
public_key: test_node_public_key("node"),
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process".to_owned(),
restart: false,
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReconnectNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
process: "process".to_owned(),
epoch: 7,
})
.unwrap();
register_test_task_assignment(
&mut service,
"tenant",
"project",
"process",
"node",
"compile-linux",
"compile-linux",
7,
);
let cancelled = service
.handle_request(CoordinatorRequest::CancelTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "node".to_owned(),
task: "compile-linux".to_owned(),
})
.unwrap();
assert_eq!(
cancelled,
CoordinatorResponse::TaskCancellationRequested {
process: ProcessId::from("process"),
task: TaskInstanceId::from("compile-linux"),
node: NodeId::from("node"),
}
);
let control = service
.handle_signed_node_request_auto(CoordinatorRequest::PollTaskControl {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "node".to_owned(),
task: "compile-linux".to_owned(),
})
.unwrap();
assert_eq!(
control,
CoordinatorResponse::TaskControl {
process: ProcessId::from("process"),
task: TaskInstanceId::from("compile-linux"),
cancel_requested: true,
abort_requested: false,
}
);
service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "node".to_owned(),
task: "compile-linux".to_owned(),
terminal_state: Some(TaskTerminalState::Cancelled),
status_code: None,
stdout_bytes: 0,
stderr_bytes: 0,
stdout_tail: String::new(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
artifact_digest: None,
artifact_size_bytes: None,
result: None,
})
.unwrap();
let CoordinatorResponse::TaskEvents { events } = service
.handle_request(CoordinatorRequest::ListTaskEvents {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: Some("process".to_owned()),
})
.unwrap()
else {
panic!("expected task events");
};
assert_eq!(events[0].terminal_state, TaskTerminalState::Cancelled);
let control = service
.handle_signed_node_request_auto(CoordinatorRequest::PollTaskControl {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "node".to_owned(),
task: "compile-linux".to_owned(),
})
.unwrap();
assert_eq!(
control,
CoordinatorResponse::TaskControl {
process: ProcessId::from("process"),
task: TaskInstanceId::from("compile-linux"),
cancel_requested: false,
abort_requested: false,
}
);
}
#[test]
fn service_authorizes_debug_attach_through_public_api() {
let mut service = CoordinatorService::new(7);
service
.handle_request(CoordinatorRequest::CreateProject {
tenant: "tenant".to_owned(),
actor_user: "user".to_owned(),
project: "project".to_owned(),
name: "Demo".to_owned(),
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process".to_owned(),
restart: false,
})
.unwrap();
let CoordinatorResponse::DebugAttach {
process,
actor,
authorization,
audit_event,
charged_debug_read_bytes,
used_debug_read_bytes,
} = service
.handle_request(CoordinatorRequest::DebugAttach {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process".to_owned(),
})
.unwrap()
else {
panic!("expected debug attach authorization");
};
assert_eq!(process, ProcessId::from("process"));
assert_eq!(actor, UserId::from("user"));
assert!(authorization.allowed);
assert_eq!(audit_event.operation, "debug_attach");
assert_eq!(audit_event.actor, UserId::from("user"));
assert!(audit_event.allowed);
assert_eq!(charged_debug_read_bytes, DEBUG_CONTROL_READ_BYTES);
assert_eq!(used_debug_read_bytes, DEBUG_CONTROL_READ_BYTES);
let CoordinatorResponse::DebugAttach {
authorization,
audit_event,
charged_debug_read_bytes,
used_debug_read_bytes,
..
} = service
.handle_request(CoordinatorRequest::DebugAttach {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "other-user".to_owned(),
process: "process".to_owned(),
})
.unwrap()
else {
panic!("expected denied debug attach authorization");
};
assert!(!authorization.allowed);
assert!(authorization.reason.contains("explicit project permission"));
assert!(!audit_event.allowed);
assert_eq!(audit_event.charged_debug_read_bytes, 0);
assert_eq!(charged_debug_read_bytes, 0);
assert_eq!(used_debug_read_bytes, DEBUG_CONTROL_READ_BYTES);
let CoordinatorResponse::DebugAttach {
authorization,
audit_event,
charged_debug_read_bytes,
used_debug_read_bytes,
..
} = service
.handle_request(CoordinatorRequest::DebugAttach {
tenant: "other-tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process".to_owned(),
})
.unwrap()
else {
panic!("expected cross-tenant debug attach denial");
};
assert!(!authorization.allowed);
assert!(authorization.reason.contains("tenant or project"));
assert!(!audit_event.allowed);
assert_eq!(charged_debug_read_bytes, 0);
assert_eq!(used_debug_read_bytes, 0);
assert_eq!(service.debug_audit_events.len(), 3);
}
#[test]
fn debug_epoch_commands_are_polled_by_signed_active_task_nodes() {
let mut service = CoordinatorService::new(7);
service
.handle_request(CoordinatorRequest::CreateProject {
tenant: "tenant".to_owned(),
actor_user: "user".to_owned(),
project: "project".to_owned(),
name: "Demo".to_owned(),
})
.unwrap();
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker".to_owned(),
public_key: test_node_public_key("worker"),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: vec![],
dependency_cache_digests: vec![],
source_snapshots: vec![],
artifact_locations: vec![],
direct_connectivity: true,
online: true,
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: Some("user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process".to_owned(),
restart: false,
})
.unwrap();
service
.handle_authorized_test_task_launch(CoordinatorRequest::LaunchTask {
task_spec: test_task_spec(
"tenant",
"project",
"process",
"compile-linux",
7,
[Capability::Command],
),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: Some("user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
wait_for_node: false,
artifact_path: "/vfs/artifacts/out.txt".to_owned(),
wasm_module_base64: test_wasm_module_base64(),
})
.unwrap();
service.main_runtime.controls.insert(
process_control_key(
&TenantId::from("tenant"),
&ProjectId::from("project"),
&ProcessId::from("process"),
),
super::main_runtime::CoordinatorMainControl {
task_definition: clusterflux_core::TaskDefinitionId::from("completed-main"),
task_instance: TaskInstanceId::from("ti:process:main"),
abort: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
debug: std::sync::Arc::new(clusterflux_wasm_runtime::WasmDebugControl::default()),
state: "completed".to_owned(),
stopped_probe_symbol: None,
handles: std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
launch_id: 1,
},
);
let CoordinatorResponse::DebugBreakpoints {
revision,
probe_symbols,
hit_epoch,
..
} = service
.handle_request(CoordinatorRequest::SetDebugBreakpoints {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process".to_owned(),
revision: 1,
probe_symbols: vec!["clusterflux.probe.compile_linux".to_owned()],
})
.unwrap()
else {
panic!("expected debug breakpoints response");
};
assert_eq!(revision, 1);
assert_eq!(probe_symbols, ["clusterflux.probe.compile_linux"]);
assert_eq!(hit_epoch, None);
let CoordinatorResponse::DebugBreakpoints {
revision,
probe_symbols,
..
} = service
.handle_request(CoordinatorRequest::SetDebugBreakpoints {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process".to_owned(),
revision: 0,
probe_symbols: vec!["clusterflux.probe.stale".to_owned()],
})
.unwrap()
else {
panic!("expected stale breakpoint response");
};
assert_eq!(revision, 1);
assert_eq!(probe_symbols, ["clusterflux.probe.compile_linux"]);
let CoordinatorResponse::DebugProbeHit {
breakpoint_matched,
debug_epoch,
probe_symbol,
..
} = service
.handle_signed_node_request_auto(CoordinatorRequest::ReportDebugProbeHit {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "worker".to_owned(),
task: "compile-linux".to_owned(),
probe_symbol: "clusterflux.probe.compile_linux".to_owned(),
})
.unwrap()
else {
panic!("expected executing Wasm probe hit response");
};
assert!(breakpoint_matched);
assert_eq!(debug_epoch, Some(1));
assert_eq!(probe_symbol, "clusterflux.probe.compile_linux");
let CoordinatorResponse::DebugBreakpoints {
hit_epoch,
hit_task,
hit_probe_symbol,
..
} = service
.handle_request(CoordinatorRequest::InspectDebugBreakpoints {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process".to_owned(),
})
.unwrap()
else {
panic!("expected breakpoint hit status");
};
assert_eq!(hit_epoch, Some(1));
assert_eq!(hit_task, Some(TaskInstanceId::from("compile-linux")));
assert_eq!(
hit_probe_symbol.as_deref(),
Some("clusterflux.probe.compile_linux")
);
let CoordinatorResponse::DebugCommand {
epoch: Some(1),
command: Some(command),
..
} = service
.handle_signed_node_request_auto(CoordinatorRequest::PollDebugCommand {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "worker".to_owned(),
task: "compile-linux".to_owned(),
})
.unwrap()
else {
panic!("expected signed node poll to receive freeze command");
};
assert_eq!(command, "freeze");
let CoordinatorResponse::DebugCommand {
epoch: None,
command: None,
..
} = service
.handle_signed_node_request_auto(CoordinatorRequest::PollDebugCommand {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "worker".to_owned(),
task: "compile-linux".to_owned(),
})
.unwrap()
else {
panic!("debug commands should be consumed after poll");
};
let CoordinatorResponse::DebugEpochStatus {
fully_frozen,
fully_resumed,
acknowledgements,
..
} = service
.handle_request(CoordinatorRequest::InspectDebugEpoch {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process".to_owned(),
epoch: 1,
})
.unwrap()
else {
panic!("expected pending debug epoch status");
};
assert!(!fully_frozen);
assert!(!fully_resumed);
assert!(acknowledgements.is_empty());
let early_resume = service
.handle_request(CoordinatorRequest::ResumeDebugEpoch {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process".to_owned(),
epoch: 1,
})
.unwrap_err();
assert!(early_resume
.to_string()
.contains("no settled frozen participant set"));
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportDebugState {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "worker".to_owned(),
task: "compile-linux".to_owned(),
epoch: 1,
state: DebugAcknowledgementState::Frozen,
stack_frames: vec!["compile_linux::wasm".to_owned()],
local_values: vec![("wasm_local_0".to_owned(), "I32(41)".to_owned())],
task_args: vec![("target".to_owned(), "linux".to_owned())],
handles: vec![("artifact".to_owned(), "pending".to_owned())],
command_status: Some("frozen at Wasm safepoint".to_owned()),
recent_output: vec![],
message: None,
})
.unwrap();
let CoordinatorResponse::DebugEpochStatus {
fully_frozen,
fully_resumed,
acknowledgements,
..
} = service
.handle_request(CoordinatorRequest::InspectDebugEpoch {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process".to_owned(),
epoch: 1,
})
.unwrap()
else {
panic!("expected frozen debug epoch status");
};
assert!(fully_frozen);
assert!(!fully_resumed);
assert_eq!(acknowledgements.len(), 1);
assert_eq!(acknowledgements[0].state, DebugAcknowledgementState::Frozen);
let CoordinatorResponse::DebugEpoch {
epoch,
command,
affected_tasks,
all_stop_requested,
audit_event,
..
} = service
.handle_request(CoordinatorRequest::ResumeDebugEpoch {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process".to_owned(),
epoch: 1,
})
.unwrap()
else {
panic!("expected debug epoch resume response");
};
assert_eq!(epoch, 1);
assert_eq!(command, "resume");
assert!(!all_stop_requested);
assert_eq!(affected_tasks.len(), 1);
assert_eq!(audit_event.operation, "resume_debug_epoch");
let CoordinatorResponse::DebugCommand {
epoch: Some(1),
command: Some(command),
..
} = service
.handle_signed_node_request_auto(CoordinatorRequest::PollDebugCommand {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "worker".to_owned(),
task: "compile-linux".to_owned(),
})
.unwrap()
else {
panic!("expected signed node poll to receive resume command");
};
assert_eq!(command, "resume");
let CoordinatorResponse::DebugEpochStatus { fully_resumed, .. } = service
.handle_request(CoordinatorRequest::InspectDebugEpoch {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process".to_owned(),
epoch: 1,
})
.unwrap()
else {
panic!("expected pending resume status");
};
assert!(!fully_resumed);
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportDebugState {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "worker".to_owned(),
task: "compile-linux".to_owned(),
epoch: 1,
state: DebugAcknowledgementState::Running,
stack_frames: vec![],
local_values: vec![],
task_args: vec![],
handles: vec![],
command_status: Some("running".to_owned()),
recent_output: vec![],
message: None,
})
.unwrap();
let CoordinatorResponse::DebugEpochStatus {
fully_frozen,
fully_resumed,
..
} = service
.handle_request(CoordinatorRequest::InspectDebugEpoch {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process".to_owned(),
epoch: 1,
})
.unwrap()
else {
panic!("expected resumed debug epoch status");
};
assert!(!fully_frozen);
assert!(fully_resumed);
}
#[test]
fn service_reports_task_restart_boundary_through_public_api() {
let mut service = CoordinatorService::new(7);
service
.handle_request(CoordinatorRequest::CreateProject {
tenant: "tenant".to_owned(),
actor_user: "user".to_owned(),
project: "project".to_owned(),
name: "Demo".to_owned(),
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process".to_owned(),
restart: false,
})
.unwrap();
let denied = service
.handle_request(CoordinatorRequest::RestartTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "other-user".to_owned(),
process: "process".to_owned(),
task: "task".to_owned(),
replacement_bundle: None,
})
.unwrap_err();
assert!(denied.to_string().contains("task restart denied"));
let CoordinatorResponse::TaskRestart {
accepted,
clean_boundary_available,
active_task,
completed_event_observed,
requires_whole_process_restart,
restarted_task_instance,
message,
audit_event,
charged_debug_read_bytes,
used_debug_read_bytes,
..
} = service
.handle_request(CoordinatorRequest::RestartTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process".to_owned(),
task: "task".to_owned(),
replacement_bundle: None,
})
.unwrap()
else {
panic!("expected task restart response");
};
assert!(!accepted);
assert!(!clean_boundary_available);
assert!(!active_task);
assert!(!completed_event_observed);
assert!(restarted_task_instance.is_none());
assert!(requires_whole_process_restart);
assert!(message.contains("not known"));
assert_eq!(audit_event.operation, "restart_task");
assert_eq!(audit_event.task, Some(TaskInstanceId::from("task")));
assert!(audit_event.allowed);
assert_eq!(charged_debug_read_bytes, DEBUG_CONTROL_READ_BYTES);
assert_eq!(used_debug_read_bytes, DEBUG_CONTROL_READ_BYTES);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker-linux".to_owned(),
public_key: test_node_public_key("worker-linux"),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker-linux".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: true,
online: true,
})
.unwrap();
service
.handle_authorized_test_task_launch(CoordinatorRequest::LaunchTask {
task_spec: test_task_spec(
"tenant",
"project",
"process",
"task",
7,
[Capability::Command],
),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: Some("user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
wait_for_node: false,
artifact_path: "/vfs/artifacts/output.txt".to_owned(),
wasm_module_base64: test_wasm_module_base64(),
})
.unwrap();
let CoordinatorResponse::TaskAssignment {
assignment: Some(initial_assignment),
} = service
.handle_signed_node_request_auto(CoordinatorRequest::PollTaskAssignment {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker-linux".to_owned(),
})
.unwrap()
else {
panic!("expected initial task assignment");
};
assert_eq!(initial_assignment.task, TaskInstanceId::from("task"));
let CoordinatorResponse::TaskRestart {
accepted,
clean_boundary_available,
active_task,
completed_event_observed,
requires_whole_process_restart,
message,
audit_event,
used_debug_read_bytes,
..
} = service
.handle_request(CoordinatorRequest::RestartTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process".to_owned(),
task: "task".to_owned(),
replacement_bundle: None,
})
.unwrap()
else {
panic!("expected active task restart response");
};
assert!(!accepted);
assert!(!clean_boundary_available);
assert!(active_task);
assert!(!completed_event_observed);
assert!(requires_whole_process_restart);
assert!(message.contains("still active"));
assert_eq!(
audit_event.charged_debug_read_bytes,
DEBUG_CONTROL_READ_BYTES
);
assert_eq!(used_debug_read_bytes, DEBUG_CONTROL_READ_BYTES * 2);
service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "worker-linux".to_owned(),
task: "task".to_owned(),
terminal_state: Some(TaskTerminalState::Completed),
status_code: Some(0),
stdout_bytes: 0,
stderr_bytes: 0,
stdout_tail: String::new(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
artifact_digest: None,
artifact_size_bytes: None,
result: None,
})
.unwrap();
let CoordinatorResponse::TaskRestart {
accepted,
clean_boundary_available,
active_task,
completed_event_observed,
requires_whole_process_restart,
restarted_task_instance,
restarted_attempt_id,
message,
audit_event,
used_debug_read_bytes,
..
} = service
.handle_request(CoordinatorRequest::RestartTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process".to_owned(),
task: "task".to_owned(),
replacement_bundle: None,
})
.unwrap()
else {
panic!("expected completed task restart response");
};
assert!(accepted);
assert!(clean_boundary_available);
assert!(!active_task);
assert!(completed_event_observed);
assert!(!requires_whole_process_restart);
let restarted_task_instance = restarted_task_instance.expect("restart returns logical id");
let restarted_attempt_id = restarted_attempt_id.expect("restart returns a new attempt id");
assert!(restarted_attempt_id.starts_with("ta_"));
assert!(message.contains("from clean VFS entry boundary epoch 7"));
let CoordinatorResponse::TaskAssignment {
assignment: Some(restarted_assignment),
} = service
.handle_signed_node_request_auto(CoordinatorRequest::PollTaskAssignment {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker-linux".to_owned(),
})
.unwrap()
else {
panic!("expected restarted task assignment");
};
assert_eq!(restarted_assignment.task, restarted_task_instance);
assert_eq!(restarted_assignment.task, TaskInstanceId::from("task"));
let mut expected_task_spec = initial_assignment.task_spec.clone();
expected_task_spec.task_instance = restarted_task_instance;
assert_eq!(restarted_assignment.task_spec, expected_task_spec);
assert_eq!(
restarted_assignment.wasm_module_base64,
initial_assignment.wasm_module_base64
);
assert_eq!(
audit_event.charged_debug_read_bytes,
DEBUG_CONTROL_READ_BYTES
);
assert_eq!(used_debug_read_bytes, DEBUG_CONTROL_READ_BYTES * 3);
assert_eq!(service.debug_audit_events.len(), 4);
}
#[test]
fn service_cancels_whole_process_and_blocks_new_task_launches() {
let mut service = CoordinatorService::new(7);
for node in ["node-a", "node-b"] {
let capabilities = if node == "node-a" {
linux_capabilities()
} else {
windows_capabilities()
};
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: node.to_owned(),
public_key: test_node_public_key(node),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: node.to_owned(),
capabilities,
cached_environment_digests: vec![],
dependency_cache_digests: vec![],
source_snapshots: vec![],
artifact_locations: vec![],
direct_connectivity: true,
online: true,
})
.unwrap();
}
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process".to_owned(),
restart: false,
})
.unwrap();
for node in ["node-a", "node-b"] {
service
.handle_signed_node_request_auto(CoordinatorRequest::ReconnectNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: node.to_owned(),
process: "process".to_owned(),
epoch: 7,
})
.unwrap();
}
for (task, required_capability, preferred_node) in [
("compile-linux", Capability::Containers, "node-a"),
("link-windows", Capability::WindowsCommandDev, "node-b"),
] {
let response = service
.handle_authorized_test_task_launch(CoordinatorRequest::LaunchTask {
task_spec: test_task_spec(
"tenant",
"project",
"process",
task,
7,
[required_capability],
),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: Some("user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
wait_for_node: false,
artifact_path: format!("/vfs/artifacts/{task}.txt"),
wasm_module_base64: test_wasm_module_base64(),
})
.unwrap();
let CoordinatorResponse::TaskLaunched { assignment, .. } = response else {
panic!("expected task launch");
};
assert_eq!(assignment.node, NodeId::from(preferred_node));
}
let CoordinatorResponse::ProcessCancellationRequested {
process,
cancelled_tasks,
affected_nodes,
} = service
.handle_request(CoordinatorRequest::CancelProcess {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process".to_owned(),
})
.unwrap()
else {
panic!("expected process cancellation");
};
assert_eq!(process, ProcessId::from("process"));
assert_eq!(cancelled_tasks.len(), 2);
assert_eq!(
affected_nodes,
vec![NodeId::from("node-a"), NodeId::from("node-b")]
);
let control = service
.handle_signed_node_request_auto(CoordinatorRequest::PollTaskControl {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "node-a".to_owned(),
task: "compile-linux".to_owned(),
})
.unwrap();
assert_eq!(
control,
CoordinatorResponse::TaskControl {
process: ProcessId::from("process"),
task: TaskInstanceId::from("compile-linux"),
cancel_requested: true,
abort_requested: false,
}
);
let blocked = service
.handle_authorized_test_task_launch(CoordinatorRequest::LaunchTask {
task_spec: test_task_spec(
"tenant",
"project",
"process",
"package-linux",
7,
[Capability::Command],
),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: Some("user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
wait_for_node: false,
artifact_path: "/vfs/artifacts/package-linux.txt".to_owned(),
wasm_module_base64: test_wasm_module_base64(),
})
.unwrap_err();
assert!(blocked
.to_string()
.contains("virtual process is cancelling"));
service
.handle_request(CoordinatorRequest::AbortProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process".to_owned(),
})
.unwrap();
let abort_control = service
.handle_signed_node_request_auto(CoordinatorRequest::PollTaskControl {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "node-a".to_owned(),
task: "compile-linux".to_owned(),
})
.unwrap();
assert_eq!(
abort_control,
CoordinatorResponse::TaskControl {
process: ProcessId::from("process"),
task: TaskInstanceId::from("compile-linux"),
cancel_requested: false,
abort_requested: true,
}
);
}
#[test]
fn service_rejects_second_active_process_unless_restarting_same_process() {
let mut service = CoordinatorService::new(7);
let started = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process-a".to_owned(),
restart: false,
})
.unwrap();
assert!(matches!(
started,
CoordinatorResponse::ProcessStarted {
launch_attempt: None,
..
}
));
let same_without_restart = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process-a".to_owned(),
restart: false,
})
.unwrap_err();
assert!(same_without_restart
.to_string()
.contains("already has active virtual process"));
let other_process = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process-b".to_owned(),
restart: true,
})
.unwrap_err();
assert!(other_process
.to_string()
.contains("already has active virtual process"));
let wrong_attempt_abort = service
.handle_request(CoordinatorRequest::AbortProcess {
launch_attempt: Some("attempt-b".to_owned()),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process-a".to_owned(),
})
.unwrap_err();
assert!(wrong_attempt_abort
.to_string()
.contains("does not own process process-a"));
assert!(service
.coordinator
.active_process(
&TenantId::from("tenant"),
&ProjectId::from("project"),
&ProcessId::from("process-a")
)
.is_some());
let retired_main_abort = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let process_key = process_control_key(
&TenantId::from("tenant"),
&ProjectId::from("project"),
&ProcessId::from("process-a"),
);
service.main_runtime.controls.insert(
process_key.clone(),
super::main_runtime::CoordinatorMainControl {
task_definition: clusterflux_core::TaskDefinitionId::from("build"),
task_instance: TaskInstanceId::from("ti:process-a:main"),
abort: std::sync::Arc::clone(&retired_main_abort),
debug: std::sync::Arc::new(clusterflux_wasm_runtime::WasmDebugControl::default()),
state: "running".to_owned(),
stopped_probe_symbol: None,
handles: std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
launch_id: 1,
},
);
let restarted = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: Some("attempt-a-restart".to_owned()),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process-a".to_owned(),
restart: true,
})
.unwrap();
assert_eq!(
restarted,
CoordinatorResponse::ProcessStarted {
launch_attempt: Some("attempt-a-restart".to_owned()),
process: ProcessId::from("process-a"),
epoch: 7,
actor: WorkflowActor {
kind: "user".to_owned(),
user: Some(UserId::from("user")),
agent: None,
credential_kind: CredentialKind::BrowserSession,
public_key_fingerprint: None,
authenticated_without_browser: false,
scopes: vec!["project:read".to_owned(), "project:run".to_owned()],
},
charged_spawns: 2,
}
);
assert!(retired_main_abort.load(std::sync::atomic::Ordering::Acquire));
assert!(!service.main_runtime.controls.contains_key(&process_key));
}
#[test]
fn completed_main_waits_for_final_child_then_preserves_history_and_releases_the_slot() {
let mut service = CoordinatorService::new(31);
let tenant = TenantId::from("tenant");
let project = ProjectId::from("project");
let process = ProcessId::from("process-main-before-child");
let child = TaskInstanceId::from("child-active");
let process_key = process_control_key(&tenant, &project, &process);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: tenant.to_string(),
project: project.to_string(),
node: "worker".to_owned(),
public_key: test_node_public_key("worker"),
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: tenant.to_string(),
project: project.to_string(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: process.to_string(),
restart: false,
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReconnectNode {
tenant: tenant.to_string(),
project: project.to_string(),
node: "worker".to_owned(),
process: process.to_string(),
epoch: 31,
})
.unwrap();
register_test_task_assignment(
&mut service,
tenant.as_str(),
project.as_str(),
process.as_str(),
"worker",
"child-definition",
child.as_str(),
31,
);
let main = TaskInstanceId::from("main-instance");
service.main_runtime.controls.insert(
process_key.clone(),
super::main_runtime::CoordinatorMainControl {
task_definition: TaskDefinitionId::from("build"),
task_instance: main.clone(),
abort: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
debug: std::sync::Arc::new(clusterflux_wasm_runtime::WasmDebugControl::default()),
state: "running".to_owned(),
stopped_probe_symbol: None,
handles: std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
launch_id: 1,
},
);
service.debug_epochs.insert(process_key.clone(), 9);
service.record_coordinator_main_completion(
super::main_runtime::MainScope {
tenant: tenant.clone(),
project: project.clone(),
process: process.clone(),
task_definition: TaskDefinitionId::from("build"),
task_instance: main,
epoch: 31,
launch_id: 1,
},
Ok(WasmTaskResult::completed(
TaskInstanceId::from("main-instance"),
TaskBoundaryValue::SmallJson(json!("main completed")),
)),
);
assert!(service
.coordinator
.active_process(&tenant, &project, &process)
.is_some());
assert!(service.debug_epochs.contains_key(&process_key));
assert!(service
.active_tasks
.iter()
.any(|(_, _, retained_process, _, task)| {
retained_process == &process && task == &child
}));
let blocked_next = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: tenant.to_string(),
project: project.to_string(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process-too-early".to_owned(),
restart: false,
})
.unwrap_err();
assert!(blocked_next
.to_string()
.contains("already has active virtual process"));
let artifact_bytes = b"child artifact survives terminal cleanup";
service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: tenant.to_string(),
project: project.to_string(),
process: process.to_string(),
node: "worker".to_owned(),
task: child.to_string(),
terminal_state: Some(TaskTerminalState::Completed),
status_code: Some(0),
stdout_bytes: artifact_bytes.len() as u64,
stderr_bytes: 0,
stdout_tail: "child completed".to_owned(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: Some("/vfs/artifacts/child-output".to_owned()),
artifact_digest: Some(Digest::sha256(artifact_bytes)),
artifact_size_bytes: Some(artifact_bytes.len() as u64),
result: Some(TaskBoundaryValue::SmallJson(json!("child completed"))),
})
.unwrap();
assert!(service
.coordinator
.active_process(&tenant, &project, &process)
.is_none());
assert!(!service.debug_epochs.contains_key(&process_key));
let CoordinatorResponse::TaskEvents { events } = service
.handle_request(CoordinatorRequest::ListTaskEvents {
tenant: tenant.to_string(),
project: project.to_string(),
actor_user: "user".to_owned(),
process: Some(process.to_string()),
})
.unwrap()
else {
panic!("expected retained task events");
};
assert_eq!(events.len(), 2);
assert!(events.iter().any(|event| {
event.executor == TaskExecutor::CoordinatorMain
&& event.terminal_state == TaskTerminalState::Completed
}));
assert!(events.iter().any(|event| {
event.task == child && event.artifact_digest == Some(Digest::sha256(artifact_bytes))
}));
let metadata = service
.artifact_registry
.metadata(&tenant, &project, &ArtifactId::from("child-output"))
.expect("artifact metadata must survive terminal cleanup");
assert_eq!(metadata.process, process);
assert_eq!(metadata.digest, Digest::sha256(artifact_bytes));
let next = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: tenant.to_string(),
project: project.to_string(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process-after-cleanup".to_owned(),
restart: false,
})
.unwrap();
assert!(matches!(next, CoordinatorResponse::ProcessStarted { .. }));
}
#[test]
fn completed_main_terminal_matrix_retires_after_failed_or_cancelled_final_child() {
for terminal_state in [TaskTerminalState::Failed, TaskTerminalState::Cancelled] {
let mut service = service_with_completed_main_and_final_child(
clusterflux_core::TaskFailurePolicy::FailFast,
);
complete_terminal_matrix_child(&mut service, terminal_state.clone());
let tenant = TenantId::from("tenant");
let project = ProjectId::from("project");
let process = ProcessId::from("terminal-matrix");
assert!(
service
.coordinator
.active_process(&tenant, &project, &process)
.is_none(),
"{terminal_state:?} final child left the process slot active"
);
assert!(!service
.debug_epochs
.contains_key(&process_control_key(&tenant, &project, &process)));
assert!(!service
.debug_breakpoints
.contains_key(&process_control_key(&tenant, &project, &process)));
assert!(service.debug_commands.keys().all(
|(task_tenant, task_project, task_process, _, _)| {
task_tenant != &tenant || task_project != &project || task_process != &process
}
));
assert!(!service
.panel_snapshots
.contains_key(&super::keys::panel_stop_key(&tenant, &project, &process)));
assert!(service.active_tasks.iter().all(
|(task_tenant, task_project, task_process, _, _)| {
task_tenant != &tenant || task_project != &project || task_process != &process
}
));
assert!(!service
.main_runtime
.controls
.contains_key(&process_control_key(&tenant, &project, &process)));
let join = service.task_join_result(
tenant.clone(),
project.clone(),
process.clone(),
TaskInstanceId::from("final-child"),
);
assert_eq!(
join.state,
match terminal_state {
TaskTerminalState::Failed => TaskJoinState::Failed,
TaskTerminalState::Cancelled => TaskJoinState::Cancelled,
TaskTerminalState::Completed => unreachable!(),
}
);
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: tenant.to_string(),
project: project.to_string(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "next-after-terminal".to_owned(),
restart: false,
})
.expect("the terminal outcome must release the one-process project slot");
}
}
#[test]
fn completed_main_unpolled_final_assignment_completion_retires_process() {
let mut service =
service_with_completed_main_and_final_child(clusterflux_core::TaskFailurePolicy::FailFast);
let tenant = TenantId::from("tenant");
let project = ProjectId::from("project");
let process = ProcessId::from("terminal-matrix");
let node = NodeId::from("worker");
let assignment_key = (tenant.clone(), project.clone(), node);
assert_eq!(
service
.task_assignments
.get(&assignment_key)
.map_or(0, VecDeque::len),
1
);
service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: tenant.to_string(),
project: project.to_string(),
process: process.to_string(),
node: "worker".to_owned(),
task: "final-child".to_owned(),
terminal_state: Some(TaskTerminalState::Completed),
status_code: Some(0),
stdout_bytes: 2,
stderr_bytes: 0,
stdout_tail: "42".to_owned(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
artifact_digest: None,
artifact_size_bytes: None,
result: Some(TaskBoundaryValue::SmallJson(json!(42))),
})
.unwrap();
assert!(service
.coordinator
.active_process(&tenant, &project, &process)
.is_none());
assert!(!service.task_assignments.contains_key(&assignment_key));
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: tenant.to_string(),
project: project.to_string(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "next-after-unpolled-completion".to_owned(),
restart: false,
})
.expect("unpolled terminal completion must release the one-process slot");
}
#[test]
fn completed_main_retires_from_authoritative_state_after_event_history_rotates() {
let mut service =
service_with_completed_main_and_final_child(clusterflux_core::TaskFailurePolicy::FailFast);
let tenant = TenantId::from("tenant");
let project = ProjectId::from("project");
let process = ProcessId::from("terminal-matrix");
for index in 0..=MAX_TASK_EVENTS_PER_PROCESS {
service.record_task_completion_event(TaskCompletionEvent {
tenant: tenant.clone(),
project: project.clone(),
process: process.clone(),
node: NodeId::from("worker"),
executor: TaskExecutor::Node,
task_definition: TaskDefinitionId::from("historical"),
task: TaskInstanceId::new(format!("historical-{index}")),
attempt_id: None,
placement: None,
terminal_state: TaskTerminalState::Completed,
status_code: Some(0),
stdout_bytes: 0,
stderr_bytes: 0,
stdout_tail: String::new(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
artifact_digest: None,
artifact_size_bytes: None,
result: None,
});
}
assert!(
service.task_events.iter().all(|event| {
event.process != process || event.executor != TaskExecutor::CoordinatorMain
}),
"the regression requires bounded history to have rotated the main event"
);
complete_terminal_matrix_child(&mut service, TaskTerminalState::Completed);
assert!(service
.coordinator
.active_process(&tenant, &project, &process)
.is_none());
let summary = service
.process_summaries
.get(&process_control_key(&tenant, &project, &process))
.expect("the terminal process summary must remain authoritative");
assert_eq!(summary.final_result, Some(ProcessFinalResult::Completed));
assert_eq!(
summary.main_terminal_state,
Some(TaskTerminalState::Completed)
);
}
#[test]
fn completed_main_await_operator_blocks_retirement_until_each_resolution() {
for resolution in [
TaskFailureResolution::AcceptFailure,
TaskFailureResolution::Cancel,
] {
let mut service = service_with_completed_main_and_final_child(
clusterflux_core::TaskFailurePolicy::AwaitOperator,
);
complete_terminal_matrix_child(&mut service, TaskTerminalState::Failed);
let tenant = TenantId::from("tenant");
let project = ProjectId::from("project");
let process = ProcessId::from("terminal-matrix");
let process_key = process_control_key(&tenant, &project, &process);
assert!(service
.coordinator
.active_process(&tenant, &project, &process)
.is_some());
assert!(service.debug_epochs.contains_key(&process_key));
assert!(service.debug_breakpoints.contains_key(&process_key));
assert!(service
.panel_snapshots
.contains_key(&super::keys::panel_stop_key(&tenant, &project, &process)));
let attempt = service
.task_attempts
.get(&super::keys::task_restart_key(
&tenant,
&project,
&process,
&TaskInstanceId::from("final-child"),
))
.and_then(|attempts| attempts.iter().rev().find(|attempt| attempt.current))
.unwrap();
assert_eq!(attempt.state, TaskAttemptState::FailedAwaitingAction);
service
.handle_request(CoordinatorRequest::ResolveTaskFailure {
tenant: tenant.to_string(),
project: project.to_string(),
actor_user: "user".to_owned(),
process: process.to_string(),
task: "final-child".to_owned(),
resolution,
})
.unwrap();
assert!(
service
.coordinator
.active_process(&tenant, &project, &process)
.is_none(),
"{resolution:?} left the process slot active"
);
assert!(!service.debug_epochs.contains_key(&process_key));
assert!(!service.debug_breakpoints.contains_key(&process_key));
assert!(!service
.panel_snapshots
.contains_key(&super::keys::panel_stop_key(&tenant, &project, &process)));
assert!(service.debug_commands.keys().all(
|(task_tenant, task_project, task_process, _, _)| {
task_tenant != &tenant || task_project != &project || task_process != &process
}
));
let join = service.task_join_result(
tenant.clone(),
project.clone(),
process.clone(),
TaskInstanceId::from("final-child"),
);
assert_eq!(
join.state,
match resolution {
TaskFailureResolution::AcceptFailure => TaskJoinState::Failed,
TaskFailureResolution::Cancel => TaskJoinState::Cancelled,
}
);
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: tenant.to_string(),
project: project.to_string(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "next-after-resolution".to_owned(),
restart: false,
})
.expect("operator resolution must release the one-process project slot");
}
}
#[test]
fn completed_main_failed_child_restarted_successfully_retires_with_successful_current_attempt() {
let mut service = service_with_completed_main_and_final_child(
clusterflux_core::TaskFailurePolicy::AwaitOperator,
);
complete_terminal_matrix_child(&mut service, TaskTerminalState::Failed);
let tenant = TenantId::from("tenant");
let project = ProjectId::from("project");
let process = ProcessId::from("terminal-matrix");
let task = TaskInstanceId::from("final-child");
let CoordinatorResponse::TaskRestart { accepted, .. } = service
.handle_request(CoordinatorRequest::RestartTask {
tenant: tenant.to_string(),
project: project.to_string(),
actor_user: "user".to_owned(),
process: process.to_string(),
task: task.to_string(),
replacement_bundle: None,
})
.unwrap()
else {
panic!("expected task restart");
};
assert!(accepted);
service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: tenant.to_string(),
project: project.to_string(),
process: process.to_string(),
node: "worker".to_owned(),
task: task.to_string(),
terminal_state: Some(TaskTerminalState::Completed),
status_code: Some(0),
stdout_bytes: 2,
stderr_bytes: 0,
stdout_tail: "ok".to_owned(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
artifact_digest: None,
artifact_size_bytes: None,
result: Some(TaskBoundaryValue::SmallJson(json!("ok"))),
})
.unwrap();
assert!(service
.coordinator
.active_process(&tenant, &project, &process)
.is_none());
let attempts = service
.task_attempts
.get(&super::keys::task_restart_key(
&tenant, &project, &process, &task,
))
.unwrap();
assert!(
attempts
.iter()
.any(|attempt| !attempt.current
&& attempt.state == TaskAttemptState::FailedAwaitingAction)
);
assert!(attempts
.iter()
.any(|attempt| attempt.current && attempt.state == TaskAttemptState::Completed));
assert_eq!(
service
.process_summaries
.get(&process_control_key(&tenant, &project, &process))
.and_then(|summary| summary.final_result.clone()),
Some(ProcessFinalResult::Completed),
"a successful current retry must override the superseded failed attempt"
);
assert_eq!(
service
.task_join_result(tenant, project, process, task)
.state,
TaskJoinState::Completed
);
}
#[test]
fn completed_main_failed_child_does_not_abort_another_active_child() {
let mut service =
service_with_completed_main_and_final_child(clusterflux_core::TaskFailurePolicy::FailFast);
register_test_task_assignment(
&mut service,
"tenant",
"project",
"terminal-matrix",
"worker",
"other-child-definition",
"other-child",
83,
);
complete_terminal_matrix_child(&mut service, TaskTerminalState::Failed);
let tenant = TenantId::from("tenant");
let project = ProjectId::from("project");
let process = ProcessId::from("terminal-matrix");
let other_key = task_control_key(
&tenant,
&project,
&process,
&NodeId::from("worker"),
&TaskInstanceId::from("other-child"),
);
assert!(service
.coordinator
.active_process(&tenant, &project, &process)
.is_some());
assert!(service.active_tasks.contains(&other_key));
assert!(!service.task_aborts.contains(&other_key));
assert_eq!(
service
.task_join_result(
tenant.clone(),
project.clone(),
process.clone(),
TaskInstanceId::from("final-child"),
)
.state,
TaskJoinState::Failed
);
service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: tenant.to_string(),
project: project.to_string(),
process: process.to_string(),
node: "worker".to_owned(),
task: "other-child".to_owned(),
terminal_state: Some(TaskTerminalState::Completed),
status_code: Some(0),
stdout_bytes: 0,
stderr_bytes: 0,
stdout_tail: String::new(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
artifact_digest: None,
artifact_size_bytes: None,
result: None,
})
.unwrap();
assert!(service
.coordinator
.active_process(&tenant, &project, &process)
.is_none());
assert!(!service.active_tasks.contains(&other_key));
}
#[test]
fn quiescent_cooperative_cancel_releases_slot_immediately() {
let mut service = CoordinatorService::new(17);
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process-a".to_owned(),
restart: false,
})
.unwrap();
service.record_task_completion_event(TaskCompletionEvent {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
process: ProcessId::from("process-a"),
node: NodeId::from("node"),
executor: TaskExecutor::Node,
task_definition: clusterflux_core::TaskDefinitionId::from("old-task"),
task: TaskInstanceId::from("ti:process-a:old"),
attempt_id: None,
placement: None,
terminal_state: TaskTerminalState::Completed,
status_code: Some(0),
stdout_bytes: 0,
stderr_bytes: 0,
stdout_tail: String::new(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
artifact_digest: None,
artifact_size_bytes: None,
result: Some(TaskBoundaryValue::SmallJson(json!("old"))),
});
service
.record_debug_audit_event(
TenantId::from("tenant"),
ProjectId::from("project"),
ProcessId::from("process-a"),
None,
UserId::from("user"),
"old_debug_event",
false,
"old process incarnation",
)
.unwrap();
service
.handle_request(CoordinatorRequest::CancelProcess {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process-a".to_owned(),
})
.unwrap();
let CoordinatorResponse::ProcessStatuses { processes, .. } = service
.handle_request(CoordinatorRequest::ListProcesses {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
})
.unwrap()
else {
panic!("expected live process statuses");
};
assert!(processes.is_empty());
let started = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process-a".to_owned(),
restart: false,
})
.unwrap();
assert!(matches!(
started,
CoordinatorResponse::ProcessStarted {
launch_attempt: None,
..
}
));
assert!(service.task_events.iter().all(|event| {
event.tenant != TenantId::from("tenant")
|| event.project != ProjectId::from("project")
|| event.process != ProcessId::from("process-a")
}));
assert!(service.debug_audit_events.iter().all(|event| {
event.tenant != TenantId::from("tenant")
|| event.project != ProjectId::from("project")
|| event.process != ProcessId::from("process-a")
}));
}
#[test]
fn aborted_process_accepts_signed_terminal_event_for_issued_task() {
let mut service = CoordinatorService::new(17);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
public_key: test_node_public_key("node"),
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process".to_owned(),
restart: false,
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReconnectNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
process: "process".to_owned(),
epoch: 17,
})
.unwrap();
register_test_task_assignment(
&mut service,
"tenant",
"project",
"process",
"node",
"compile",
"compile-1",
17,
);
let CoordinatorResponse::ProcessAborted { aborted_tasks, .. } = service
.handle_request(CoordinatorRequest::AbortProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "process".to_owned(),
})
.unwrap()
else {
panic!("expected process abort");
};
assert_eq!(aborted_tasks.len(), 1);
let recorded = service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "node".to_owned(),
task: "compile-1".to_owned(),
terminal_state: Some(TaskTerminalState::Cancelled),
status_code: None,
stdout_bytes: 0,
stderr_bytes: 0,
stdout_tail: String::new(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
artifact_digest: None,
artifact_size_bytes: None,
result: None,
})
.unwrap();
assert_eq!(
recorded,
CoordinatorResponse::TaskRecorded {
process: ProcessId::from("process"),
task: TaskInstanceId::from("compile-1"),
events_recorded: 1,
}
);
assert_eq!(
service.task_events[0].task_definition,
clusterflux_core::TaskDefinitionId::from("compile")
);
assert!(service.task_restart_checkpoints.is_empty());
}
#[test]
fn service_download_links_are_scoped_and_streaming_is_metered() {
let mut service = CoordinatorService::new(7);
let artifact_bytes = b"0123456789abcdef0123456789abcdef";
service.set_server_time(10);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
public_key: test_node_public_key("node"),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: false,
online: true,
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process".to_owned(),
restart: false,
})
.unwrap();
register_test_task_assignment(
&mut service,
"tenant",
"project",
"process",
"node",
"compile-linux",
"compile-linux",
7,
);
service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "node".to_owned(),
task: "compile-linux".to_owned(),
terminal_state: None,
status_code: Some(0),
stdout_bytes: 32,
stderr_bytes: 0,
stdout_tail: "artifact bytes".to_owned(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: Some("/vfs/artifacts/app.txt".to_owned()),
artifact_digest: Some(Digest::sha256(artifact_bytes)),
artifact_size_bytes: Some(artifact_bytes.len() as u64),
result: None,
})
.unwrap();
service.quota.set_download_limits(ResourceLimits {
limits: BTreeMap::from([(LimitKind::ArtifactDownloadBytes, 31)]),
});
let quota_denied_link = service
.handle_request(CoordinatorRequest::CreateArtifactDownloadLink {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 64,
ttl_seconds: 60,
})
.unwrap_err();
assert!(quota_denied_link
.to_string()
.contains("ArtifactDownloadBytes"));
service.quota.set_download_limits(ResourceLimits {
limits: BTreeMap::from([(LimitKind::ArtifactDownloadBytes, 64)]),
});
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: vec!["app.txt".to_owned()],
direct_connectivity: false,
online: true,
})
.unwrap();
let private_node_link = service
.handle_request(CoordinatorRequest::CreateArtifactDownloadLink {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 64,
ttl_seconds: 60,
})
.unwrap();
assert!(matches!(
private_node_link,
CoordinatorResponse::ArtifactDownloadLink { .. }
));
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: vec!["app.txt".to_owned()],
direct_connectivity: true,
online: true,
})
.unwrap();
let CoordinatorResponse::ArtifactDownloadLink { link } = service
.handle_request(CoordinatorRequest::CreateArtifactDownloadLink {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 64,
ttl_seconds: 60,
})
.unwrap()
else {
panic!("expected download link");
};
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: vec!["app.txt".to_owned()],
direct_connectivity: false,
online: true,
})
.unwrap();
let private_node_open = service
.handle_request(CoordinatorRequest::OpenArtifactDownloadStream {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 64,
token_digest: link.scoped_token_digest.clone(),
chunk_bytes: 1,
})
.unwrap();
assert!(matches!(
private_node_open,
CoordinatorResponse::ArtifactDownloadStream {
content_bytes_available: false,
..
}
));
let CoordinatorResponse::ArtifactTransferAssignment {
transfer: Some(pending_transfer),
} = service
.handle_signed_node_request_auto(CoordinatorRequest::PollArtifactTransfer {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
})
.unwrap()
else {
panic!("expected reverse transfer assignment");
};
let bad_chunk = service
.handle_signed_node_request_auto(CoordinatorRequest::UploadArtifactTransferChunk {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
transfer_id: pending_transfer.transfer_id,
artifact: pending_transfer.artifact.as_str().to_owned(),
offset: 0,
content_base64: BASE64_STANDARD.encode(artifact_bytes),
chunk_digest: Digest::sha256("wrong bytes"),
eof: true,
})
.unwrap_err();
assert!(bad_chunk.to_string().contains("chunk digest mismatch"));
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: vec!["app.txt".to_owned()],
direct_connectivity: true,
online: true,
})
.unwrap();
assert_eq!(link.tenant, TenantId::from("tenant"));
assert_eq!(link.project, ProjectId::from("project"));
assert_eq!(link.process, ProcessId::from("process"));
assert_eq!(link.actor, Actor::User(UserId::from("user")));
assert_eq!(link.max_bytes, 64);
assert!(link.policy_context_digest.is_valid_sha256());
assert_eq!(link.expires_at_epoch_seconds, 70);
assert!(link.url_path.contains("/artifacts/tenant/project/process"));
let cross_tenant = service
.handle_request(CoordinatorRequest::CreateArtifactDownloadLink {
tenant: "other".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 64,
ttl_seconds: 60,
})
.unwrap_err();
assert!(cross_tenant.to_string().contains("does not exist"));
let cross_project = service
.handle_request(CoordinatorRequest::CreateArtifactDownloadLink {
tenant: "tenant".to_owned(),
project: "other-project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 64,
ttl_seconds: 60,
})
.unwrap_err();
assert!(cross_project.to_string().contains("does not exist"));
let cross_tenant_open = service
.handle_request(CoordinatorRequest::OpenArtifactDownloadStream {
tenant: "other".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 64,
token_digest: link.scoped_token_digest.clone(),
chunk_bytes: 1,
})
.unwrap_err();
assert!(cross_tenant_open.to_string().contains("does not exist"));
let cross_project_open = service
.handle_request(CoordinatorRequest::OpenArtifactDownloadStream {
tenant: "tenant".to_owned(),
project: "other-project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 64,
token_digest: link.scoped_token_digest.clone(),
chunk_bytes: 1,
})
.unwrap_err();
assert!(cross_project_open.to_string().contains("does not exist"));
let guessed = service
.handle_request(CoordinatorRequest::OpenArtifactDownloadStream {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 64,
token_digest: Digest::sha256("guessed"),
chunk_bytes: 1,
})
.unwrap_err();
assert!(guessed.to_string().contains("token is invalid"));
let cross_actor_open = service
.handle_request(CoordinatorRequest::OpenArtifactDownloadStream {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "other-user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 64,
token_digest: link.scoped_token_digest.clone(),
chunk_bytes: 1,
})
.unwrap_err();
assert!(cross_actor_open.to_string().contains("token is invalid"));
service.set_server_time(11);
upload_pending_artifact_transfer(&mut service, "node", artifact_bytes);
let CoordinatorResponse::ArtifactDownloadStream {
streamed_bytes,
charged_download_bytes,
content_base64: Some(content_base64),
content_source: Some(content_source),
..
} = service
.handle_request(CoordinatorRequest::OpenArtifactDownloadStream {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 64,
token_digest: link.scoped_token_digest.clone(),
chunk_bytes: 16,
})
.unwrap()
else {
panic!("expected download stream");
};
assert_eq!(streamed_bytes, 16);
assert_eq!(charged_download_bytes, 16);
assert_eq!(
BASE64_STANDARD.decode(content_base64).unwrap(),
&artifact_bytes[..16]
);
assert_eq!(content_source, "retaining_node_reverse_stream");
let CoordinatorResponse::ArtifactDownloadLink {
link: expiring_link,
} = service
.handle_request(CoordinatorRequest::CreateArtifactDownloadLink {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 64,
ttl_seconds: 1,
})
.unwrap()
else {
panic!("expected expiring download link");
};
service.set_server_time(13);
let expired = service
.handle_request(CoordinatorRequest::OpenArtifactDownloadStream {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 64,
token_digest: expiring_link.scoped_token_digest,
chunk_bytes: 1,
})
.unwrap_err();
assert!(expired.to_string().contains("expired"));
service.set_server_time(11);
let cross_actor_revoke = service
.handle_request(CoordinatorRequest::RevokeArtifactDownloadLink {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "other-user".to_owned(),
artifact: "app.txt".to_owned(),
token_digest: link.scoped_token_digest.clone(),
})
.unwrap_err();
assert!(cross_actor_revoke.to_string().contains("token is invalid"));
let CoordinatorResponse::ArtifactDownloadLinkRevoked { link: revoked } = service
.handle_request(CoordinatorRequest::RevokeArtifactDownloadLink {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
token_digest: link.scoped_token_digest.clone(),
})
.unwrap()
else {
panic!("expected revoked download link");
};
assert_eq!(revoked.scoped_token_digest, link.scoped_token_digest);
let revoked = service
.handle_request(CoordinatorRequest::OpenArtifactDownloadStream {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 64,
token_digest: link.scoped_token_digest,
chunk_bytes: 1,
})
.unwrap_err();
assert!(revoked.to_string().contains("revoked"));
}
#[test]
fn download_quota_cannot_be_reset_by_reconnect_retry_or_scope_switch() {
let mut service = CoordinatorService::new(7);
let artifact_bytes = b"0123456789abcdef";
service.quota.set_download_limits(ResourceLimits {
limits: BTreeMap::from([(LimitKind::ArtifactDownloadBytes, 16)]),
});
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
public_key: test_node_public_key("node"),
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process".to_owned(),
restart: false,
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: true,
online: true,
})
.unwrap();
register_test_task_assignment(
&mut service,
"tenant",
"project",
"process",
"node",
"compile-linux",
"compile-linux",
7,
);
service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "node".to_owned(),
task: "compile-linux".to_owned(),
terminal_state: None,
status_code: Some(0),
stdout_bytes: 16,
stderr_bytes: 0,
stdout_tail: "artifact bytes".to_owned(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: Some("/vfs/artifacts/app.txt".to_owned()),
artifact_digest: Some(Digest::sha256(artifact_bytes)),
artifact_size_bytes: Some(artifact_bytes.len() as u64),
result: None,
})
.unwrap();
let CoordinatorResponse::ArtifactDownloadLink { link } = service
.handle_request(CoordinatorRequest::CreateArtifactDownloadLink {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 16,
ttl_seconds: 60,
})
.unwrap()
else {
panic!("expected initial download link");
};
let pending = service
.handle_request(CoordinatorRequest::OpenArtifactDownloadStream {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 16,
token_digest: link.scoped_token_digest.clone(),
chunk_bytes: 16,
})
.unwrap();
assert!(matches!(
pending,
CoordinatorResponse::ArtifactDownloadStream {
content_bytes_available: false,
..
}
));
upload_pending_artifact_transfer(&mut service, "node", artifact_bytes);
let CoordinatorResponse::ArtifactDownloadStream {
charged_download_bytes,
..
} = service
.handle_request(CoordinatorRequest::OpenArtifactDownloadStream {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 16,
token_digest: link.scoped_token_digest.clone(),
chunk_bytes: 16,
})
.unwrap()
else {
panic!("expected initial download stream");
};
assert_eq!(charged_download_bytes, 16);
service
.handle_signed_node_request_auto(CoordinatorRequest::ReconnectNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
process: "process".to_owned(),
epoch: 7,
})
.unwrap();
let after_reconnect = service
.handle_request(CoordinatorRequest::CreateArtifactDownloadLink {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 16,
ttl_seconds: 60,
})
.unwrap_err();
assert!(after_reconnect
.to_string()
.contains("ArtifactDownloadBytes"));
let restarted_cli_retry = service
.handle_request(CoordinatorRequest::CreateArtifactDownloadLink {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 16,
ttl_seconds: 60,
})
.unwrap_err();
assert!(restarted_cli_retry
.to_string()
.contains("ArtifactDownloadBytes"));
let cross_tenant_retry = service
.handle_request(CoordinatorRequest::OpenArtifactDownloadStream {
tenant: "other-tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 16,
token_digest: link.scoped_token_digest.clone(),
chunk_bytes: 1,
})
.unwrap_err();
assert!(cross_tenant_retry.to_string().contains("does not exist"));
let cross_project_retry = service
.handle_request(CoordinatorRequest::OpenArtifactDownloadStream {
tenant: "tenant".to_owned(),
project: "other-project".to_owned(),
actor_user: "user".to_owned(),
artifact: "app.txt".to_owned(),
max_bytes: 16,
token_digest: link.scoped_token_digest,
chunk_bytes: 1,
})
.unwrap_err();
assert!(cross_project_retry.to_string().contains("does not exist"));
}
#[test]
fn retained_artifact_reverse_stream_is_chunked_below_control_frame_limit() {
let mut service = CoordinatorService::new(7);
let artifact_bytes = vec![0x5a; 600_000];
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
public_key: test_node_public_key("node"),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: false,
online: true,
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process".to_owned(),
restart: false,
})
.unwrap();
register_test_task_assignment(
&mut service,
"tenant",
"project",
"process",
"node",
"package",
"package",
7,
);
service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "node".to_owned(),
task: "package".to_owned(),
terminal_state: None,
status_code: Some(0),
stdout_bytes: 0,
stderr_bytes: 0,
stdout_tail: String::new(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: Some("/vfs/artifacts/large.bin".to_owned()),
artifact_digest: Some(Digest::sha256(&artifact_bytes)),
artifact_size_bytes: Some(artifact_bytes.len() as u64),
result: Some(TaskBoundaryValue::Artifact(ArtifactHandle {
id: ArtifactId::from("large.bin"),
digest: Digest::sha256(&artifact_bytes),
size_bytes: artifact_bytes.len() as u64,
})),
})
.unwrap();
let CoordinatorResponse::ArtifactDownloadLink { link } = service
.handle_request(CoordinatorRequest::CreateArtifactDownloadLink {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "large.bin".to_owned(),
max_bytes: artifact_bytes.len() as u64,
ttl_seconds: 60,
})
.unwrap()
else {
panic!("expected download link");
};
let pending = service
.handle_request(CoordinatorRequest::OpenArtifactDownloadStream {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "large.bin".to_owned(),
max_bytes: artifact_bytes.len() as u64,
token_digest: link.scoped_token_digest.clone(),
chunk_bytes: artifact_bytes.len() as u64,
})
.unwrap();
assert!(matches!(
pending,
CoordinatorResponse::ArtifactDownloadStream {
content_bytes_available: false,
..
}
));
upload_pending_artifact_transfer(&mut service, "node", &artifact_bytes);
let mut downloaded = Vec::new();
loop {
let CoordinatorResponse::ArtifactDownloadStream {
streamed_bytes,
content_offset: Some(offset),
content_eof,
content_base64: Some(content),
..
} = service
.handle_request(CoordinatorRequest::OpenArtifactDownloadStream {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
artifact: "large.bin".to_owned(),
max_bytes: artifact_bytes.len() as u64,
token_digest: link.scoped_token_digest.clone(),
chunk_bytes: artifact_bytes.len() as u64,
})
.unwrap()
else {
panic!("expected verified reverse-stream content chunk");
};
assert_eq!(offset, downloaded.len() as u64);
assert!(streamed_bytes <= super::artifacts::MAX_ARTIFACT_REVERSE_CHUNK_BYTES);
downloaded.extend(BASE64_STANDARD.decode(content).unwrap());
if content_eof {
break;
}
}
assert_eq!(downloaded, artifact_bytes);
assert_eq!(
service.quota.used_download_bytes(
&TenantId::from("tenant"),
&ProjectId::from("project"),
service.current_epoch_seconds().unwrap(),
),
600_000
);
}
#[test]
fn windows_task_events_share_the_virtual_process_scope() {
let mut service = CoordinatorService::new(7);
service.record_task_completion_event(TaskCompletionEvent {
tenant: TenantId::from("other-tenant"),
project: ProjectId::from("other-project"),
process: ProcessId::from("other-process"),
node: NodeId::from("other-node"),
executor: super::TaskExecutor::Node,
task_definition: TaskDefinitionId::from("other-task"),
task: TaskInstanceId::from("other-task"),
attempt_id: None,
placement: None,
terminal_state: TaskTerminalState::Completed,
status_code: Some(0),
stdout_bytes: 0,
stderr_bytes: 0,
stdout_tail: String::new(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
artifact_digest: None,
artifact_size_bytes: None,
result: None,
});
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "windows-node".to_owned(),
public_key: test_node_public_key("windows-node"),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "windows-node".to_owned(),
capabilities: windows_capabilities(),
cached_environment_digests: vec![Digest::sha256("env-windows-command-dev")],
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: true,
online: true,
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process".to_owned(),
restart: false,
})
.unwrap();
register_test_task_assignment(
&mut service,
"tenant",
"project",
"process",
"windows-node",
"windows-command-dev",
"windows-command-dev",
7,
);
let recorded = service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "process".to_owned(),
node: "windows-node".to_owned(),
task: "windows-command-dev".to_owned(),
terminal_state: None,
status_code: Some(0),
stdout_bytes: 24,
stderr_bytes: 0,
stdout_tail: "windows output".to_owned(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: Some("/vfs/artifacts/windows-output.txt".to_owned()),
artifact_digest: Some(Digest::sha256("windows-artifact")),
artifact_size_bytes: Some(24),
result: None,
})
.unwrap();
assert_eq!(
recorded,
CoordinatorResponse::TaskRecorded {
process: ProcessId::from("process"),
task: TaskInstanceId::from("windows-command-dev"),
events_recorded: 1,
}
);
let CoordinatorResponse::TaskEvents { events } = service
.handle_request(CoordinatorRequest::ListTaskEvents {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: Some("process".to_owned()),
})
.unwrap()
else {
panic!("expected task events");
};
assert_eq!(events.len(), 1);
assert_eq!(events[0].process, ProcessId::from("process"));
assert_eq!(events[0].node, NodeId::from("windows-node"));
assert_eq!(events[0].task, TaskInstanceId::from("windows-command-dev"));
assert_eq!(
events[0].artifact_path,
Some(VfsPath::new("/vfs/artifacts/windows-output.txt").unwrap())
);
}
#[test]
fn service_schedules_task_across_reported_node_descriptors() {
let mut service = CoordinatorService::new(7);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "other-tenant".to_owned(),
project: "other-project".to_owned(),
node: "other-node".to_owned(),
public_key: test_node_public_key("other-node"),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "other-tenant".to_owned(),
project: "other-project".to_owned(),
node: "other-node".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: false,
online: true,
})
.unwrap();
for node in ["cold-node", "warm-node"] {
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: node.to_owned(),
public_key: test_node_public_key(node),
})
.unwrap();
}
let recorded = service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "cold-node".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: false,
online: true,
})
.unwrap();
assert_eq!(
recorded,
CoordinatorResponse::NodeCapabilitiesRecorded {
node: NodeId::from("cold-node"),
node_descriptors: 1,
}
);
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "warm-node".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: vec![Digest::sha256("env")],
dependency_cache_digests: vec![Digest::sha256("deps")],
source_snapshots: vec![Digest::sha256("source")],
artifact_locations: vec!["build-output".to_owned()],
direct_connectivity: true,
online: true,
})
.unwrap();
let CoordinatorResponse::NodeDescriptors { descriptors, actor } = service
.handle_request(CoordinatorRequest::ListNodeDescriptors {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "operator".to_owned(),
})
.unwrap()
else {
panic!("expected node descriptor inspector state");
};
assert_eq!(actor, UserId::from("operator"));
assert_eq!(descriptors.len(), 2);
let warm = descriptors
.iter()
.find(|descriptor| descriptor.id == NodeId::from("warm-node"))
.expect("warm node descriptor is visible to inspector state");
assert!(warm
.capabilities
.capabilities
.contains(&Capability::Command));
assert!(warm.cached_environments.contains(&Digest::sha256("env")));
assert!(warm.dependency_caches.contains(&Digest::sha256("deps")));
assert!(warm.source_snapshots.contains(&Digest::sha256("source")));
assert!(warm
.artifact_locations
.contains(&ArtifactId::from("build-output")));
let CoordinatorResponse::TaskPlacement { placement } = service
.handle_request(CoordinatorRequest::ScheduleTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
environment: Some(EnvironmentRequirements::linux_container()),
environment_digest: Some(Digest::sha256("env")),
required_capabilities: vec![Capability::Command],
dependency_cache: Some(Digest::sha256("deps")),
source_snapshot: Some(Digest::sha256("source")),
required_artifacts: vec!["build-output".to_owned()],
prefer_node: None,
})
.unwrap()
else {
panic!("expected task placement");
};
assert_eq!(placement.node, NodeId::from("warm-node"));
assert!(placement
.reasons
.iter()
.any(|reason| reason.contains("environment")));
assert!(placement
.reasons
.iter()
.any(|reason| reason.contains("source")));
assert!(placement
.reasons
.iter()
.any(|reason| reason.contains("dependency")));
assert!(placement
.reasons
.iter()
.any(|reason| reason.contains("artifact")));
let error = service
.handle_request(CoordinatorRequest::ScheduleTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
environment: None,
environment_digest: None,
required_capabilities: vec![Capability::WindowsCommandDev],
dependency_cache: None,
source_snapshot: None,
required_artifacts: Vec::new(),
prefer_node: None,
})
.unwrap_err();
assert!(error.to_string().contains("WindowsCommandDev"));
service.quota.set_workflow_limits(ResourceLimits {
limits: BTreeMap::from([(LimitKind::Spawn, 0)]),
});
let quota_error = service
.handle_request(CoordinatorRequest::ScheduleTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
environment: None,
environment_digest: None,
required_capabilities: vec![Capability::Command],
dependency_cache: None,
source_snapshot: None,
required_artifacts: Vec::new(),
prefer_node: None,
})
.unwrap_err();
assert!(quota_error
.to_string()
.contains("quota unavailable for placement"));
service.quota.set_workflow_limits(ResourceLimits {
limits: BTreeMap::from([(LimitKind::Spawn, 10)]),
});
service.admission.workflow_placement_allowed = false;
let policy_error = service
.handle_request(CoordinatorRequest::ScheduleTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
environment: None,
environment_digest: None,
required_capabilities: vec![Capability::Command],
dependency_cache: None,
source_snapshot: None,
required_artifacts: Vec::new(),
prefer_node: None,
})
.unwrap_err();
assert!(policy_error.to_string().contains("policy denied placement"));
}
#[test]
fn coordinator_side_task_launch_queues_worker_assignment() {
let mut service = CoordinatorService::new(9);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker-linux".to_owned(),
public_key: test_node_public_key("worker-linux"),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker-linux".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: vec![Digest::sha256("env")],
dependency_cache_digests: vec![Digest::sha256("deps")],
source_snapshots: vec![Digest::sha256("source")],
artifact_locations: vec!["bootstrap-artifact".to_owned()],
direct_connectivity: true,
online: true,
})
.unwrap();
let CoordinatorResponse::ProcessStarted {
launch_attempt: None,
epoch,
..
} = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "vp-control".to_owned(),
restart: false,
})
.unwrap()
else {
panic!("expected coordinator-side process start");
};
service
.handle_signed_node_request_auto(CoordinatorRequest::ReconnectNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker-linux".to_owned(),
process: "vp-control".to_owned(),
epoch,
})
.unwrap();
service.artifact_registry.flush_metadata(ArtifactFlush {
id: ArtifactId::from("bootstrap-artifact"),
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
process: ProcessId::from("vp-control"),
producer_task: TaskInstanceId::from("bootstrap"),
retaining_node: NodeId::from("worker-linux"),
digest: Digest::sha256("bootstrap"),
size: 9,
});
let submitted_task_spec = TaskSpec {
tenant: TenantId::from("tenant"),
project: ProjectId::from("project"),
process: ProcessId::from("vp-control"),
task_definition: clusterflux_core::TaskDefinitionId::from("compile-linux"),
task_instance: clusterflux_core::TaskInstanceId::from("compile-linux-1"),
dispatch: TaskDispatch::CoordinatorNodeWasm {
export: Some("compile-linux".to_owned()),
abi: WasmExportAbi::TaskV1,
},
environment_id: Some("linux".to_owned()),
environment: None,
environment_digest: Some(Digest::sha256("env")),
required_capabilities: BTreeSet::from([Capability::Command]),
dependency_cache: Some(Digest::sha256("deps")),
source_snapshot: Some(Digest::sha256("source")),
required_artifacts: vec![ArtifactId::from("bootstrap-artifact")],
args: vec![
TaskBoundaryValue::SmallJson(serde_json::json!("test")),
TaskBoundaryValue::SourceSnapshot(Digest::sha256("source")),
TaskBoundaryValue::Artifact(ArtifactHandle {
id: ArtifactId::from("bootstrap-artifact"),
digest: Digest::sha256("bootstrap"),
size_bytes: 9,
}),
],
vfs_epoch: epoch,
failure_policy: Default::default(),
bundle_digest: Some(Digest::sha256(TEST_WASM_MODULE)),
};
let CoordinatorResponse::TaskLaunched {
process,
task,
placement,
assignment,
..
} = service
.handle_authorized_test_task_launch(CoordinatorRequest::LaunchTask {
task_spec: submitted_task_spec.clone(),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: Some("user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
wait_for_node: false,
artifact_path: "/vfs/artifacts/dap-output.txt".to_owned(),
wasm_module_base64: test_wasm_module_base64(),
})
.unwrap()
else {
panic!("expected launched task");
};
assert_eq!(process, ProcessId::from("vp-control"));
assert_eq!(task, TaskInstanceId::from("compile-linux-1"));
assert_eq!(placement.node, NodeId::from("worker-linux"));
assert!(placement
.reasons
.iter()
.any(|reason| reason.contains("environment")));
assert!(placement
.reasons
.iter()
.any(|reason| reason.contains("source")));
assert_eq!(assignment.node, NodeId::from("worker-linux"));
assert_eq!(assignment.epoch, epoch);
assert_eq!(
assignment.task_spec.bundle_digest,
Some(Digest::sha256(TEST_WASM_MODULE))
);
assert!(assignment.task_spec.product_mode_uses_remote_dispatch());
assert_eq!(assignment.task_spec, submitted_task_spec);
assert_eq!(
assignment.task_spec.environment_id.as_deref(),
Some("linux")
);
assert!(matches!(
assignment.task_spec.dispatch,
TaskDispatch::CoordinatorNodeWasm {
export: Some(ref export),
abi: WasmExportAbi::TaskV1,
} if export == "compile-linux"
));
assert_eq!(assignment.task_spec.vfs_epoch, epoch);
assert_eq!(assignment.task_spec.args, submitted_task_spec.args);
let CoordinatorResponse::TaskAssignment { assignment } = service
.handle_signed_node_request_auto(CoordinatorRequest::PollTaskAssignment {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker-linux".to_owned(),
})
.unwrap()
else {
panic!("expected worker assignment poll");
};
let assignment = assignment.expect("worker should receive queued assignment");
assert_eq!(assignment.process, ProcessId::from("vp-control"));
assert_eq!(assignment.task, TaskInstanceId::from("compile-linux-1"));
assert!(assignment.task_spec.product_mode_uses_remote_dispatch());
assert_eq!(
assignment.task_spec.bundle_digest,
Some(Digest::sha256(TEST_WASM_MODULE))
);
assert_eq!(assignment.wasm_module_base64, test_wasm_module_base64());
let CoordinatorResponse::TaskJoined { join } = service
.handle_request(CoordinatorRequest::JoinTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "vp-control".to_owned(),
task: "compile-linux-1".to_owned(),
})
.unwrap()
else {
panic!("expected pending join result");
};
assert_eq!(join.state, TaskJoinState::Pending);
assert!(!join.remote_completion_observed);
let CoordinatorResponse::TaskAssignment { assignment } = service
.handle_signed_node_request_auto(CoordinatorRequest::PollTaskAssignment {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker-linux".to_owned(),
})
.unwrap()
else {
panic!("expected empty worker assignment poll");
};
assert!(assignment.is_none());
service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "vp-control".to_owned(),
node: "worker-linux".to_owned(),
task: "compile-linux-1".to_owned(),
terminal_state: Some(TaskTerminalState::Completed),
status_code: Some(0),
stdout_bytes: 12,
stderr_bytes: 0,
stdout_tail: "ok".to_owned(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: Some("/vfs/artifacts/dap-output.txt".to_owned()),
artifact_digest: Some(Digest::sha256("artifact")),
artifact_size_bytes: Some(12),
result: Some(TaskBoundaryValue::Artifact(ArtifactHandle {
id: ArtifactId::from("dap-output.txt"),
digest: Digest::sha256("artifact"),
size_bytes: 12,
})),
})
.unwrap();
let CoordinatorResponse::TaskEvents { events } = service
.handle_request(CoordinatorRequest::ListTaskEvents {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: Some("vp-control".to_owned()),
})
.unwrap()
else {
panic!("expected task events");
};
let event_placement = events[0]
.placement
.as_ref()
.expect("task event should retain launch placement explanation");
assert_eq!(event_placement.node, NodeId::from("worker-linux"));
assert_eq!(event_placement.reasons, placement.reasons);
assert_eq!(
events[0].result,
Some(TaskBoundaryValue::Artifact(ArtifactHandle {
id: ArtifactId::from("dap-output.txt"),
digest: Digest::sha256("artifact"),
size_bytes: 12,
}))
);
let CoordinatorResponse::TaskJoined { join } = service
.handle_request(CoordinatorRequest::JoinTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "vp-control".to_owned(),
task: "compile-linux-1".to_owned(),
})
.unwrap()
else {
panic!("expected completed join result");
};
assert_eq!(join.state, TaskJoinState::Completed);
assert!(join.remote_completion_observed);
assert_eq!(
join.result,
Some(TaskBoundaryValue::Artifact(ArtifactHandle {
id: ArtifactId::from("dap-output.txt"),
digest: Digest::sha256("artifact"),
size_bytes: 12,
}))
);
register_test_task_assignment(
&mut service,
"tenant",
"project",
"vp-control",
"worker-linux",
"wasm-add",
"wasm-add",
epoch,
);
service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "vp-control".to_owned(),
node: "worker-linux".to_owned(),
task: "wasm-add".to_owned(),
terminal_state: Some(TaskTerminalState::Completed),
status_code: Some(0),
stdout_bytes: 3,
stderr_bytes: 0,
stdout_tail: "42\n".to_owned(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
artifact_digest: None,
artifact_size_bytes: None,
result: Some(TaskBoundaryValue::SmallJson(serde_json::json!(42))),
})
.unwrap();
let CoordinatorResponse::TaskJoined { join } = service
.handle_request(CoordinatorRequest::JoinTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "vp-control".to_owned(),
task: "wasm-add".to_owned(),
})
.unwrap()
else {
panic!("expected serialized join result");
};
assert_eq!(
join.result,
Some(TaskBoundaryValue::SmallJson(serde_json::json!(42)))
);
}
#[test]
fn same_definition_instances_join_correctly_when_they_complete_in_reverse_order() {
let mut service = CoordinatorService::new(41);
service
.handle_request(CoordinatorRequest::CreateProject {
tenant: "tenant".to_owned(),
actor_user: "user".to_owned(),
project: "project".to_owned(),
name: "Duplicate instances".to_owned(),
})
.unwrap();
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker".to_owned(),
public_key: test_node_public_key("worker"),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: true,
online: true,
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: Some("user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "vp".to_owned(),
restart: false,
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReconnectNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker".to_owned(),
process: "vp".to_owned(),
epoch: 41,
})
.unwrap();
let restart_compatibility = Digest::sha256("compile-u32-to-u32-abi-v1");
let (initial_module, initial_bundle_digest) =
test_edited_task_bundle(&restart_compatibility, "initial-body");
for (instance, argument) in [("compile-1", 1), ("compile-2", 2)] {
let mut spec =
test_task_spec_instance("tenant", "project", "vp", "compile", instance, 41, []);
spec.args = vec![TaskBoundaryValue::SmallJson(serde_json::json!(argument))];
spec.dispatch = TaskDispatch::CoordinatorNodeWasm {
export: Some("compile_export".to_owned()),
abi: WasmExportAbi::TaskV1,
};
spec.bundle_digest = Some(initial_bundle_digest.clone());
let CoordinatorResponse::TaskLaunched {
task, assignment, ..
} = service
.handle_authorized_test_task_launch(CoordinatorRequest::LaunchTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: Some("user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
task_spec: spec,
wait_for_node: false,
artifact_path: format!("/vfs/artifacts/{instance}.json"),
wasm_module_base64: initial_module.clone(),
})
.unwrap()
else {
panic!("expected launched task instance");
};
assert_eq!(task, TaskInstanceId::from(instance));
assert_eq!(
assignment.task_spec.task_definition,
clusterflux_core::TaskDefinitionId::from("compile")
);
assert_eq!(
assignment.task_spec.task_instance,
clusterflux_core::TaskInstanceId::from(instance)
);
}
for instance in ["compile-1", "compile-2"] {
let CoordinatorResponse::TaskAssignment {
assignment: Some(assignment),
} = service
.handle_signed_node_request_auto(CoordinatorRequest::PollTaskAssignment {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker".to_owned(),
})
.unwrap()
else {
panic!("expected queued task assignment");
};
assert_eq!(assignment.task, TaskInstanceId::from(instance));
}
service
.handle_request(CoordinatorRequest::CancelTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "vp".to_owned(),
node: "worker".to_owned(),
task: "compile-1".to_owned(),
})
.unwrap();
for (instance, expected_cancelled) in [("compile-1", true), ("compile-2", false)] {
let CoordinatorResponse::TaskControl {
cancel_requested,
abort_requested,
..
} = service
.handle_signed_node_request_auto(CoordinatorRequest::PollTaskControl {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "vp".to_owned(),
node: "worker".to_owned(),
task: instance.to_owned(),
})
.unwrap()
else {
panic!("expected task control response");
};
assert_eq!(cancel_requested, expected_cancelled);
assert!(!abort_requested);
}
for (instance, result) in [("compile-2", 22), ("compile-1", 11)] {
service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "vp".to_owned(),
node: "worker".to_owned(),
task: instance.to_owned(),
terminal_state: Some(TaskTerminalState::Completed),
status_code: Some(0),
stdout_bytes: 0,
stderr_bytes: 0,
stdout_tail: String::new(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
artifact_digest: None,
artifact_size_bytes: None,
result: Some(TaskBoundaryValue::SmallJson(serde_json::json!(result))),
})
.unwrap();
}
for (instance, expected) in [("compile-1", 11), ("compile-2", 22)] {
let CoordinatorResponse::TaskJoined { join } = service
.handle_request(CoordinatorRequest::JoinTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "vp".to_owned(),
task: instance.to_owned(),
})
.unwrap()
else {
panic!("expected task join");
};
assert_eq!(
join.task_instance,
clusterflux_core::TaskInstanceId::from(instance)
);
assert_eq!(
join.result,
Some(TaskBoundaryValue::SmallJson(serde_json::json!(expected)))
);
}
let duplicate = service
.handle_authorized_test_task_launch(CoordinatorRequest::LaunchTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: Some("user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
task_spec: test_task_spec_instance(
"tenant",
"project",
"vp",
"compile",
"compile-1",
41,
[],
),
wait_for_node: false,
artifact_path: "/vfs/artifacts/duplicate.json".to_owned(),
wasm_module_base64: test_wasm_module_base64(),
})
.unwrap_err();
assert!(duplicate.to_string().contains("fresh task-instance id"));
let (replacement_module, replacement_bundle_digest) =
test_edited_task_bundle(&restart_compatibility, "edited-body");
assert_ne!(replacement_bundle_digest, initial_bundle_digest);
let CoordinatorResponse::TaskRestart {
accepted,
restarted_task_instance,
restarted_attempt_id,
..
} = service
.handle_request(CoordinatorRequest::RestartTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "vp".to_owned(),
task: "compile-1".to_owned(),
replacement_bundle: Some(TaskReplacementBundle {
bundle_digest: replacement_bundle_digest.clone(),
wasm_module_base64: replacement_module.clone(),
source_snapshot: None,
}),
})
.unwrap()
else {
panic!("expected task restart response");
};
assert!(accepted);
let restarted = restarted_task_instance.expect("restart returns the logical instance");
let restarted_attempt_id = restarted_attempt_id.expect("restart creates a new attempt");
assert!(restarted_attempt_id.starts_with("ta_"));
assert_eq!(restarted, TaskInstanceId::from("compile-1"));
assert_ne!(restarted, TaskInstanceId::from("compile-2"));
let CoordinatorResponse::TaskAssignment {
assignment: Some(restarted_assignment),
} = service
.handle_signed_node_request_auto(CoordinatorRequest::PollTaskAssignment {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker".to_owned(),
})
.unwrap()
else {
panic!("expected restarted assignment");
};
assert_eq!(restarted_assignment.task, restarted);
assert_eq!(
restarted_assignment.task_spec.task_definition,
clusterflux_core::TaskDefinitionId::from("compile")
);
assert_eq!(
restarted_assignment.task_spec.args,
vec![TaskBoundaryValue::SmallJson(serde_json::json!(1))]
);
assert_eq!(
restarted_assignment.task_spec.bundle_digest,
Some(replacement_bundle_digest)
);
assert_eq!(
restarted_assignment.task_spec.dispatch,
TaskDispatch::CoordinatorNodeWasm {
export: Some("compile_export".to_owned()),
abi: WasmExportAbi::TaskV1,
}
);
assert_eq!(restarted_assignment.wasm_module_base64, replacement_module);
let incompatible_compatibility = Digest::sha256("compile-changed-signature");
let (incompatible_module, incompatible_bundle_digest) =
test_edited_task_bundle(&incompatible_compatibility, "incompatible-body");
let CoordinatorResponse::TaskRestart {
accepted,
requires_whole_process_restart,
restarted_task_instance,
message,
..
} = service
.handle_request(CoordinatorRequest::RestartTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "vp".to_owned(),
task: "compile-2".to_owned(),
replacement_bundle: Some(TaskReplacementBundle {
bundle_digest: incompatible_bundle_digest,
wasm_module_base64: incompatible_module,
source_snapshot: None,
}),
})
.unwrap()
else {
panic!("expected incompatible task restart response");
};
assert!(!accepted);
assert!(requires_whole_process_restart);
assert!(restarted_task_instance.is_none());
assert!(message.contains("task ABI changed"));
let CoordinatorResponse::TaskJoined { join } = service
.handle_request(CoordinatorRequest::JoinTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
process: "vp".to_owned(),
task: "compile-2".to_owned(),
})
.unwrap()
else {
panic!("expected unaffected sibling join");
};
assert_eq!(join.state, TaskJoinState::Completed);
assert_eq!(
join.result,
Some(TaskBoundaryValue::SmallJson(serde_json::json!(22)))
);
}
#[test]
fn long_lived_process_state_reaches_a_scoped_bounded_steady_state() {
let mut service = CoordinatorService::new(73);
service
.handle_request(CoordinatorRequest::CreateProject {
tenant: "tenant-soak".to_owned(),
actor_user: "user-soak".to_owned(),
project: "project-soak".to_owned(),
name: "Bounded soak".to_owned(),
})
.unwrap();
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant-soak".to_owned(),
project: "project-soak".to_owned(),
node: "worker-soak".to_owned(),
public_key: test_node_public_key("worker-soak"),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant-soak".to_owned(),
project: "project-soak".to_owned(),
node: "worker-soak".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: true,
online: true,
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant-soak".to_owned(),
project: "project-soak".to_owned(),
actor_user: Some("user-soak".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "vp-soak".to_owned(),
restart: false,
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReconnectNode {
tenant: "tenant-soak".to_owned(),
project: "project-soak".to_owned(),
node: "worker-soak".to_owned(),
process: "vp-soak".to_owned(),
epoch: 73,
})
.unwrap();
let event_waves = 4;
for index in 0..super::MAX_TASK_EVENTS_PER_PROCESS * event_waves {
let task = format!("soak-task-{index}");
register_test_task_assignment(
&mut service,
"tenant-soak",
"project-soak",
"vp-soak",
"worker-soak",
"soak-task",
&task,
73,
);
service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant-soak".to_owned(),
project: "project-soak".to_owned(),
process: "vp-soak".to_owned(),
node: "worker-soak".to_owned(),
task,
terminal_state: Some(TaskTerminalState::Completed),
status_code: Some(0),
stdout_bytes: 8,
stderr_bytes: 0,
stdout_tail: "soak-log".to_owned(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
artifact_digest: None,
artifact_size_bytes: None,
result: Some(TaskBoundaryValue::SmallJson(json!(index))),
})
.unwrap();
}
for index in 0..super::MAX_DEBUG_AUDIT_EVENTS_PER_PROCESS * 3 {
service
.record_debug_audit_event(
TenantId::from("tenant-soak"),
ProjectId::from("project-soak"),
ProcessId::from("vp-soak"),
None,
UserId::from("user-soak"),
"soak_inspect",
true,
format!("bounded audit {index}"),
)
.unwrap();
}
for _ in 0..128 {
service
.handle_signed_node_request_auto(CoordinatorRequest::PollTaskAssignment {
tenant: "tenant-soak".to_owned(),
project: "project-soak".to_owned(),
node: "worker-soak".to_owned(),
})
.unwrap();
}
for index in 0..2_000 {
service.node_replay_nonces.insert(
(
crate::NodeScopeKey::new(
TenantId::from("tenant-soak"),
ProjectId::from("project-soak"),
NodeId::from("worker-soak"),
),
format!("expired-{index}"),
),
0,
);
}
service
.handle_request(CoordinatorRequest::NodeHeartbeat {
tenant: "tenant-soak".to_owned(),
project: "project-soak".to_owned(),
node: "worker-soak".to_owned(),
node_signature: Some(signed_node_heartbeat_in_scope(
"tenant-soak",
"project-soak",
"worker-soak",
"post-expiry-prune",
)),
})
.unwrap();
for index in 0..3 {
service.record_task_completion_event(TaskCompletionEvent {
tenant: TenantId::from("tenant-other"),
project: ProjectId::from("project-other"),
process: ProcessId::from("vp-other"),
node: NodeId::from("worker-other"),
executor: TaskExecutor::Node,
task_definition: clusterflux_core::TaskDefinitionId::from("other"),
task: TaskInstanceId::new(format!("other-{index}")),
attempt_id: None,
placement: None,
terminal_state: TaskTerminalState::Completed,
status_code: Some(0),
stdout_bytes: 0,
stderr_bytes: 0,
stdout_tail: String::new(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: None,
artifact_digest: None,
artifact_size_bytes: None,
result: None,
});
}
let retained_soak_events = service
.task_events
.iter()
.filter(|event| event.process == ProcessId::from("vp-soak"))
.count();
let retained_other_events = service
.task_events
.iter()
.filter(|event| event.process == ProcessId::from("vp-other"))
.count();
let retained_soak_audit = service
.debug_audit_events
.iter()
.filter(|event| event.process == ProcessId::from("vp-soak"))
.count();
let retained_soak_checkpoints = service
.task_restart_checkpoints
.keys()
.filter(|(_, _, process, _)| process == &ProcessId::from("vp-soak"))
.count();
let retained_soak_nonces = service
.node_replay_nonces
.keys()
.filter(|(scope, _)| {
scope
== &crate::NodeScopeKey::new(
TenantId::from("tenant-soak"),
ProjectId::from("project-soak"),
NodeId::from("worker-soak"),
)
})
.count();
assert_eq!(retained_soak_events, super::MAX_TASK_EVENTS_PER_PROCESS);
assert_eq!(retained_other_events, 3);
assert_eq!(
retained_soak_audit,
super::MAX_DEBUG_AUDIT_EVENTS_PER_PROCESS
);
assert_eq!(
retained_soak_checkpoints,
super::MAX_RESTART_CHECKPOINTS_PER_PROCESS
);
assert!(retained_soak_nonces <= super::MAX_NODE_REPLAY_NONCES_PER_AUTHORITY);
assert!(
super::MAX_NODE_REPLAY_NONCES_PER_AUTHORITY
>= super::NODE_SIGNATURE_WINDOW_SECONDS as usize * 2 * 1_000
/ clusterflux_core::MIN_SIGNED_NODE_POLL_INTERVAL_MS as usize
+ 64,
"the bounded node replay window must sustain artifact and assignment polls at the protocol minimum with control-message headroom"
);
assert!(service
.coordinator
.active_process(
&TenantId::from("tenant-soak"),
&ProjectId::from("project-soak"),
&ProcessId::from("vp-soak"),
)
.is_some());
}
#[test]
fn signed_active_wasm_task_can_spawn_and_join_child_in_its_process_only() {
let mut service = CoordinatorService::new(12);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker".to_owned(),
public_key: test_node_public_key("worker"),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: true,
online: true,
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: Some("user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "vp".to_owned(),
restart: false,
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReconnectNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "worker".to_owned(),
process: "vp".to_owned(),
epoch: 12,
})
.unwrap();
service
.handle_authorized_test_task_launch(CoordinatorRequest::LaunchTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: Some("user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
task_spec: test_task_spec("tenant", "project", "vp", "parent", 12, []),
wait_for_node: false,
artifact_path: "/vfs/artifacts/parent.json".to_owned(),
wasm_module_base64: test_wasm_module_base64(),
})
.unwrap();
let child_request = CoordinatorRequest::LaunchChildTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "vp".to_owned(),
node: "worker".to_owned(),
parent_task: "parent".to_owned(),
task_spec: test_task_spec("tenant", "project", "vp", "child", 12, []),
wait_for_node: false,
artifact_path: "/vfs/artifacts/child.json".to_owned(),
wasm_module_base64: test_wasm_module_base64(),
};
let unsigned = service.handle_request(child_request.clone()).unwrap_err();
assert!(unsigned.to_string().contains("signed_node"));
let CoordinatorResponse::TaskLaunched { actor, task, .. } = service
.handle_signed_node_request_auto(child_request)
.unwrap()
else {
panic!("expected signed child launch");
};
assert_eq!(task, TaskInstanceId::from("child"));
assert_eq!(actor.kind, "task");
assert_eq!(actor.credential_kind, CredentialKind::TaskCredential);
assert!(actor.scopes.contains(&"process:spawn-child".to_owned()));
let wrong_parent = service
.handle_signed_node_request_auto(CoordinatorRequest::JoinChildTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "vp".to_owned(),
node: "worker".to_owned(),
parent_task: "not-active".to_owned(),
task: "child".to_owned(),
})
.unwrap_err();
assert!(wrong_parent.to_string().contains("active parent"));
let CoordinatorResponse::TaskJoined { join } = service
.handle_signed_node_request_auto(CoordinatorRequest::JoinChildTask {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "vp".to_owned(),
node: "worker".to_owned(),
parent_task: "parent".to_owned(),
task: "child".to_owned(),
})
.unwrap()
else {
panic!("expected signed child join");
};
assert_eq!(join.state, TaskJoinState::Pending);
}
#[test]
fn coordinator_rejects_named_environment_without_requirements() {
let mut service = CoordinatorService::new(10);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "uncached-worker".to_owned(),
public_key: test_node_public_key("uncached-worker"),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "uncached-worker".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: true,
online: true,
})
.unwrap();
let CoordinatorResponse::ProcessStarted {
launch_attempt: None,
epoch,
..
} = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "vp-environment".to_owned(),
restart: false,
})
.unwrap()
else {
panic!("expected process start");
};
service
.handle_signed_node_request_auto(CoordinatorRequest::ReconnectNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "uncached-worker".to_owned(),
process: "vp-environment".to_owned(),
epoch,
})
.unwrap();
let mut task_spec = test_task_spec(
"tenant",
"project",
"vp-environment",
"compile-linux",
epoch,
[],
);
task_spec.environment_id = Some("missing-environment".to_owned());
task_spec.environment_digest = Some(Digest::sha256("missing-environment"));
let error = service
.handle_authorized_test_task_launch(CoordinatorRequest::LaunchTask {
task_spec,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: Some("user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
wait_for_node: false,
artifact_path: "/vfs/artifacts/environment-output.txt".to_owned(),
wasm_module_base64: test_wasm_module_base64(),
})
.unwrap_err();
assert!(error.to_string().contains("named environment cache"));
}
#[test]
fn coordinator_side_task_launch_fails_cleanly_without_capable_worker() {
let mut service = CoordinatorService::new(10);
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "vp-control".to_owned(),
restart: false,
})
.unwrap();
let error = service
.handle_authorized_test_task_launch(CoordinatorRequest::LaunchTask {
task_spec: test_task_spec(
"tenant",
"project",
"vp-control",
"compile-linux",
10,
[Capability::Command],
),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: Some("user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
wait_for_node: false,
artifact_path: "/vfs/artifacts/dap-output.txt".to_owned(),
wasm_module_base64: test_wasm_module_base64(),
})
.unwrap_err();
assert!(error.to_string().contains("no capable node"));
}
#[test]
fn coordinator_side_task_launch_can_wait_for_capable_worker() {
let mut service = CoordinatorService::new(11);
let CoordinatorResponse::ProcessStarted {
epoch: other_epoch, ..
} = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "other-tenant".to_owned(),
project: "other-project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "other-vp-wait".to_owned(),
restart: false,
})
.unwrap()
else {
panic!("expected unrelated process start");
};
let CoordinatorResponse::TaskQueued {
queued_tasks: other_queued_tasks,
..
} = service
.handle_authorized_test_task_launch(CoordinatorRequest::LaunchTask {
task_spec: test_task_spec(
"other-tenant",
"other-project",
"other-vp-wait",
"other-compile",
other_epoch,
[Capability::Command],
),
tenant: "other-tenant".to_owned(),
project: "other-project".to_owned(),
actor_user: Some("other-user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
wait_for_node: true,
artifact_path: "/vfs/artifacts/other-wait-output.txt".to_owned(),
wasm_module_base64: test_wasm_module_base64(),
})
.unwrap()
else {
panic!("expected unrelated queued task launch");
};
assert_eq!(other_queued_tasks, 1);
let CoordinatorResponse::ProcessStarted {
launch_attempt: None,
epoch,
..
} = service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "vp-wait".to_owned(),
restart: false,
})
.unwrap()
else {
panic!("expected process start");
};
let CoordinatorResponse::TaskQueued {
process,
task,
reason,
queued_tasks,
charged_spawns,
..
} = service
.handle_authorized_test_task_launch(CoordinatorRequest::LaunchTask {
task_spec: test_task_spec(
"tenant",
"project",
"vp-wait",
"compile-linux",
epoch,
[Capability::Command],
),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: Some("user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
wait_for_node: true,
artifact_path: "/vfs/artifacts/wait-output.txt".to_owned(),
wasm_module_base64: test_wasm_module_base64(),
})
.unwrap()
else {
panic!("expected queued task launch");
};
assert_eq!(process, ProcessId::from("vp-wait"));
assert_eq!(task, TaskInstanceId::from("compile-linux"));
assert!(reason.contains("waiting for any capable node"));
assert_eq!(queued_tasks, 1);
assert_eq!(charged_spawns, 2);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "late-worker".to_owned(),
public_key: test_node_public_key("late-worker"),
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "late-worker".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: true,
online: true,
})
.unwrap();
let CoordinatorResponse::TaskAssignment { assignment } = service
.handle_signed_node_request_auto(CoordinatorRequest::PollTaskAssignment {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "late-worker".to_owned(),
})
.unwrap()
else {
panic!("expected pending assignment poll");
};
let assignment = assignment.expect("late worker should receive pending assignment");
assert_eq!(assignment.process, ProcessId::from("vp-wait"));
assert_eq!(assignment.task, TaskInstanceId::from("compile-linux"));
assert_eq!(assignment.node, NodeId::from("late-worker"));
assert_eq!(assignment.epoch, epoch);
assert!(assignment.task_spec.product_mode_uses_remote_dispatch());
assert_eq!(assignment.wasm_module_base64, test_wasm_module_base64());
let CoordinatorResponse::TaskAssignment { assignment } = service
.handle_signed_node_request_auto(CoordinatorRequest::PollTaskAssignment {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "late-worker".to_owned(),
})
.unwrap()
else {
panic!("expected empty assignment poll");
};
assert!(assignment.is_none());
}
#[test]
fn service_rejects_malformed_node_capability_report() {
let mut service = CoordinatorService::new(1);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
public_key: test_node_public_key("node"),
})
.unwrap();
let mut capabilities = linux_capabilities();
capabilities
.source_providers
.insert("../checkout".to_owned());
let error = service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
capabilities,
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: true,
online: true,
})
.unwrap_err();
assert!(error.to_string().contains("source provider id"));
assert!(service.node_descriptors.is_empty());
}
#[test]
fn service_meters_rendezvous_before_direct_transfer_plan() {
let mut service = CoordinatorService::new(7);
service.quota.set_rendezvous_limits(ResourceLimits {
limits: BTreeMap::from([(LimitKind::RendezvousAttempt, 2)]),
});
let CoordinatorResponse::RendezvousPlan {
plan,
charged_rendezvous_attempts,
} = service
.handle_request(CoordinatorRequest::RequestRendezvous {
scope: data_plane_scope("project"),
source: endpoint("node-a"),
destination: endpoint("node-b"),
direct_connectivity: true,
failure_reason: String::new(),
})
.unwrap()
else {
panic!("expected rendezvous plan");
};
assert_eq!(charged_rendezvous_attempts, 1);
assert_eq!(plan.scope.project, ProjectId::from("project"));
assert_eq!(plan.source.node, NodeId::from("node-a"));
assert_eq!(plan.destination.node, NodeId::from("node-b"));
assert!(plan.coordinator_assisted_rendezvous);
assert!(!plan.coordinator_bulk_relay_allowed);
let unavailable = service
.handle_request(CoordinatorRequest::RequestRendezvous {
scope: data_plane_scope("project"),
source: endpoint("node-a"),
destination: endpoint("node-b"),
direct_connectivity: false,
failure_reason: "nat traversal failed".to_owned(),
})
.unwrap_err();
let unavailable = unavailable.to_string();
assert!(unavailable.contains("nat traversal failed"));
assert!(unavailable.contains("coordinator bulk relay is disabled"));
let quota = service
.handle_request(CoordinatorRequest::RequestRendezvous {
scope: data_plane_scope("project"),
source: endpoint("node-a"),
destination: endpoint("node-b"),
direct_connectivity: true,
failure_reason: String::new(),
})
.unwrap_err();
assert!(quota.to_string().contains("RendezvousAttempt"));
}
#[test]
fn enrolled_node_can_request_a_structurally_scoped_rendezvous_with_signed_authority() {
let mut service = CoordinatorService::new(7);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node-a".to_owned(),
public_key: test_node_public_key("node-a"),
})
.unwrap();
let response = service
.handle_signed_node_request_auto(CoordinatorRequest::RequestRendezvous {
scope: data_plane_scope("project"),
source: endpoint("node-a"),
destination: endpoint("node-b"),
direct_connectivity: true,
failure_reason: String::new(),
})
.unwrap();
let CoordinatorResponse::RendezvousPlan { plan, .. } = response else {
panic!("expected signed rendezvous plan");
};
assert_eq!(plan.source.node, NodeId::from("node-a"));
assert_eq!(plan.destination.node, NodeId::from("node-b"));
}
#[test]
fn signed_hostile_artifact_paths_return_errors_and_the_same_service_stays_healthy() {
let mut service = CoordinatorService::new(27);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
public_key: test_node_public_key("node"),
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "hostile-path-process".to_owned(),
restart: false,
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReconnectNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
process: "hostile-path-process".to_owned(),
epoch: 27,
})
.unwrap();
let invalid_metadata = service
.handle_signed_node_request_auto(CoordinatorRequest::ReportVfsMetadata {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "hostile-path-process".to_owned(),
node: "node".to_owned(),
task: "metadata-task".to_owned(),
artifact_path: Some("/vfs/artifacts/bad artifact!".to_owned()),
artifact_digest: Some(Digest::sha256("bad")),
artifact_size_bytes: Some(3),
large_bytes_uploaded: false,
})
.unwrap_err();
assert!(
invalid_metadata
.to_string()
.contains("invalid VFS artifact path"),
"unexpected error: {invalid_metadata}"
);
let valid_metadata = service
.handle_signed_node_request_auto(CoordinatorRequest::ReportVfsMetadata {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "hostile-path-process".to_owned(),
node: "node".to_owned(),
task: "metadata-task".to_owned(),
artifact_path: Some("/vfs/artifacts/valid-artifact".to_owned()),
artifact_digest: Some(Digest::sha256("valid")),
artifact_size_bytes: Some(5),
large_bytes_uploaded: false,
})
.unwrap();
assert!(matches!(
valid_metadata,
CoordinatorResponse::VfsMetadataRecorded { .. }
));
register_test_task_assignment(
&mut service,
"tenant",
"project",
"hostile-path-process",
"node",
"child",
"child-instance",
27,
);
let invalid_completion = service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "hostile-path-process".to_owned(),
node: "node".to_owned(),
task: "child-instance".to_owned(),
terminal_state: Some(TaskTerminalState::Completed),
status_code: Some(0),
stdout_bytes: 0,
stderr_bytes: 0,
stdout_tail: String::new(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: Some("/vfs/artifacts/repeated//component".to_owned()),
artifact_digest: Some(Digest::sha256("bad-completion")),
artifact_size_bytes: Some(0),
result: None,
})
.unwrap_err();
assert!(
invalid_completion
.to_string()
.contains("invalid VFS artifact path"),
"unexpected error: {invalid_completion}"
);
let valid_completion = service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
process: "hostile-path-process".to_owned(),
node: "node".to_owned(),
task: "child-instance".to_owned(),
terminal_state: Some(TaskTerminalState::Completed),
status_code: Some(0),
stdout_bytes: 0,
stderr_bytes: 0,
stdout_tail: String::new(),
stderr_tail: String::new(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: Some("/vfs/artifacts/valid-completion".to_owned()),
artifact_digest: Some(Digest::sha256("valid-completion")),
artifact_size_bytes: Some(0),
result: None,
})
.unwrap();
assert!(matches!(
valid_completion,
CoordinatorResponse::TaskRecorded { .. }
));
}
#[test]
fn service_rejects_task_completion_outside_node_scope() {
let mut service = CoordinatorService::new(1);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant-a".to_owned(),
project: "project-a".to_owned(),
node: "node".to_owned(),
public_key: test_node_public_key("node"),
})
.unwrap();
service
.handle_request(CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "tenant-b".to_owned(),
project: "project-b".to_owned(),
actor_user: None,
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "process".to_owned(),
restart: false,
})
.unwrap();
let error = service
.handle_signed_node_request_auto(CoordinatorRequest::TaskCompleted {
tenant: "tenant-b".to_owned(),
project: "project-b".to_owned(),
process: "process".to_owned(),
node: "node".to_owned(),
task: "compile-linux".to_owned(),
terminal_state: None,
status_code: Some(0),
stdout_bytes: 128,
stderr_bytes: 64,
stdout_tail: "foreign stdout".to_owned(),
stderr_tail: "foreign stderr".to_owned(),
stdout_truncated: false,
stderr_truncated: false,
artifact_path: Some("/vfs/artifacts/foreign.txt".to_owned()),
artifact_digest: Some(Digest::sha256("foreign-artifact")),
artifact_size_bytes: Some(128),
result: None,
})
.unwrap_err();
assert!(error.to_string().contains("not enrolled"));
let CoordinatorResponse::TaskEvents { events } = service
.handle_request(CoordinatorRequest::ListTaskEvents {
tenant: "tenant-b".to_owned(),
project: "project-b".to_owned(),
actor_user: "user".to_owned(),
process: Some("process".to_owned()),
})
.unwrap()
else {
panic!("expected task events");
};
assert!(events.is_empty());
}
#[test]
fn service_rejects_task_event_access_using_retained_process_scope() {
let mut service = CoordinatorService::new(1);
service.process_scope_history.push_back((
TenantId::from("tenant-a"),
ProjectId::from("project-a"),
ProcessId::from("process"),
));
let error = service
.handle_request(CoordinatorRequest::ListTaskEvents {
tenant: "tenant-b".to_owned(),
project: "project-b".to_owned(),
actor_user: "user".to_owned(),
process: Some("process".to_owned()),
})
.unwrap_err();
assert!(error
.to_string()
.contains("outside the virtual process tenant/project scope"));
}
#[test]
fn service_rejects_node_capability_report_outside_enrollment_scope() {
let mut service = CoordinatorService::new(1);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant-a".to_owned(),
project: "project-a".to_owned(),
node: "node".to_owned(),
public_key: test_node_public_key("node"),
})
.unwrap();
let error = service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant-b".to_owned(),
project: "project-b".to_owned(),
node: "node".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: true,
online: true,
})
.unwrap_err();
assert!(error.to_string().contains("not enrolled"));
assert!(service.node_descriptors.is_empty());
}
#[test]
fn service_rejects_source_preparation_completion_outside_node_scope() {
let mut service = CoordinatorService::new(1);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant-a".to_owned(),
project: "project-a".to_owned(),
node: "node".to_owned(),
public_key: test_node_public_key("node"),
})
.unwrap();
let error = service
.handle_signed_node_request_auto(CoordinatorRequest::CompleteSourcePreparation {
tenant: "tenant-b".to_owned(),
project: "project-b".to_owned(),
node: "node".to_owned(),
provider: SourceProviderKind::Filesystem,
source_snapshot: Digest::sha256("foreign-source"),
})
.unwrap_err();
assert!(error.to_string().contains("not enrolled"));
}
#[test]
fn service_rejects_unknown_node_heartbeat() {
let mut service = CoordinatorService::new(1);
let error = service
.handle_request(CoordinatorRequest::NodeHeartbeat {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "missing".to_owned(),
node_signature: None,
})
.unwrap_err();
assert!(error.to_string().contains("not enrolled"));
}
#[test]
fn service_requires_signed_node_heartbeat_from_enrolled_key() {
let mut service = CoordinatorService::new(5);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
public_key: test_node_public_key("node"),
})
.unwrap();
let unsigned = service
.handle_request(CoordinatorRequest::NodeHeartbeat {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
node_signature: None,
})
.unwrap_err();
assert!(unsigned.to_string().contains("signed proof"));
let wrong_private_key = test_node_private_key("other-node");
let wrong_signature = service
.handle_request(CoordinatorRequest::NodeHeartbeat {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
node_signature: Some(signed_node_heartbeat_with_private_key(
"node",
&wrong_private_key,
"wrong-node-key",
)),
})
.unwrap_err();
assert!(wrong_signature.to_string().contains("signature"));
let signed = signed_node_heartbeat("node", "fresh-node-heartbeat");
let heartbeat = service
.handle_request(CoordinatorRequest::NodeHeartbeat {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
node_signature: Some(signed.clone()),
})
.unwrap();
assert_eq!(
heartbeat,
CoordinatorResponse::NodeHeartbeat {
node: NodeId::from("node"),
epoch: 5,
}
);
let replay = service
.handle_request(CoordinatorRequest::NodeHeartbeat {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
node_signature: Some(signed),
})
.unwrap_err();
assert!(replay.to_string().contains("nonce"));
}
#[test]
fn service_rejects_raw_node_originated_requests_without_signed_envelope() {
let mut service = CoordinatorService::new(5);
service
.handle_request(CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
public_key: test_node_public_key("node"),
})
.unwrap();
let error = service
.handle_request(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: true,
online: true,
})
.unwrap_err();
assert!(error.to_string().contains("signed_node envelope proof"));
}
#[test]
fn service_stream_accepts_multiple_requests_on_one_connection() {
let (listener, addr) = bind_listener("127.0.0.1:0").unwrap();
let server = std::thread::spawn(move || {
let mut service = CoordinatorService::new(3);
let (stream, _) = listener.accept().unwrap();
service.handle_stream_local_trusted(stream).unwrap();
});
let mut stream = TcpStream::connect(addr).unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
write_coordinator_wire_request(
&mut stream,
&CoordinatorRequest::AttachNode {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
public_key: test_node_public_key("node"),
},
"stream-1",
);
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(matches!(
serde_json::from_str::<CoordinatorResponse>(&line).unwrap(),
CoordinatorResponse::NodeAttached { .. }
));
write_coordinator_wire_request(
&mut stream,
&CoordinatorRequest::NodeHeartbeat {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: "node".to_owned(),
node_signature: Some(signed_node_heartbeat("node", "stream-heartbeat")),
},
"stream-2",
);
line.clear();
reader.read_line(&mut line).unwrap();
assert_eq!(
serde_json::from_str::<CoordinatorResponse>(&line).unwrap(),
CoordinatorResponse::NodeHeartbeat {
node: NodeId::from("node"),
epoch: 3,
}
);
stream.shutdown(std::net::Shutdown::Both).unwrap();
server.join().unwrap();
}
#[test]
fn strict_service_stream_rejects_body_authority_and_accepts_cli_session() {
let (listener, addr) = bind_listener("127.0.0.1:0").unwrap();
let server = std::thread::spawn(move || {
let mut service = CoordinatorService::new(3);
service
.issue_cli_session(
TenantId::from("tenant"),
ProjectId::from("project"),
UserId::from("user"),
"strict-stream-session",
None,
)
.unwrap();
let (stream, _) = listener.accept().unwrap();
service.handle_stream(stream).unwrap();
});
let mut stream = TcpStream::connect(addr).unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
write_coordinator_wire_request(
&mut stream,
&CoordinatorRequest::StartProcess {
launch_attempt: None,
tenant: "victim-tenant".to_owned(),
project: "victim-project".to_owned(),
actor_user: Some("forged-user".to_owned()),
actor_agent: None,
agent_public_key_fingerprint: None,
agent_signature: None,
process: "vp-forged".to_owned(),
restart: false,
},
"strict-stream-forged",
);
let mut line = String::new();
reader.read_line(&mut line).unwrap();
let CoordinatorResponse::Error { error } =
serde_json::from_str::<CoordinatorResponse>(&line).unwrap()
else {
panic!("expected strict body-authority denial");
};
assert!(error
.message
.contains("request-body identity fields are not authority"));
assert_eq!(error.request_id, "strict-stream-forged");
assert_eq!(error.code, clusterflux_core::ApiErrorCode::Forbidden);
write_coordinator_wire_request(
&mut stream,
&CoordinatorRequest::Authenticated {
session_secret: "strict-stream-session".to_owned(),
request: AuthenticatedCoordinatorRequest::StartProcess {
launch_attempt: None,
process: "vp-authenticated".to_owned(),
restart: false,
},
},
"strict-stream-authenticated",
);
line.clear();
reader.read_line(&mut line).unwrap();
let CoordinatorResponse::ProcessStarted {
launch_attempt: None,
process,
actor,
..
} = serde_json::from_str::<CoordinatorResponse>(&line).unwrap()
else {
panic!("expected authenticated strict process start");
};
assert_eq!(process, ProcessId::from("vp-authenticated"));
assert_eq!(actor.user, Some(UserId::from("user")));
stream.shutdown(std::net::Shutdown::Both).unwrap();
server.join().unwrap();
}
#[test]
fn service_stream_rejects_invalid_versioned_envelope_metadata() {
let (listener, addr) = bind_listener("127.0.0.1:0").unwrap();
let server = std::thread::spawn(move || {
let mut service = CoordinatorService::new(3);
let (stream, _) = listener.accept().unwrap();
service.handle_stream(stream).unwrap();
});
let mut stream = TcpStream::connect(addr).unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut wire_request = coordinator_wire_request(
"bad-operation",
serde_json::to_value(CoordinatorRequest::Ping).unwrap(),
);
wire_request["operation"] = serde_json::Value::String("attach_node".to_owned());
serde_json::to_writer(&mut stream, &wire_request).unwrap();
stream.write_all(b"\n").unwrap();
stream.flush().unwrap();
let mut line = String::new();
reader.read_line(&mut line).unwrap();
let response = serde_json::from_str::<CoordinatorResponse>(&line).unwrap();
let CoordinatorResponse::Error { error } = response else {
panic!("expected invalid wire envelope response");
};
assert!(error
.message
.contains("operation attach_node does not match payload operation ping"));
assert_eq!(error.request_id, "bad-operation");
assert_eq!(error.code, clusterflux_core::ApiErrorCode::ValidationError);
stream.shutdown(std::net::Shutdown::Both).unwrap();
server.join().unwrap();
}
#[test]
fn coordinator_protocol_rejects_client_supplied_authority_fields() {
for payload in [
json!({
"type": "create_node_enrollment_grant",
"tenant": "tenant",
"project": "project",
"actor_user": "user",
"grant": "attacker-chosen",
"ttl_seconds": 60,
}),
json!({
"type": "exchange_node_enrollment_grant",
"tenant": "tenant",
"project": "project",
"node": "node",
"public_key": "key",
"enrollment_grant": "grant",
"now_epoch_seconds": 0,
}),
json!({
"type": "schedule_task",
"tenant": "tenant",
"project": "project",
"environment": null,
"environment_digest": null,
"required_capabilities": [],
"dependency_cache": null,
"source_snapshot": null,
"required_artifacts": [],
"quota_available": true,
"policy_allowed": true,
"prefer_node": null,
}),
json!({
"type": "create_artifact_download_link",
"tenant": "tenant",
"project": "project",
"actor_user": "user",
"artifact": "artifact",
"max_bytes": 1,
"token_nonce": "attacker-chosen",
"ttl_seconds": 60,
}),
] {
let error = serde_json::from_value::<CoordinatorRequest>(payload).unwrap_err();
assert!(error.to_string().contains("unknown field"));
}
}
#[test]
fn coordinator_generates_and_bounds_node_enrollment_grants() {
let mut service = CoordinatorService::new(7);
service.set_server_time(100);
let first = service
.handle_request(CoordinatorRequest::CreateNodeEnrollmentGrant {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
ttl_seconds: u64::MAX,
})
.unwrap();
let second = service
.handle_request(CoordinatorRequest::CreateNodeEnrollmentGrant {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
ttl_seconds: u64::MAX,
})
.unwrap();
let CoordinatorResponse::NodeEnrollmentGrantCreated {
grant: first_grant,
expires_at_epoch_seconds,
..
} = first
else {
panic!("expected enrollment grant");
};
let CoordinatorResponse::NodeEnrollmentGrantCreated {
grant: second_grant,
..
} = second
else {
panic!("expected enrollment grant");
};
assert!(first_grant.starts_with("node_grant_"));
assert_ne!(first_grant, second_grant);
assert_eq!(expires_at_epoch_seconds, 100 + 15 * 60);
}
#[test]
fn web_process_summaries_are_scoped_paginated_and_retain_authoritative_terminal_state() {
let mut service = CoordinatorService::new(7);
for (tenant, project, user, secret) in [
("tenant-a", "project-a", "user-a", "session-a"),
("tenant-b", "project-b", "user-b", "session-b"),
] {
service
.issue_cli_session(
TenantId::from(tenant),
ProjectId::from(project),
UserId::from(user),
secret,
None,
)
.unwrap();
}
service.set_server_time(100);
service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::StartProcess {
launch_attempt: None,
process: "process-one".to_owned(),
restart: false,
},
})
.unwrap();
let CoordinatorResponse::ProcessSummaries {
processes,
next_cursor,
..
} = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::ListProcessSummaries {
cursor: None,
limit: 1,
},
})
.unwrap()
else {
panic!("expected process summaries");
};
assert_eq!(processes.len(), 1);
assert_eq!(processes[0].process, ProcessId::from("process-one"));
assert_eq!(processes[0].lifecycle, ProcessLifecycleState::Active);
assert_eq!(processes[0].activity, ProcessActivityState::Running);
assert_eq!(processes[0].started_at_epoch_seconds, 100);
assert!(next_cursor.is_none());
let CoordinatorResponse::ProcessSummaries { processes, .. } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-b".to_owned(),
request: AuthenticatedCoordinatorRequest::ListProcessSummaries {
cursor: None,
limit: 10,
},
})
.unwrap()
else {
panic!("expected scoped process summaries");
};
assert!(processes.is_empty());
service.set_server_time(120);
service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::AbortProcess {
process: "process-one".to_owned(),
launch_attempt: None,
},
})
.unwrap();
let CoordinatorResponse::ProcessSummaries { processes, .. } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::ListProcessSummaries {
cursor: None,
limit: 10,
},
})
.unwrap()
else {
panic!("expected terminal process summary");
};
assert_eq!(
processes[0].lifecycle,
ProcessLifecycleState::RecentTerminal
);
assert_eq!(processes[0].activity, ProcessActivityState::Cancelled);
assert_eq!(
processes[0].final_result,
Some(ProcessFinalResult::Cancelled)
);
assert_eq!(processes[0].ended_at_epoch_seconds, Some(120));
service.set_server_time(130);
service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::StartProcess {
launch_attempt: None,
process: "process-two".to_owned(),
restart: false,
},
})
.unwrap();
let CoordinatorResponse::ProcessSummaries {
processes,
next_cursor: Some(cursor),
..
} = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::ListProcessSummaries {
cursor: None,
limit: 1,
},
})
.unwrap()
else {
panic!("expected first process summary page");
};
assert_eq!(processes[0].process, ProcessId::from("process-two"));
let CoordinatorResponse::ProcessSummaries {
processes,
next_cursor: None,
..
} = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::ListProcessSummaries {
cursor: Some(cursor),
limit: 1,
},
})
.unwrap()
else {
panic!("expected final process summary page");
};
assert_eq!(processes[0].process, ProcessId::from("process-one"));
let oversized = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::ListProcessSummaries {
cursor: None,
limit: 101,
},
})
.unwrap_err();
assert!(oversized.to_string().contains("limit"));
assert!(oversized.to_string().contains("100"));
}
#[test]
fn process_summary_eviction_releases_live_log_accounting_state() {
let mut service = CoordinatorService::new(7);
let tenant = TenantId::from("tenant-summary-bound");
let project = ProjectId::from("project-summary-bound");
let task = TaskInstanceId::from("task");
for index in 0..MAX_RECENT_PROCESS_SUMMARIES_PER_PROJECT {
let process = ProcessId::new(format!("process-{index:03}"));
service.record_process_started(&tenant, &project, &process, index as u64);
service.record_process_terminal(
&tenant,
&project,
&process,
ProcessFinalResult::Completed,
index as u64 + 1,
);
let key = (
tenant.clone(),
project.clone(),
process,
task.clone(),
"stdout".to_owned(),
);
service.recent_log_accounted_bytes.insert(key.clone(), 10);
service.recent_log_truncated_streams.insert(key);
}
let evicted = ProcessId::from("process-000");
service.record_process_started(&tenant, &project, &ProcessId::from("process-next"), 1_000);
assert!(!service.process_summaries.contains_key(&(
tenant.clone(),
project.clone(),
evicted.clone()
)));
assert!(!service.recent_log_accounted_bytes.keys().any(
|(entry_tenant, entry_project, process, _, _)| {
entry_tenant == &tenant && entry_project == &project && process == &evicted
}
));
assert!(!service.recent_log_truncated_streams.iter().any(
|(entry_tenant, entry_project, process, _, _)| {
entry_tenant == &tenant && entry_project == &project && process == &evicted
}
));
}
#[test]
fn web_node_summaries_are_scoped_paginated_and_hard_bounded() {
let mut service = CoordinatorService::new(7);
service
.issue_cli_session(
TenantId::from("tenant"),
ProjectId::from("project"),
UserId::from("user"),
"session",
None,
)
.unwrap();
for node in ["node-a", "node-b", "node-c"] {
enroll_test_node(
&mut service,
"tenant",
"project",
node,
&test_node_public_key(node),
);
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: "tenant".to_owned(),
project: "project".to_owned(),
node: node.to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: Vec::new(),
direct_connectivity: true,
online: true,
})
.unwrap();
}
let CoordinatorResponse::NodeSummaries {
nodes,
next_cursor: Some(cursor),
..
} = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session".to_owned(),
request: AuthenticatedCoordinatorRequest::ListNodeSummaries {
cursor: None,
limit: 2,
},
})
.unwrap()
else {
panic!("expected first node-summary page");
};
assert_eq!(
nodes
.iter()
.map(|node| node.id.as_str())
.collect::<Vec<_>>(),
["node-a", "node-b"]
);
let CoordinatorResponse::NodeSummaries {
nodes,
next_cursor: None,
..
} = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session".to_owned(),
request: AuthenticatedCoordinatorRequest::ListNodeSummaries {
cursor: Some(cursor),
limit: 2,
},
})
.unwrap()
else {
panic!("expected final node-summary page");
};
assert_eq!(nodes.len(), 1);
assert_eq!(nodes[0].id, NodeId::from("node-c"));
let oversized = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session".to_owned(),
request: AuthenticatedCoordinatorRequest::ListNodeSummaries {
cursor: None,
limit: 201,
},
})
.unwrap_err();
assert!(oversized.to_string().contains("limit"));
assert!(oversized.to_string().contains("200"));
}
#[test]
fn web_artifact_queries_are_scoped_paginated_and_track_retention_availability() {
let mut service = CoordinatorService::new(7);
for (tenant, project, user, secret, node) in [
("tenant-a", "project-a", "user-a", "session-a", "node-a"),
("tenant-b", "project-b", "user-b", "session-b", "node-b"),
] {
service
.issue_cli_session(
TenantId::from(tenant),
ProjectId::from(project),
UserId::from(user),
secret,
None,
)
.unwrap();
enroll_test_node(
&mut service,
tenant,
project,
node,
&test_node_public_key(node),
);
}
service.set_server_time(100);
for (tenant, project, node) in [
("tenant-a", "project-a", "node-a"),
("tenant-b", "project-b", "node-b"),
] {
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportNodeCapabilities {
tenant: tenant.to_owned(),
project: project.to_owned(),
node: node.to_owned(),
capabilities: linux_capabilities(),
cached_environment_digests: Vec::new(),
dependency_cache_digests: Vec::new(),
source_snapshots: Vec::new(),
artifact_locations: vec!["shared-artifact".to_owned()],
direct_connectivity: true,
online: false,
})
.unwrap();
}
let CoordinatorResponse::NodeSummaries { nodes, .. } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::ListNodeSummaries {
cursor: None,
limit: 200,
},
})
.unwrap()
else {
panic!("expected node summaries");
};
assert_eq!(nodes.len(), 1);
assert_eq!(nodes[0].id, NodeId::from("node-a"));
assert!(nodes[0].online);
assert!(!nodes[0].stale);
assert_eq!(nodes[0].last_seen_epoch_seconds, Some(100));
assert_eq!(nodes[0].capabilities.os, Os::Linux);
for (tenant, project, node, digest) in [
("tenant-a", "project-a", "node-a", "tenant-a-bytes"),
("tenant-b", "project-b", "node-b", "tenant-b-bytes"),
] {
service.artifact_registry.flush_metadata(ArtifactFlush {
id: ArtifactId::from("shared-artifact"),
tenant: TenantId::from(tenant),
project: ProjectId::from(project),
process: ProcessId::from("process-one"),
producer_task: TaskInstanceId::from("task-one"),
retaining_node: NodeId::from(node),
digest: Digest::sha256(digest),
size: digest.len() as u64,
});
}
service.artifact_registry.flush_metadata(ArtifactFlush {
id: ArtifactId::from("second-artifact"),
tenant: TenantId::from("tenant-a"),
project: ProjectId::from("project-a"),
process: ProcessId::from("process-two"),
producer_task: TaskInstanceId::from("task-two"),
retaining_node: NodeId::from("node-a"),
digest: Digest::sha256("second"),
size: 6,
});
let CoordinatorResponse::Artifacts {
artifacts,
next_cursor: Some(cursor),
} = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::ListArtifacts {
process: None,
cursor: None,
limit: 1,
},
})
.unwrap()
else {
panic!("expected first artifact page");
};
assert_eq!(artifacts.len(), 1);
assert_eq!(artifacts[0].id, ArtifactId::from("second-artifact"));
assert_eq!(artifacts[0].availability, ArtifactAvailability::Available);
let CoordinatorResponse::Artifacts {
artifacts,
next_cursor: None,
} = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::ListArtifacts {
process: None,
cursor: Some(cursor),
limit: 1,
},
})
.unwrap()
else {
panic!("expected final artifact page");
};
assert_eq!(artifacts[0].id, ArtifactId::from("shared-artifact"));
assert_eq!(artifacts[0].digest, Digest::sha256("tenant-a-bytes"));
let cross_tenant = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::GetArtifact {
artifact: "tenant-b-only".to_owned(),
},
})
.unwrap_err();
assert!(cross_tenant.to_string().contains("does not exist"));
let CoordinatorResponse::Artifact { artifact } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-b".to_owned(),
request: AuthenticatedCoordinatorRequest::GetArtifact {
artifact: "shared-artifact".to_owned(),
},
})
.unwrap()
else {
panic!("expected tenant-b artifact");
};
assert_eq!(artifact.digest, Digest::sha256("tenant-b-bytes"));
service.set_server_time(131);
let CoordinatorResponse::NodeSummaries { nodes, .. } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::ListNodeSummaries {
cursor: None,
limit: 200,
},
})
.unwrap()
else {
panic!("expected stale node summary");
};
assert!(!nodes[0].online);
assert!(nodes[0].stale);
assert_eq!(nodes[0].last_seen_epoch_seconds, Some(100));
let CoordinatorResponse::Artifact { artifact } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::GetArtifact {
artifact: "shared-artifact".to_owned(),
},
})
.unwrap()
else {
panic!("expected offline artifact metadata");
};
assert_eq!(artifact.availability, ArtifactAvailability::NodeOffline);
assert!(!artifact.downloadable_now);
service
.artifact_registry
.sync_to_explicit_store(
&TenantId::from("tenant-a"),
&ProjectId::from("project-a"),
&ArtifactId::from("shared-artifact"),
"store://tenant-a/shared-artifact",
)
.unwrap();
let CoordinatorResponse::Artifact { artifact } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::GetArtifact {
artifact: "shared-artifact".to_owned(),
},
})
.unwrap()
else {
panic!("expected explicitly retained artifact metadata");
};
assert_eq!(artifact.availability, ArtifactAvailability::Available);
assert_eq!(
artifact.retention_state,
ArtifactRetentionState::ExplicitStorage
);
assert!(artifact.downloadable_now);
}
#[test]
fn web_recent_logs_are_signed_scoped_cursor_safe_and_memory_bounded() {
let mut service = CoordinatorService::new(7);
for (tenant, project, user, secret, node, process) in [
(
"tenant-a",
"project-a",
"user-a",
"session-a",
"node-a",
"process-shared",
),
(
"tenant-b",
"project-b",
"user-b",
"session-b",
"node-b",
"process-b",
),
] {
service
.issue_cli_session(
TenantId::from(tenant),
ProjectId::from(project),
UserId::from(user),
secret,
None,
)
.unwrap();
enroll_test_node(
&mut service,
tenant,
project,
node,
&test_node_public_key(node),
);
service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: secret.to_owned(),
request: AuthenticatedCoordinatorRequest::StartProcess {
launch_attempt: None,
process: process.to_owned(),
restart: false,
},
})
.unwrap();
service
.handle_signed_node_request_auto(CoordinatorRequest::ReconnectNode {
tenant: tenant.to_owned(),
project: project.to_owned(),
node: node.to_owned(),
process: process.to_owned(),
epoch: 7,
})
.unwrap();
}
service.set_server_time(100);
let first = service
.handle_signed_node_request_auto(CoordinatorRequest::ReportTaskLogChunk {
tenant: "tenant-a".to_owned(),
project: "project-a".to_owned(),
process: "process-shared".to_owned(),
node: "node-a".to_owned(),
task: "task-one".to_owned(),
stream: TaskLogStream::Stdout,
offset: 0,
source_bytes: 5,
text: "hello".to_owned(),
truncated: false,
})
.unwrap();
let CoordinatorResponse::TaskLogChunkRecorded {
sequence: Some(first_sequence),
next_offset: 5,
..
} = first
else {
panic!("expected first live log sequence");
};
assert_eq!(
service.quota.used_log_bytes(
&TenantId::from("tenant-a"),
&ProjectId::from("project-a"),
100,
),
5,
"live bytes must be charged when accepted"
);
let retry = service
.handle_signed_node_request_auto(CoordinatorRequest::ReportTaskLogChunk {
tenant: "tenant-a".to_owned(),
project: "project-a".to_owned(),
process: "process-shared".to_owned(),
node: "node-a".to_owned(),
task: "task-one".to_owned(),
stream: TaskLogStream::Stdout,
offset: 0,
source_bytes: 5,
text: "hello".to_owned(),
truncated: false,
})
.unwrap();
assert!(matches!(
retry,
CoordinatorResponse::TaskLogChunkRecorded { sequence: None, .. }
));
assert_eq!(
service.quota.used_log_bytes(
&TenantId::from("tenant-a"),
&ProjectId::from("project-a"),
100,
),
5,
"a retried chunk must not be charged twice"
);
service
.handle_signed_node_request_auto(CoordinatorRequest::ReportTaskLogChunk {
tenant: "tenant-a".to_owned(),
project: "project-a".to_owned(),
process: "process-shared".to_owned(),
node: "node-a".to_owned(),
task: "task-one".to_owned(),
stream: TaskLogStream::Stdout,
offset: 8,
source_bytes: 2,
text: "ok".to_owned(),
truncated: false,
})
.unwrap();
assert_eq!(
service.quota.used_log_bytes(
&TenantId::from("tenant-a"),
&ProjectId::from("project-a"),
100,
),
10,
"a gap and the delivered bytes must both count toward source-byte usage"
);
let CoordinatorResponse::RecentLogs {
entries,
next_sequence: Some(cursor),
history_truncated: false,
} = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::ListRecentLogs {
process: "process-shared".to_owned(),
task: None,
after_sequence: None,
limit: 2,
},
})
.unwrap()
else {
panic!("expected first recent-log page");
};
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].sequence, first_sequence);
assert_eq!(entries[0].text, "hello");
assert!(entries[1].text.contains("3 bytes"));
assert!(entries[1].truncated);
let CoordinatorResponse::RecentLogs { entries, .. } = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::ListRecentLogs {
process: "process-shared".to_owned(),
task: None,
after_sequence: Some(cursor),
limit: 2,
},
})
.unwrap()
else {
panic!("expected second recent-log page");
};
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].text, "ok");
service
.handle_report_task_log(
"tenant-a".to_owned(),
"project-a".to_owned(),
"process-shared".to_owned(),
"node-a".to_owned(),
"task-one".to_owned(),
12,
0,
"hello???okZZ".to_owned(),
String::new(),
false,
false,
false,
)
.unwrap();
assert_eq!(
service.quota.used_log_bytes(
&TenantId::from("tenant-a"),
&ProjectId::from("project-a"),
100,
),
12,
"the final summary must charge only source bytes not already charged live"
);
assert_eq!(
service
.recent_logs
.get(&(TenantId::from("tenant-a"), ProjectId::from("project-a")))
.unwrap()
.back()
.unwrap()
.text,
"ZZ",
"final-tail reconciliation must append only the nonduplicating suffix"
);
service
.handle_report_task_log(
"tenant-a".to_owned(),
"project-a".to_owned(),
"process-shared".to_owned(),
"node-a".to_owned(),
"task-one".to_owned(),
12,
0,
"hello???okZZ".to_owned(),
String::new(),
false,
false,
false,
)
.unwrap();
assert_eq!(
service.quota.used_log_bytes(
&TenantId::from("tenant-a"),
&ProjectId::from("project-a"),
100,
),
12,
"replayed final accounting must be idempotent"
);
let marker = service
.handle_report_task_log_chunk(
"tenant-a".to_owned(),
"project-a".to_owned(),
"process-shared".to_owned(),
"node-a".to_owned(),
"task-one".to_owned(),
TaskLogStream::Stdout,
12,
0,
"[log output truncated at node capture limit]".to_owned(),
true,
)
.unwrap();
assert!(matches!(
marker,
CoordinatorResponse::TaskLogChunkRecorded {
sequence: Some(_),
next_offset: 12,
..
}
));
let repeated_marker = service
.handle_report_task_log_chunk(
"tenant-a".to_owned(),
"project-a".to_owned(),
"process-shared".to_owned(),
"node-a".to_owned(),
"task-one".to_owned(),
TaskLogStream::Stdout,
12,
0,
"[log output truncated at node capture limit]".to_owned(),
true,
)
.unwrap();
assert!(matches!(
repeated_marker,
CoordinatorResponse::TaskLogChunkRecorded {
sequence: None,
next_offset: 12,
..
}
));
let cross_tenant = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-b".to_owned(),
request: AuthenticatedCoordinatorRequest::ListRecentLogs {
process: "process-shared".to_owned(),
task: None,
after_sequence: None,
limit: 10,
},
})
.unwrap_err();
assert!(cross_tenant.to_string().contains("outside"));
let oversized = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::ListRecentLogs {
process: "process-shared".to_owned(),
task: None,
after_sequence: None,
limit: 201,
},
})
.unwrap_err();
assert!(oversized.to_string().contains("limit"));
assert!(oversized.to_string().contains("200"));
service
.handle_report_task_log_chunk(
"tenant-b".to_owned(),
"project-b".to_owned(),
"process-b".to_owned(),
"node-b".to_owned(),
"task-b".to_owned(),
TaskLogStream::Stderr,
0,
1,
"b".to_owned(),
false,
)
.unwrap();
for offset in 10..310 {
service
.handle_report_task_log_chunk(
"tenant-a".to_owned(),
"project-a".to_owned(),
"process-shared".to_owned(),
"node-a".to_owned(),
"task-one".to_owned(),
TaskLogStream::Stdout,
offset,
1,
"x".to_owned(),
false,
)
.unwrap();
}
let tenant_a_logs =
&service.recent_logs[&(TenantId::from("tenant-a"), ProjectId::from("project-a"))];
assert!(tenant_a_logs.len() <= MAX_RECENT_LOG_ENTRIES_PER_PROCESS);
let tenant_b_logs =
&service.recent_logs[&(TenantId::from("tenant-b"), ProjectId::from("project-b"))];
assert_eq!(tenant_b_logs.len(), 1);
assert_eq!(tenant_b_logs[0].text, "b");
let CoordinatorResponse::RecentLogs {
entries,
history_truncated,
..
} = service
.handle_request(CoordinatorRequest::Authenticated {
session_secret: "session-a".to_owned(),
request: AuthenticatedCoordinatorRequest::ListRecentLogs {
process: "process-shared".to_owned(),
task: None,
after_sequence: None,
limit: 200,
},
})
.unwrap()
else {
panic!("expected bounded recent-log response");
};
assert_eq!(entries.len(), 200);
assert!(history_truncated);
}