From b8f2fa479c898ec1ec36322850a39ede7b2ff28f Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 24 Oct 2026 12:15:10 +0900 Subject: [PATCH] test: add isolated backend fixture seeding --- crates/workspace-server/Cargo.toml | 4 + crates/workspace-server/src/lib.rs | 2 + crates/workspace-server/src/store.rs | 42 +- crates/workspace-server/src/test_support.rs | 688 ++++++++++++++++++++ 4 files changed, 732 insertions(+), 4 deletions(-) create mode 100644 crates/workspace-server/src/test_support.rs diff --git a/crates/workspace-server/Cargo.toml b/crates/workspace-server/Cargo.toml index b561ca4eb..0879f2b07 100644 --- a/crates/workspace-server/Cargo.toml +++ b/crates/workspace-server/Cargo.toml @@ -10,6 +10,10 @@ autobins = false name = "yoi-server" path = "src/main.rs" +[features] +# Offline fixture preparation only; build product binaries without this feature. +test-support = [] + [dependencies] api-macros = { workspace = true, features = ["axum"] } async-trait.workspace = true diff --git a/crates/workspace-server/src/lib.rs b/crates/workspace-server/src/lib.rs index 02c40beb4..b615a312a 100644 --- a/crates/workspace-server/src/lib.rs +++ b/crates/workspace-server/src/lib.rs @@ -25,6 +25,8 @@ pub mod runtime_subscription; pub mod server; pub mod skills; pub mod store; +#[cfg(feature = "test-support")] +pub use store::fixture_support as test_support; pub mod subjektiv; #[cfg(test)] mod ticket_browse_performance_tests; diff --git a/crates/workspace-server/src/store.rs b/crates/workspace-server/src/store.rs index d742f0ec6..f12c54334 100644 --- a/crates/workspace-server/src/store.rs +++ b/crates/workspace-server/src/store.rs @@ -2589,6 +2589,10 @@ pub struct SqliteWorkspaceStore { db_executor: crate::db_executor::DbExecutor, } +#[cfg(feature = "test-support")] +#[path = "test_support.rs"] +pub mod fixture_support; + impl SqliteWorkspaceStore { pub fn open(path: impl AsRef) -> Result { let conn = Connection::open(path)?; @@ -2900,6 +2904,33 @@ impl SqliteWorkspaceStore { request_fingerprint: &str, singleton_key: Option<&str>, current_memory_settings: &WorkspaceMemorySettingsRecord, + ) -> Result { + self.reserve_worker_create_with( + workspace_id, + runtime_id, + allocation_key, + request_fingerprint, + singleton_key, + current_memory_settings, + |_| Ok(None), + |_, _| Ok(()), + ) + } + + // Keep reservation validation, Memory binding and writes in one transaction. The + // nondefault test-support module supplies fixed identity / offline catalog hooks; + // ordinary creation always supplies a newly generated ID and no completion hook. + #[allow(clippy::too_many_arguments)] + fn reserve_worker_create_with( + &self, + workspace_id: &str, + runtime_id: &str, + allocation_key: &str, + request_fingerprint: &str, + singleton_key: Option<&str>, + current_memory_settings: &WorkspaceMemorySettingsRecord, + prepare: impl FnOnce(&rusqlite::Transaction<'_>) -> Result>, + complete: impl FnOnce(&rusqlite::Transaction<'_>, &WorkerCreateReservation) -> Result<()>, ) -> Result { if allocation_key.trim().is_empty() || request_fingerprint.trim().is_empty() { return Err(Error::InvalidInput( @@ -2912,6 +2943,7 @@ impl SqliteWorkspaceStore { validate_workspace_memory_settings_record(current_memory_settings, workspace_id)?; self.with_conn_mut(|conn| { let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; + let fixed_worker_id = prepare(&tx)?; let existing = tx .query_row( "SELECT worker_id, runtime_id, request_fingerprint, create_fingerprint, \ @@ -3094,7 +3126,7 @@ impl SqliteWorkspaceStore { validate_workspace_memory_settings_snapshot(&snapshot, workspace_id)?; let create_fingerprint = bound_worker_create_fingerprint(request_fingerprint, &snapshot); - let worker_id = WorkerId::now_v7(); + let worker_id = fixed_worker_id.unwrap_or_else(WorkerId::now_v7); let worker_id_text = worker_id.to_string(); let now = chrono::Utc::now().to_rfc3339(); let singleton_generation = match singleton_owner.as_ref() { @@ -3183,13 +3215,15 @@ impl SqliteWorkspaceStore { )); } } - tx.commit()?; - Ok(WorkerCreateReservation { + let reservation = WorkerCreateReservation { worker_id, create_fingerprint, memory_settings: snapshot, singleton, - }) + }; + complete(&tx, &reservation)?; + tx.commit()?; + Ok(reservation) }) } diff --git a/crates/workspace-server/src/test_support.rs b/crates/workspace-server/src/test_support.rs new file mode 100644 index 000000000..fcdf63087 --- /dev/null +++ b/crates/workspace-server/src/test_support.rs @@ -0,0 +1,688 @@ +//! Offline Backend fixture preparation, available only with `test-support`. +//! +//! Use a new isolated database and the existing account, Workspace signing-identity / +//! `ControlPlaneStore::create_workspace_bootstrap`, and Runtime binding APIs first. +//! Generate fresh account credentials and signing keys for each environment; no fixture +//! credentials are accepted here. Install Profile configuration through the existing +//! config-source mechanisms before starting the product processes. +//! +//! This module seeds Backend ownership only. It neither writes Runtime storage nor +//! launches a Worker, fabricates Runtime observations, changes authorization, or sends +//! initial input. The caller must separately install the matching stopped Runtime +//! fixture. Build product binaries in a separate Cargo invocation **without** this +//! feature (Cargo features unify within an invocation). + +use super::*; +use crate::hosts::{WorkerSpawnIntent, WorkerSpawnRequest, worker_spawn_create_fingerprint}; +use worker_runtime::catalog::ProfileSelector; + +/// One ordinary, attachment-free Worker from an externally validated fixture. +/// +/// IDs must match the Runtime fixture. `request` is the unresolved canonical create +/// intent, not a Runtime launch request containing credentials or execution grants. +/// The Profile selector must already be installed by the harness. `initial_submit` +/// must be empty: saved Session contents belong to the Runtime fixture, not new input. +/// Singleton/service, Ticket-assigned and attachment-bearing Workers are deliberately +/// unsupported. +pub struct FixedWorkerSeed<'a> { + pub workspace_id: &'a str, + pub runtime_id: &'a str, + pub worker_id: WorkerId, + pub allocation_key: &'a str, + pub request: &'a WorkerSpawnRequest, +} + +/// Insert a fixed Worker catalog entry and completed creation reservation atomically. +/// +/// Uses the normal reservation validators, authoritative Memory snapshot, fingerprint +/// binding, resource-key allocator and immediate transaction. Even an exact repeat is +/// rejected: this is not an upsert/import/recovery API. Existing allocations, Worker +/// identities and residual lifecycle/attachment records are never adopted or repaired. +/// Returns the creation binding needed to check the corresponding Runtime fixture. +pub fn seed_fixed_worker( + store: &SqliteWorkspaceStore, + seed: FixedWorkerSeed<'_>, +) -> Result { + validate_identifier("workspace_id", seed.workspace_id)?; + validate_identifier("runtime_id", seed.runtime_id)?; + let request = seed.request; + if !request.initial_submit.is_empty() { + return Err(Error::InvalidInput( + "fixture Worker cannot contain initial_submit; offline seeding accepts no new input" + .into(), + )); + } + if !matches!(request.intent, WorkerSpawnIntent::WorkspaceCoding) + || request.singleton_key.is_some() + || request.ticket_assignment.is_some() + || request.resolved_subjektiv_attached + || !request.workdir_attachment_requests.is_empty() + || !request.resolved_workdir_attachment_requests.is_empty() + || !request.resolved_workdir_attachments.is_empty() + || request.resolved_config_bundle.is_some() + || request.resolved_workspace_api.is_some() + || request.resolved_memory_settings.is_some() + || request.resolved_worker_observation_enabled + || !request.resolved_worker_observation_grants.is_empty() + || request.resolved_control_operation.is_some() + || request.resolved_execution_grant.is_some() + { + return Err(Error::InvalidInput( + "fixture Worker must be ordinary unresolved create intent without singleton, assignments, attachments or grants".into(), + )); + } + // Builtin selectors already carry their canonical `builtin:` prefix. + let profile = match &request.profile { + ProfileSelector::Builtin(name) | ProfileSelector::Named(name) => name, + }; + validate_non_empty("fixture Profile selector", profile)?; + let worker_id = seed.worker_id.to_string(); + let display_name = request + .requested_worker_name + .as_deref() + .unwrap_or(&worker_id); + validate_non_empty("fixture Worker display name", display_name)?; + let fingerprint = worker_spawn_create_fingerprint(request).map_err(Error::InvalidInput)?; + let memory = store.get_workspace_memory_settings(seed.workspace_id)?; + store.reserve_worker_create_with( + seed.workspace_id, + seed.runtime_id, + seed.allocation_key, + &fingerprint, + None, + &memory, + |tx| { + require_seed_target(tx, &seed)?; + Ok(Some(seed.worker_id)) + }, + |tx, reservation| { + let now = chrono::Utc::now().to_rfc3339(); + tx.execute( + "INSERT INTO worker_registry ( + workspace_id, runtime_id, worker_id, display_name, profile, + retention_state, created_at, updated_at + ) VALUES (?1, ?2, ?3, ?4, ?5, 'normal', ?6, ?6)", + params![seed.workspace_id, seed.runtime_id, worker_id, display_name, profile, now], + )?; + let changed = tx.execute( + "UPDATE worker_create_reservations SET state = 'created', updated_at = ?4 + WHERE workspace_id = ?1 AND runtime_id = ?2 AND worker_id = ?3 AND state = 'reserved'", + params![seed.workspace_id, seed.runtime_id, reservation.worker_id.to_string(), now], + )?; + if changed != 1 { + return Err(Error::Store("fixture creation reservation disappeared".into())); + } + Ok(()) + }, + ) +} + +fn require_seed_target(tx: &rusqlite::Transaction<'_>, seed: &FixedWorkerSeed<'_>) -> Result<()> { + let active: bool = tx.query_row( + "SELECT EXISTS(SELECT 1 FROM workspaces w + JOIN workspace_runtime_bindings r ON r.workspace_id = w.workspace_id + JOIN workspace_signing_identities s ON s.workspace_id = w.workspace_id + WHERE w.workspace_id = ?1 AND w.state = 'active' + AND r.runtime_id = ?2 AND r.revoked_at IS NULL + AND r.authentication_mode = 'workspace_identity' AND s.state = 'active')", + params![seed.workspace_id, seed.runtime_id], + |row| row.get(0), + )?; + if !active { + return Err(Error::InvalidInput( + "fixture requires an active bootstrapped Workspace and authenticated Runtime binding" + .into(), + )); + } + let worker_id = seed.worker_id.to_string(); + let allocation_exists: bool = tx.query_row( + "SELECT EXISTS(SELECT 1 FROM worker_create_reservations + WHERE workspace_id = ?1 AND allocation_key = ?2)", + params![seed.workspace_id, seed.allocation_key], + |row| row.get(0), + )?; + if allocation_exists { + return Err(Error::InvalidInput( + "fixture Worker allocation already exists".into(), + )); + } + // Check by WorkerId, not just the proposed Runtime tuple: orphaned and terminal + // ownership is still a collision. These tables are owned by this store schema. + for table in [ + "worker_create_reservations", + "worker_registry", + "worker_registry_observations", + "worker_registry_projection_removals", + "worker_removal_operations", + "worker_singleton_owners", + "worker_workdir_links", + "worker_restore_intents", + "worker_tombstones", + "worker_orphan_diagnostics", + "worker_registry_projection_diagnostics", + "worker_session_archives", + "worker_diagnostics_archives", + "ticket_assignment_worker_tombstones", + ] { + let exists: bool = tx.query_row( + &format!("SELECT EXISTS(SELECT 1 FROM {table} WHERE worker_id = ?1)"), + [&worker_id], + |row| row.get(0), + )?; + if exists { + return Err(Error::InvalidInput(format!( + "fixture Worker identity already exists in {table}" + ))); + } + } + // A resource key without a catalog/reservation must not be silently reused. + let mut statement = tx.prepare( + "SELECT resource_id FROM workspace_resource_keys WHERE resource_kind = 'worker'", + )?; + let resources = statement.query_map([], |row| row.get::<_, String>(0))?; + for resource in resources { + let resource = resource?; + let existing = resource + .is_ascii() + .then(|| parse_worker_resource_id(&resource)) + .flatten() + .ok_or_else(|| { + Error::InvalidInput("unknown fixture Worker resource identity collision".into()) + })?; + if existing.worker_id == worker_id { + return Err(Error::InvalidInput( + "fixture Worker resource identity already exists".into(), + )); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hosts::{ + WorkerSpawnAcceptanceRequirement, WorkerSpawnWorkingDirectoryAttachmentRequest, + WorkerSpawnWorkingDirectoryRequest, + }; + use crate::workspace_signing_identity::{ + InMemoryWorkspaceSigningMaterialStore, WorkspaceSigningIdentityService, + }; + + const WORKSPACE: &str = "0197a949-4b6b-7f2a-9d9a-1f87e3a4c501"; + const RUNTIME: &str = "fixture-runtime"; + const WORKER: &str = "0197a949-4b6b-7f2a-9d9a-1f87e3a4c502"; + + // Exercises the same public bootstrap APIs the offline harness uses. All account + // and key identities are newly generated, even across stores with fixed fixture IDs. + fn bootstrap() -> SqliteWorkspaceStore { + let store = SqliteWorkspaceStore::in_memory_for_test().unwrap(); + let account = AccountRecord { + account_id: format!("fixture-account-{}", Uuid::now_v7()), + kind: "user".into(), + handle: format!("fixture-{}", Uuid::now_v7()), + display_name: "Fixture owner".into(), + created_at: "2026-10-24T00:00:00Z".into(), + updated_at: "2026-10-24T00:00:00Z".into(), + }; + store.upsert_account(&account).unwrap(); + let identities = WorkspaceSigningIdentityService::new( + Arc::new(store.clone()), + Arc::new(InMemoryWorkspaceSigningMaterialStore::default()), + ); + let (identity, operation) = identities + .prepare_workspace_creation( + "fixture-bootstrap", + "fixture-bootstrap", + WORKSPACE, + &account.account_id, + ) + .unwrap(); + store + .create_workspace_bootstrap( + &WorkspaceBootstrapRecord { + operation_key: "fixture-bootstrap".into(), + request_fingerprint: "fixture-bootstrap".into(), + workspace: WorkspaceRecord { + workspace_id: WORKSPACE.into(), + owner_account_id: account.account_id, + display_name: "Fixture".into(), + state: "active".into(), + created_at: account.created_at, + updated_at: account.updated_at, + }, + repository: None, + }, + &identity, + &operation, + ) + .unwrap(); + let runtime_key = worker_runtime::auth::RuntimeIdentityMaterial::generate(RUNTIME).unwrap(); + store + .upsert_workspace_runtime_binding( + WorkspaceRuntimeBinding { + workspace_id: WORKSPACE.into(), + runtime_id: RUNTIME.into(), + display_name: "Fixture Runtime".into(), + base_url: "http://127.0.0.1:12345".into(), + public_key: runtime_key.public_key, + public_key_fingerprint: String::new(), + binding_id: String::new(), + authentication_mode: WorkspaceRuntimeAuthenticationMode::WorkspaceIdentity, + created_at: "2026-10-24T00:00:00Z".into(), + updated_at: "2026-10-24T00:00:00Z".into(), + revoked_at: None, + }, + false, + ) + .unwrap(); + store + } + + fn request() -> WorkerSpawnRequest { + WorkerSpawnRequest { + intent: WorkerSpawnIntent::WorkspaceCoding, + requested_worker_name: Some("Fixture Worker".into()), + singleton_key: None, + acceptance: WorkerSpawnAcceptanceRequirement::SocketReady, + profile: ProfileSelector::Builtin("builtin:coder".into()), + ticket_assignment: None, + initial_submit: Vec::new(), + workdir_attachment_requests: Vec::new(), + resolved_workdir_attachment_requests: Vec::new(), + resolved_workdir_attachments: Vec::new(), + resolved_config_bundle: None, + resolved_workspace_api: None, + resolved_memory_settings: None, + resolved_subjektiv_attached: false, + resolved_worker_observation_enabled: false, + resolved_worker_observation_grants: Vec::new(), + resolved_control_operation: None, + resolved_execution_grant: None, + } + } + + fn seed(request: &WorkerSpawnRequest) -> FixedWorkerSeed<'_> { + FixedWorkerSeed { + workspace_id: WORKSPACE, + runtime_id: RUNTIME, + worker_id: WORKER.parse().unwrap(), + allocation_key: "fixture-allocation", + request, + } + } + + fn worker() -> RuntimeWorkerRef { + RuntimeWorkerRef::new(RUNTIME, WORKER) + } + + #[test] + fn fixed_ids_are_isolated_and_seed_has_no_runtime_observation() { + let a = bootstrap(); + let b = bootstrap(); + let request = request(); + let first = seed_fixed_worker(&a, seed(&request)).unwrap(); + let second = seed_fixed_worker(&b, seed(&request)).unwrap(); + assert_eq!(first, second); + assert_eq!(first.worker_id.to_string(), WORKER); + assert_eq!(first.memory_settings.workspace_id, WORKSPACE); + assert_eq!(first.memory_settings.language, "English"); + assert!(first.singleton.is_none()); + assert_ne!( + a.get_workspace_signing_identity(WORKSPACE) + .unwrap() + .unwrap() + .public_key, + b.get_workspace_signing_identity(WORKSPACE) + .unwrap() + .unwrap() + .public_key + ); + assert_ne!( + a.get_workspace_runtime_binding(WORKSPACE, RUNTIME) + .unwrap() + .unwrap() + .public_key, + b.get_workspace_runtime_binding(WORKSPACE, RUNTIME) + .unwrap() + .unwrap() + .public_key + ); + for store in [&a, &b] { + assert_eq!( + store + .worker_resource_key(WORKSPACE, &worker()) + .unwrap() + .as_deref(), + Some("W-1") + ); + assert_eq!( + store + .resolve_worker_resource_reference(WORKSPACE, "W-1") + .unwrap(), + Some(worker()) + ); + assert!( + !store + .has_active_worker_create_reservation(WORKSPACE, &worker()) + .unwrap() + ); + let registry = store + .get_worker_registry(WORKSPACE, &worker()) + .unwrap() + .unwrap(); + assert_eq!(registry.profile.as_deref(), Some("builtin:coder")); + assert_eq!(registry.display_name, "Fixture Worker"); + assert_eq!(registry.retention_state, "normal"); + let projection = store + .with_conn(|conn| read_worker_registry_projection(conn, WORKSPACE, &worker())) + .unwrap() + .unwrap(); + assert!(projection.observation.is_none()); + assert!(projection.job.is_none()); + } + } + + #[test] + fn initial_submit_is_refused_without_registration_or_allocation() { + let store = bootstrap(); + let mut request = request(); + // Even an empty text segment is new input; the vector itself must be empty. + for text in ["new input must not be seeded", ""] { + request.initial_submit = vec![protocol::Segment::text(text)]; + let error = seed_fixed_worker(&store, seed(&request)).unwrap_err(); + assert!( + matches!(error, Error::InvalidInput(ref message) if message.contains("initial_submit")) + ); + assert!( + store + .get_worker_registry(WORKSPACE, &worker()) + .unwrap() + .is_none() + ); + assert!( + store + .worker_resource_key(WORKSPACE, &worker()) + .unwrap() + .is_none() + ); + assert!( + !store + .has_active_worker_create_reservation(WORKSPACE, &worker()) + .unwrap() + ); + } + // The rejected attempts must not consume the allocation or resource counter. + request.initial_submit.clear(); + seed_fixed_worker(&store, seed(&request)).unwrap(); + assert_eq!( + store + .worker_resource_key(WORKSPACE, &worker()) + .unwrap() + .as_deref(), + Some("W-1") + ); + } + + #[test] + fn exact_replay_changed_input_and_cross_runtime_identity_are_refused() { + let store = bootstrap(); + let mut request = request(); + seed_fixed_worker(&store, seed(&request)).unwrap(); + let before = store.get_worker_registry(WORKSPACE, &worker()).unwrap(); + assert!(seed_fixed_worker(&store, seed(&request)).is_err()); + request.requested_worker_name = Some("Must not overwrite".into()); + assert!(seed_fixed_worker(&store, seed(&request)).is_err()); + let mut new_allocation = seed(&request); + new_allocation.allocation_key = "another-allocation"; + assert!(seed_fixed_worker(&store, new_allocation).is_err()); + let mut same_allocation = seed(&request); + same_allocation.worker_id = WorkerId::now_v7(); + assert!(seed_fixed_worker(&store, same_allocation).is_err()); + assert_eq!( + store.get_worker_registry(WORKSPACE, &worker()).unwrap(), + before + ); + + let other = bootstrap(); + let mut record = before.unwrap(); + record.worker.runtime_id = "different-runtime".into(); + other.upsert_worker_registry(&record).unwrap(); + assert!(seed_fixed_worker(&other, seed(&request)).is_err()); + assert!( + other + .get_worker_registry(WORKSPACE, &worker()) + .unwrap() + .is_none() + ); + } + + #[test] + fn reserved_and_orphaned_resource_identities_are_not_adopted() { + let store = bootstrap(); + let request = request(); + let memory = store.get_workspace_memory_settings(WORKSPACE).unwrap(); + let ordinary = store + .reserve_worker_create(WORKSPACE, RUNTIME, "ordinary", "fingerprint", None, &memory) + .unwrap(); + let mut collision = seed(&request); + collision.worker_id = ordinary.worker_id; + assert!(seed_fixed_worker(&store, collision).is_err()); + // Store-level fault setup only: the external harness never writes SQL. + store + .with_conn(|conn| { + allocate_resource_key( + conn, + WORKSPACE, + WorkspaceResourceKind::Worker, + &worker_resource_id(&worker()), + "now", + )?; + Ok(()) + }) + .unwrap(); + assert!(seed_fixed_worker(&store, seed(&request)).is_err()); + assert!( + store + .get_worker_registry(WORKSPACE, &worker()) + .unwrap() + .is_none() + ); + } + + #[test] + fn unknown_resource_identity_and_retained_removal_fence_are_refused() { + let request = request(); + let malformed = bootstrap(); + malformed + .with_conn(|conn| { + allocate_resource_key( + conn, + WORKSPACE, + WorkspaceResourceKind::Worker, + "unknown-identity", + "now", + )?; + Ok(()) + }) + .unwrap(); + assert!(matches!( + seed_fixed_worker(&malformed, seed(&request)), + Err(Error::InvalidInput(_)) + )); + assert!( + malformed + .get_worker_registry(WORKSPACE, &worker()) + .unwrap() + .is_none() + ); + + let removed = bootstrap(); + removed.with_conn(|conn| { + conn.execute( + "INSERT INTO worker_registry_projection_removals(workspace_id, runtime_id, worker_id) VALUES (?1, ?2, ?3)", + params![WORKSPACE, RUNTIME, WORKER], + )?; + Ok(()) + }).unwrap(); + assert!(matches!( + seed_fixed_worker(&removed, seed(&request)), + Err(Error::InvalidInput(_)) + )); + assert!( + removed + .worker_resource_key(WORKSPACE, &worker()) + .unwrap() + .is_none() + ); + } + + #[test] + fn unknown_workspace_runtime_and_revoked_binding_are_refused() { + let store = bootstrap(); + let request = request(); + let mut unknown = seed(&request); + unknown.workspace_id = "unknown"; + assert!(seed_fixed_worker(&store, unknown).is_err()); + let mut unknown = seed(&request); + unknown.runtime_id = "unknown"; + assert!(seed_fixed_worker(&store, unknown).is_err()); + store + .revoke_workspace_runtime_binding(WORKSPACE, RUNTIME, "2026-10-24T01:00:00Z") + .unwrap(); + assert!(seed_fixed_worker(&store, seed(&request)).is_err()); + assert!( + store + .worker_resource_key(WORKSPACE, &worker()) + .unwrap() + .is_none() + ); + } + + #[test] + fn unsupported_intent_is_refused_without_partial_registration() { + let store = bootstrap(); + let base = request(); + let mut requests = Vec::new(); + let mut request = base.clone(); + request.singleton_key = Some("fixture-singleton".into()); + requests.push(request); + let mut request = base.clone(); + request.intent = WorkerSpawnIntent::WorkspaceOrchestrator; + requests.push(request); + let mut request = base.clone(); + request.resolved_subjektiv_attached = true; + requests.push(request); + let mut request = base.clone(); + request + .workdir_attachment_requests + .push(WorkerSpawnWorkingDirectoryAttachmentRequest { + alias: workdir::WorkdirAttachmentAlias::new("repo").unwrap(), + working_directory: WorkerSpawnWorkingDirectoryRequest { + repository_key: "repo".into(), + selector: None, + }, + }); + requests.push(request); + for request in requests { + assert!(seed_fixed_worker(&store, seed(&request)).is_err()); + } + assert!( + store + .get_worker_registry(WORKSPACE, &worker()) + .unwrap() + .is_none() + ); + assert!( + store + .worker_resource_key(WORKSPACE, &worker()) + .unwrap() + .is_none() + ); + assert_eq!( + seed_fixed_worker(&store, seed(&base)) + .unwrap() + .worker_id + .to_string(), + WORKER + ); + } + + #[test] + fn late_catalog_failure_rolls_back_reservation_and_resource_allocation() { + let store = bootstrap(); + let request = request(); + store + .with_conn(|conn| { + conn.execute_batch( + "CREATE TEMP TRIGGER fail_fixture_catalog BEFORE INSERT ON worker_registry + BEGIN SELECT RAISE(ABORT, 'injected catalog failure'); END", + )?; + Ok(()) + }) + .unwrap(); + assert!(seed_fixed_worker(&store, seed(&request)).is_err()); + assert!( + !store + .has_active_worker_create_reservation(WORKSPACE, &worker()) + .unwrap() + ); + assert!( + store + .worker_resource_key(WORKSPACE, &worker()) + .unwrap() + .is_none() + ); + store + .with_conn(|conn| { + conn.execute_batch("DROP TRIGGER fail_fixture_catalog")?; + Ok(()) + }) + .unwrap(); + seed_fixed_worker(&store, seed(&request)).unwrap(); + assert_eq!( + store + .worker_resource_key(WORKSPACE, &worker()) + .unwrap() + .as_deref(), + Some("W-1") + ); + } + + #[test] + fn ordinary_creation_still_allocates_and_replays_without_catalog_insertion() { + let store = bootstrap(); + let memory = store.get_workspace_memory_settings(WORKSPACE).unwrap(); + let reserve = |key| { + store + .reserve_worker_create(WORKSPACE, RUNTIME, key, "fingerprint", None, &memory) + .unwrap() + }; + let first = reserve("ordinary-one"); + assert_eq!(first, reserve("ordinary-one")); + let second = reserve("ordinary-two"); + assert_ne!(first.worker_id, second.worker_id); + assert_ne!(first.worker_id.to_string(), WORKER); + assert_eq!( + first.worker_id.as_uuid().get_version(), + Some(uuid::Version::SortRand) + ); + let worker = RuntimeWorkerRef::new(RUNTIME, first.worker_id.to_string()); + assert!( + store + .has_active_worker_create_reservation(WORKSPACE, &worker) + .unwrap() + ); + assert!( + store + .get_worker_registry(WORKSPACE, &worker) + .unwrap() + .is_none() + ); + assert!( + store + .reserve_worker_create(WORKSPACE, RUNTIME, "ordinary-one", "changed", None, &memory) + .is_err() + ); + } +}