Skip to main content

alien_core/resources/
worker.rs

1use crate::error::{ErrorData, Result};
2use crate::resource::{ResourceDefinition, ResourceOutputsDefinition, ResourceRef, ResourceType};
3use crate::LoadBalancerEndpoint;
4use alien_error::AlienError;
5use bon::Builder;
6use serde::{Deserialize, Serialize};
7use std::any::Any;
8use std::collections::HashMap;
9use std::fmt::Debug;
10
11/// Controls network accessibility of the worker.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
14#[serde(rename_all = "camelCase")]
15pub enum Ingress {
16    /// Worker accessible from the internet with an HTTPS URL
17    Public,
18    /// Worker accessible only from within the customer's cloud via triggers, other workers, etc. No external HTTP URL is created.
19    Private,
20}
21
22/// Specifies the source of the worker's executable code.
23/// This can be a pre-built container image or source code that the system will build.
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
26#[serde(rename_all = "camelCase", tag = "type")]
27pub enum WorkerCode {
28    /// Container image.
29    #[serde(rename_all = "camelCase")]
30    Image {
31        /// Container image (e.g., `ghcr.io/myorg/myimage:latest`).
32        image: String,
33    },
34    /// Source code to be built.
35    #[serde(rename_all = "camelCase")]
36    Source {
37        /// The source directory to build from
38        src: String,
39        /// Toolchain configuration with type-safe options
40        toolchain: ToolchainConfig,
41    },
42}
43
44/// Configuration for different programming language toolchains.
45/// Each toolchain provides type-safe build configuration and auto-detection capabilities.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
48#[serde(rename_all = "lowercase", tag = "type")]
49pub enum ToolchainConfig {
50    /// Rust with Cargo build system
51    #[serde(rename_all = "camelCase")]
52    Rust {
53        /// Name of the binary to build and run
54        binary_name: String,
55    },
56    /// TypeScript/JavaScript compiled to single executable with Bun
57    #[serde(rename_all = "camelCase")]
58    TypeScript {
59        /// Name of the compiled binary (defaults to package.json name if not specified)
60        #[serde(default, skip_serializing_if = "Option::is_none")]
61        binary_name: Option<String>,
62    },
63    /// Docker build from Dockerfile
64    #[serde(rename_all = "camelCase")]
65    Docker {
66        /// Dockerfile path relative to src (default: "Dockerfile")
67        #[serde(skip_serializing_if = "Option::is_none")]
68        dockerfile: Option<String>,
69        /// Build arguments for docker build
70        #[serde(skip_serializing_if = "Option::is_none")]
71        build_args: Option<HashMap<String, String>>,
72        /// Multi-stage build target
73        #[serde(skip_serializing_if = "Option::is_none")]
74        target: Option<String>,
75    },
76}
77
78/// Defines what triggers a worker execution.
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
81#[serde(tag = "type", rename_all = "camelCase")]
82pub enum WorkerTrigger {
83    /// Worker triggered by queue messages (always 1 message per invocation)
84    Queue {
85        /// Reference to the queue resource
86        queue: ResourceRef,
87    },
88    /// Worker triggered by storage events (object created, deleted, etc.)
89    Storage {
90        /// Reference to the storage resource
91        storage: ResourceRef,
92        /// Events to trigger on (e.g., ["created", "deleted"])
93        events: Vec<String>,
94    },
95    /// Worker triggered on a schedule (cron expression)
96    Schedule {
97        /// Cron expression for scheduling (standard 5-field unix cron)
98        cron: String,
99    },
100}
101
102impl WorkerTrigger {
103    /// Creates a queue trigger for the specified queue resource.
104    /// The worker will be automatically invoked when messages arrive in the queue.
105    /// Each message is processed individually (batch size of 1).
106    pub fn queue<R: ?Sized>(queue: &R) -> Self
107    where
108        for<'a> &'a R: Into<ResourceRef>,
109    {
110        let queue_ref: ResourceRef = queue.into();
111        WorkerTrigger::Queue { queue: queue_ref }
112    }
113
114    /// Creates a storage trigger for the specified storage resource.
115    /// The worker will be invoked when matching events occur on the storage resource.
116    pub fn storage<R: ?Sized>(storage: &R, events: Vec<String>) -> Self
117    where
118        for<'a> &'a R: Into<ResourceRef>,
119    {
120        let storage_ref: ResourceRef = storage.into();
121        WorkerTrigger::Storage {
122            storage: storage_ref,
123            events,
124        }
125    }
126
127    /// Creates a schedule trigger with the specified cron expression.
128    /// Uses standard 5-field unix cron format (minute hour day-of-month month day-of-week).
129    pub fn schedule<S: Into<String>>(cron: S) -> Self {
130        WorkerTrigger::Schedule { cron: cron.into() }
131    }
132}
133
134/// Represents a serverless worker that executes code in response to triggers or direct invocations.
135/// Workers are the primary compute resource in serverless applications, designed to be stateless and ephemeral.
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
137#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
138#[serde(rename_all = "camelCase", deny_unknown_fields)]
139#[builder(start_fn = new)]
140pub struct Worker {
141    /// Identifier for the worker. Must contain only alphanumeric characters, hyphens, and underscores ([A-Za-z0-9-_]).
142    /// Maximum 64 characters.
143    #[builder(start_fn)]
144    pub id: String,
145
146    /// List of resource references this worker depends on.
147    // TODO: We need to verify that the same link isn't added multiple times.
148    #[builder(field)]
149    pub links: Vec<ResourceRef>,
150
151    /// List of triggers that define what events automatically invoke this worker.
152    /// If empty, the worker is only invokable directly via HTTP calls or platform-specific invocation APIs.
153    /// When configured, the worker will be automatically invoked when any of the specified trigger conditions are met.
154    #[builder(field)]
155    pub triggers: Vec<WorkerTrigger>,
156
157    /// Permission profile name that defines the permissions granted to this worker.
158    /// This references a profile defined in the stack's permission definitions.
159    pub permissions: String,
160
161    /// Code for the worker, either a pre-built image or source code to be built.
162    pub code: WorkerCode,
163
164    /// Memory allocated to the worker in megabytes (MB).
165    /// Default: 256
166    ///
167    /// Platform-specific constraints:
168    /// - **AWS Lambda**: 128–10240 MB in 1 MB increments
169    /// - **GCP Cloud Run**: 128–32768 MB
170    /// - **Azure Container Apps**: fixed CPU/memory pairs — 512, 1024, 1536, 2048, 2560,
171    ///   3072, 3584, 4096 MB. Values below 512 are automatically rounded up at deploy time.
172    #[builder(default = default_memory_mb())]
173    #[serde(default = "default_memory_mb")]
174    #[cfg_attr(feature = "openapi", schema(default = default_memory_mb))]
175    pub memory_mb: u32,
176
177    /// Maximum execution time for the worker in seconds.
178    /// Constraints: 1‑3600 seconds (platform-specific limits may apply)
179    /// Default: 30
180    #[builder(default = default_timeout_seconds())]
181    #[serde(default = "default_timeout_seconds")]
182    #[cfg_attr(feature = "openapi", schema(default = default_timeout_seconds))]
183    pub timeout_seconds: u32,
184
185    /// Key-value pairs to set as environment variables for the worker.
186    #[builder(default)]
187    #[serde(default)]
188    pub environment: HashMap<String, String>,
189
190    /// Controls network accessibility of the worker.
191    #[builder(default = default_ingress())]
192    #[serde(default = "default_ingress")]
193    #[cfg_attr(feature = "openapi", schema(default = default_ingress))]
194    pub ingress: Ingress,
195
196    /// Whether the worker can receive remote commands via the Commands protocol.
197    /// When enabled, the runtime polls the manager for pending commands and executes registered handlers.
198    #[builder(default = default_commands_enabled())]
199    #[serde(default = "default_commands_enabled")]
200    #[cfg_attr(feature = "openapi", schema(default = default_commands_enabled))]
201    pub commands_enabled: bool,
202
203    /// Maximum number of concurrent executions allowed for the worker.
204    /// None means platform default applies.
205    pub concurrency_limit: Option<u32>,
206
207    /// Optional readiness probe configuration.
208    /// Only applicable for workers with Public ingress.
209    /// When configured, the probe will be executed after provisioning/update to verify the worker is ready.
210    pub readiness_probe: Option<ReadinessProbe>,
211}
212
213impl Worker {
214    /// The resource type identifier for Workers
215    pub const RESOURCE_TYPE: ResourceType = ResourceType::from_static("worker");
216
217    /// Returns the permission profile name for this worker.
218    pub fn get_permissions(&self) -> &str {
219        &self.permissions
220    }
221}
222
223fn default_memory_mb() -> u32 {
224    256
225}
226
227fn default_timeout_seconds() -> u32 {
228    180
229}
230
231fn default_ingress() -> Ingress {
232    Ingress::Private
233}
234
235fn default_commands_enabled() -> bool {
236    false
237}
238
239/// HTTP method for readiness probe requests.
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
241#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
242#[serde(rename_all = "UPPERCASE")]
243#[derive(Default)]
244pub enum HttpMethod {
245    #[default]
246    Get,
247    Post,
248    Put,
249    Delete,
250    Head,
251    Options,
252    Patch,
253}
254
255/// Configuration for HTTP-based readiness probe.
256/// This probe is executed after worker provisioning/update to verify the worker is ready to serve traffic.
257/// Only works with workers that have Public ingress.
258#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
259#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
260#[serde(rename_all = "camelCase")]
261pub struct ReadinessProbe {
262    /// HTTP method to use for the probe request.
263    /// Default: GET
264    #[serde(default)]
265    pub method: HttpMethod,
266
267    /// Path to request for the probe (e.g., "/health", "/ready").
268    /// Default: "/"
269    #[serde(default = "default_probe_path")]
270    pub path: String,
271}
272
273fn default_probe_path() -> String {
274    "/".to_string()
275}
276
277impl Default for ReadinessProbe {
278    fn default() -> Self {
279        Self {
280            method: HttpMethod::default(),
281            path: default_probe_path(),
282        }
283    }
284}
285
286use crate::resources::worker::worker_builder::State;
287
288impl<S: State> WorkerBuilder<S> {
289    /// Links the worker to another resource with specified permissions.
290    /// Accepts a reference to any type `R` where `&R` can be converted into `ResourceRef`.
291    pub fn link<R: ?Sized>(mut self, resource: &R) -> Self
292    where
293        for<'a> &'a R: Into<ResourceRef>, // Use Higher-Rank Trait Bound (HRTB)
294    {
295        // Perform the conversion from &R to ResourceRef using .into()
296        let resource_ref: ResourceRef = resource.into();
297        self.links.push(resource_ref);
298        self
299    }
300
301    /// Adds a trigger to the worker. Workers can have multiple triggers.
302    /// Each trigger will independently invoke the worker when its conditions are met.
303    ///
304    /// # Examples
305    /// ```rust
306    /// # use alien_core::{Worker, WorkerTrigger, WorkerCode, Queue};
307    /// # let queue1 = Queue::new("queue-1".to_string()).build();
308    /// # let queue2 = Queue::new("queue-2".to_string()).build();
309    /// let worker = Worker::new("my-worker".to_string())
310    ///     .code(WorkerCode::Image { image: "test:latest".to_string() })
311    ///     .permissions("execution".to_string())
312    ///     .trigger(WorkerTrigger::queue(&queue1))
313    ///     .trigger(WorkerTrigger::queue(&queue2))
314    ///     .build();
315    /// ```
316    pub fn trigger(mut self, trigger: WorkerTrigger) -> Self {
317        self.triggers.push(trigger);
318        self
319    }
320}
321
322// Implementation of ResourceDefinition trait for Worker
323impl ResourceDefinition for Worker {
324    fn get_resource_type(&self) -> ResourceType {
325        Self::RESOURCE_TYPE
326    }
327
328    fn id(&self) -> &str {
329        &self.id
330    }
331
332    fn get_dependencies(&self) -> Vec<ResourceRef> {
333        let mut dependencies = self.links.clone();
334
335        // Add trigger dependencies
336        for trigger in &self.triggers {
337            match trigger {
338                WorkerTrigger::Queue { queue } => {
339                    dependencies.push(queue.clone());
340                }
341                WorkerTrigger::Storage { storage, .. } => {
342                    dependencies.push(storage.clone());
343                }
344                WorkerTrigger::Schedule { .. } => {
345                    // Schedule triggers don't depend on other resources
346                }
347            }
348        }
349
350        dependencies
351    }
352
353    fn get_permissions(&self) -> Option<&str> {
354        Some(&self.permissions)
355    }
356
357    fn validate_update(&self, new_config: &dyn ResourceDefinition) -> Result<()> {
358        // Downcast to Worker type to use the existing validate_update method
359        let new_worker = new_config
360            .as_any()
361            .downcast_ref::<Worker>()
362            .ok_or_else(|| {
363                AlienError::new(ErrorData::UnexpectedResourceType {
364                    resource_id: self.id.clone(),
365                    expected: Self::RESOURCE_TYPE,
366                    actual: new_config.get_resource_type(),
367                })
368            })?;
369
370        if self.id != new_worker.id {
371            return Err(AlienError::new(ErrorData::InvalidResourceUpdate {
372                resource_id: self.id.clone(),
373                reason: "the 'id' field is immutable".to_string(),
374            }));
375        }
376        Ok(())
377    }
378
379    fn as_any(&self) -> &dyn Any {
380        self
381    }
382
383    fn as_any_mut(&mut self) -> &mut dyn Any {
384        self
385    }
386
387    fn box_clone(&self) -> Box<dyn ResourceDefinition> {
388        Box::new(self.clone())
389    }
390
391    fn resource_eq(&self, other: &dyn ResourceDefinition) -> bool {
392        other.as_any().downcast_ref::<Worker>() == Some(self)
393    }
394
395    fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
396        serde_json::to_value(self)
397    }
398}
399
400/// Outputs generated by a successfully provisioned Worker.
401#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
402#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
403#[serde(rename_all = "camelCase")]
404pub struct WorkerOutputs {
405    /// The platform-specific worker name.
406    pub worker_name: String,
407    /// The invocation URL (if applicable, e.g., for public ingress or specific platforms).
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub url: Option<String>,
410    /// The ARN or platform-specific identifier.
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub identifier: Option<String>,
413    /// Load balancer endpoint information for DNS management (optional).
414    /// Used by the DNS controller to create custom domain mappings.
415    #[serde(skip_serializing_if = "Option::is_none")]
416    pub load_balancer_endpoint: Option<LoadBalancerEndpoint>,
417    /// Push target for commands delivery. Platform-specific:
418    /// - AWS: Lambda function name or ARN
419    /// - GCP: Full Pub/Sub topic path (projects/{project}/topics/{topic})
420    /// - Azure: Service Bus "{namespace}/{queue}"
421    #[serde(default, skip_serializing_if = "Option::is_none")]
422    pub commands_push_target: Option<String>,
423}
424
425impl ResourceOutputsDefinition for WorkerOutputs {
426    fn get_resource_type(&self) -> ResourceType {
427        Worker::RESOURCE_TYPE.clone()
428    }
429
430    fn as_any(&self) -> &dyn Any {
431        self
432    }
433
434    fn box_clone(&self) -> Box<dyn ResourceOutputsDefinition> {
435        Box::new(self.clone())
436    }
437
438    fn outputs_eq(&self, other: &dyn ResourceOutputsDefinition) -> bool {
439        other.as_any().downcast_ref::<WorkerOutputs>() == Some(self)
440    }
441
442    fn to_json_value(&self) -> serde_json::Result<serde_json::Value> {
443        serde_json::to_value(self)
444    }
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450    use crate::Storage;
451
452    #[test]
453    fn test_worker_builder_direct_refs() {
454        let dummy_storage = Storage::new("test-storage".to_string()).build();
455        let dummy_storage_2 = Storage::new("test-storage-2".to_string()).build();
456
457        let worker = Worker::new("my-worker".to_string())
458            .code(WorkerCode::Image {
459                image: "test-image".to_string(),
460            })
461            .permissions("execution".to_string())
462            .link(&dummy_storage) // Pass reference directly
463            .link(&dummy_storage_2) // Add a second link
464            .build();
465
466        assert_eq!(worker.id, "my-worker");
467        assert_eq!(
468            worker.code,
469            WorkerCode::Image {
470                image: "test-image".to_string()
471            }
472        );
473
474        // Verify permissions was set correctly
475        assert_eq!(worker.permissions, "execution");
476
477        // Verify links were added correctly
478        assert!(worker
479            .links
480            .contains(&ResourceRef::new(Storage::RESOURCE_TYPE, "test-storage")));
481        assert!(worker
482            .links
483            .contains(&ResourceRef::new(Storage::RESOURCE_TYPE, "test-storage-2")));
484        assert_eq!(worker.links.len(), 2); // Expect 2 links now
485    }
486
487    #[test]
488    fn test_worker_with_readiness_probe() {
489        let probe = ReadinessProbe {
490            method: HttpMethod::Post,
491            path: "/health".to_string(),
492        };
493
494        let worker = Worker::new("my-worker".to_string())
495            .code(WorkerCode::Image {
496                image: "test-image".to_string(),
497            })
498            .permissions("execution".to_string())
499            .ingress(Ingress::Public)
500            .readiness_probe(probe.clone())
501            .build();
502
503        assert_eq!(worker.id, "my-worker");
504        assert_eq!(worker.ingress, Ingress::Public);
505        assert_eq!(worker.readiness_probe, Some(probe));
506    }
507
508    #[test]
509    fn test_readiness_probe_defaults() {
510        let probe = ReadinessProbe::default();
511        assert_eq!(probe.method, HttpMethod::Get);
512        assert_eq!(probe.path, "/");
513    }
514
515    #[test]
516    fn test_worker_with_rust_toolchain() {
517        let worker = Worker::new("my-rust-worker".to_string())
518            .code(WorkerCode::Source {
519                src: "./".to_string(),
520                toolchain: ToolchainConfig::Rust {
521                    binary_name: "my-app".to_string(),
522                },
523            })
524            .permissions("execution".to_string())
525            .build();
526
527        assert_eq!(worker.id, "my-rust-worker");
528
529        match &worker.code {
530            WorkerCode::Source { src, toolchain } => {
531                assert_eq!(src, "./");
532                assert_eq!(
533                    toolchain,
534                    &ToolchainConfig::Rust {
535                        binary_name: "my-app".to_string(),
536                    }
537                );
538            }
539            _ => panic!("Expected Source code"),
540        }
541    }
542
543    #[test]
544    fn test_worker_with_typescript_toolchain() {
545        let worker = Worker::new("my-ts-worker".to_string())
546            .code(WorkerCode::Source {
547                src: "./".to_string(),
548                toolchain: ToolchainConfig::TypeScript {
549                    binary_name: Some("my-ts-worker".to_string()),
550                },
551            })
552            .permissions("execution".to_string())
553            .build();
554
555        assert_eq!(worker.id, "my-ts-worker");
556
557        match &worker.code {
558            WorkerCode::Source { src, toolchain } => {
559                assert_eq!(src, "./");
560                assert_eq!(
561                    toolchain,
562                    &ToolchainConfig::TypeScript {
563                        binary_name: Some("my-ts-worker".to_string())
564                    }
565                );
566            }
567            _ => panic!("Expected Source code"),
568        }
569    }
570
571    #[test]
572    fn test_worker_with_queue_trigger() {
573        use crate::Queue;
574
575        let queue = Queue::new("test-queue".to_string()).build();
576
577        let worker = Worker::new("triggered-worker".to_string())
578            .code(WorkerCode::Image {
579                image: "test-image".to_string(),
580            })
581            .permissions("execution".to_string())
582            .trigger(WorkerTrigger::queue(&queue))
583            .build();
584
585        assert_eq!(worker.triggers.len(), 1);
586        if let WorkerTrigger::Queue { queue: queue_ref } = &worker.triggers[0] {
587            assert_eq!(queue_ref.resource_type, Queue::RESOURCE_TYPE);
588            assert_eq!(queue_ref.id, "test-queue");
589        } else {
590            panic!("Expected queue trigger");
591        }
592    }
593
594    #[test]
595    fn test_worker_trigger_dependencies() {
596        use crate::Queue;
597
598        let queue = Queue::new("test-queue".to_string()).build();
599        let storage = Storage::new("test-storage".to_string()).build();
600
601        let worker = Worker::new("triggered-worker".to_string())
602            .code(WorkerCode::Image {
603                image: "test-image".to_string(),
604            })
605            .permissions("execution".to_string())
606            .link(&storage) // regular link dependency
607            .trigger(WorkerTrigger::queue(&queue)) // trigger dependency
608            .build();
609
610        let dependencies = worker.get_dependencies();
611
612        // Should have both link and trigger dependencies
613        assert_eq!(dependencies.len(), 2);
614        assert!(dependencies.contains(&ResourceRef::new(Storage::RESOURCE_TYPE, "test-storage")));
615        assert!(dependencies.contains(&ResourceRef::new(Queue::RESOURCE_TYPE, "test-queue")));
616    }
617
618    #[test]
619    fn test_worker_trigger_helper_methods() {
620        use crate::Queue;
621
622        let queue = Queue::new("my-queue".to_string()).build();
623
624        // Test the helper method
625        let trigger = WorkerTrigger::queue(&queue);
626
627        if let WorkerTrigger::Queue { queue: queue_ref } = trigger {
628            assert_eq!(queue_ref.resource_type, Queue::RESOURCE_TYPE);
629            assert_eq!(queue_ref.id, "my-queue");
630        } else {
631            panic!("Expected queue trigger");
632        }
633    }
634
635    #[test]
636    fn test_worker_with_multiple_triggers() {
637        use crate::Queue;
638
639        let queue1 = Queue::new("queue-1".to_string()).build();
640        let queue2 = Queue::new("queue-2".to_string()).build();
641
642        let worker = Worker::new("multi-triggered-worker".to_string())
643            .code(WorkerCode::Image {
644                image: "test-image".to_string(),
645            })
646            .permissions("execution".to_string())
647            .trigger(WorkerTrigger::queue(&queue1))
648            .trigger(WorkerTrigger::queue(&queue2))
649            .trigger(WorkerTrigger::schedule("0 * * * *".to_string()))
650            .build();
651
652        assert_eq!(worker.triggers.len(), 3);
653
654        // Check first queue trigger
655        if let WorkerTrigger::Queue { queue: queue_ref } = &worker.triggers[0] {
656            assert_eq!(queue_ref.id, "queue-1");
657        } else {
658            panic!("Expected first trigger to be queue-1");
659        }
660
661        // Check second queue trigger
662        if let WorkerTrigger::Queue { queue: queue_ref } = &worker.triggers[1] {
663            assert_eq!(queue_ref.id, "queue-2");
664        } else {
665            panic!("Expected second trigger to be queue-2");
666        }
667
668        // Check schedule trigger
669        if let WorkerTrigger::Schedule { cron } = &worker.triggers[2] {
670            assert_eq!(cron, "0 * * * *");
671        } else {
672            panic!("Expected third trigger to be schedule");
673        }
674
675        // Check dependencies include both queues
676        let dependencies = worker.get_dependencies();
677        assert_eq!(dependencies.len(), 2); // Only queues, schedule has no dependency
678        assert!(dependencies.contains(&ResourceRef::new(Queue::RESOURCE_TYPE, "queue-1")));
679        assert!(dependencies.contains(&ResourceRef::new(Queue::RESOURCE_TYPE, "queue-2")));
680    }
681
682    #[test]
683    fn test_worker_with_commands_enabled() {
684        let worker = Worker::new("cmd-worker".to_string())
685            .code(WorkerCode::Image {
686                image: "test-image".to_string(),
687            })
688            .permissions("execution".to_string())
689            .ingress(Ingress::Private)
690            .commands_enabled(true)
691            .build();
692
693        assert_eq!(worker.id, "cmd-worker");
694        assert_eq!(worker.ingress, Ingress::Private);
695        assert_eq!(worker.commands_enabled, true);
696    }
697
698    #[test]
699    fn test_worker_defaults() {
700        let worker = Worker::new("default-worker".to_string())
701            .code(WorkerCode::Image {
702                image: "test-image".to_string(),
703            })
704            .permissions("execution".to_string())
705            .build();
706
707        // Test that defaults are applied correctly
708        assert_eq!(worker.ingress, Ingress::Private);
709        assert_eq!(worker.commands_enabled, false);
710        assert_eq!(worker.memory_mb, 256);
711        assert_eq!(worker.timeout_seconds, 180);
712    }
713
714    #[test]
715    fn test_worker_public_ingress_with_commands() {
716        let worker = Worker::new("public-cmd-worker".to_string())
717            .code(WorkerCode::Image {
718                image: "test-image".to_string(),
719            })
720            .permissions("execution".to_string())
721            .ingress(Ingress::Public)
722            .commands_enabled(true)
723            .build();
724
725        assert_eq!(worker.ingress, Ingress::Public);
726        assert_eq!(worker.commands_enabled, true);
727    }
728}