making linters happy
Some checks failed
CI / Rust Blocking Checks (push) Failing after 22s
CI / Web Blocking Checks (push) Failing after 26s
CI / Security Blocking Checks (push) Successful in 9s
CI / Web Advisory Checks (push) Successful in 32s
CI / Security Advisory Checks (push) Has been cancelled

This commit is contained in:
2026-03-04 23:44:45 -06:00
parent 6a5a3c2b78
commit 13749409cd
81 changed files with 468 additions and 599 deletions

View File

@@ -107,7 +107,7 @@ impl HistoryQueryParams {
pub fn to_repo_params( pub fn to_repo_params(
&self, &self,
) -> attune_common::repositories::entity_history::HistoryQueryParams { ) -> attune_common::repositories::entity_history::HistoryQueryParams {
let limit = (self.page_size.min(1000).max(1)) as i64; let limit = (self.page_size.clamp(1, 1000)) as i64;
let offset = ((self.page.saturating_sub(1)) as i64) * limit; let offset = ((self.page.saturating_sub(1)) as i64) * limit;
attune_common::repositories::entity_history::HistoryQueryParams { attune_common::repositories::entity_history::HistoryQueryParams {

View File

@@ -160,7 +160,10 @@ pub async fn create_action(
request.validate()?; request.validate()?;
// Check if action with same ref already exists // Check if action with same ref already exists
if let Some(_) = ActionRepository::find_by_ref(&state.db, &request.r#ref).await? { if ActionRepository::find_by_ref(&state.db, &request.r#ref)
.await?
.is_some()
{
return Err(ApiError::Conflict(format!( return Err(ApiError::Conflict(format!(
"Action with ref '{}' already exists", "Action with ref '{}' already exists",
request.r#ref request.r#ref

View File

@@ -1877,7 +1877,7 @@ pub async fn stream_artifact(
Some(( Some((
Ok(Event::default() Ok(Event::default()
.event("content") .event("content")
.data(String::from_utf8_lossy(&buf).into_owned())), .data(String::from_utf8_lossy(&buf))),
TailState::Tailing { TailState::Tailing {
full_path, full_path,
file_path, file_path,
@@ -1967,7 +1967,7 @@ pub async fn stream_artifact(
Some(( Some((
Ok(Event::default() Ok(Event::default()
.event("append") .event("append")
.data(String::from_utf8_lossy(&new_buf).into_owned())), .data(String::from_utf8_lossy(&new_buf))),
TailState::Tailing { TailState::Tailing {
full_path, full_path,
file_path, file_path,

View File

@@ -158,7 +158,10 @@ pub async fn register(
.map_err(|e| ApiError::ValidationError(format!("Invalid registration request: {}", e)))?; .map_err(|e| ApiError::ValidationError(format!("Invalid registration request: {}", e)))?;
// Check if login already exists // Check if login already exists
if let Some(_) = IdentityRepository::find_by_login(&state.db, &payload.login).await? { if IdentityRepository::find_by_login(&state.db, &payload.login)
.await?
.is_some()
{
return Err(ApiError::Conflict(format!( return Err(ApiError::Conflict(format!(
"Identity with login '{}' already exists", "Identity with login '{}' already exists",
payload.login payload.login

View File

@@ -138,7 +138,10 @@ pub async fn create_key(
request.validate()?; request.validate()?;
// Check if key with same ref already exists // Check if key with same ref already exists
if let Some(_) = KeyRepository::find_by_ref(&state.db, &request.r#ref).await? { if KeyRepository::find_by_ref(&state.db, &request.r#ref)
.await?
.is_some()
{
return Err(ApiError::Conflict(format!( return Err(ApiError::Conflict(format!(
"Key with ref '{}' already exists", "Key with ref '{}' already exists",
request.r#ref request.r#ref

View File

@@ -585,10 +585,9 @@ pub async fn upload_pack(
skip_tests, skip_tests,
) )
.await .await
.map_err(|e| { .inspect_err(|_e| {
// Clean up permanent storage on failure // Clean up permanent storage on failure
let _ = std::fs::remove_dir_all(&final_path); let _ = std::fs::remove_dir_all(&final_path);
e
})?; })?;
// Fetch the registered pack // Fetch the registered pack
@@ -947,8 +946,8 @@ async fn register_pack_internal(
// a best-effort optimisation for non-Docker (bare-metal) setups // a best-effort optimisation for non-Docker (bare-metal) setups
// where the API host has the interpreter available. // where the API host has the interpreter available.
if let Some(ref env_cfg) = exec_config.environment { if let Some(ref env_cfg) = exec_config.environment {
if env_cfg.env_type != "none" { if env_cfg.env_type != "none"
if !env_dir.exists() && !env_cfg.create_command.is_empty() { && !env_dir.exists() && !env_cfg.create_command.is_empty() {
// Ensure parent directories exist // Ensure parent directories exist
if let Some(parent) = env_dir.parent() { if let Some(parent) = env_dir.parent() {
let _ = std::fs::create_dir_all(parent); let _ = std::fs::create_dir_all(parent);
@@ -1002,7 +1001,6 @@ async fn register_pack_internal(
} }
} }
} }
}
} }
// Attempt to install dependencies if manifest file exists. // Attempt to install dependencies if manifest file exists.
@@ -1107,9 +1105,7 @@ async fn register_pack_internal(
if is_new_pack { if is_new_pack {
let _ = PackRepository::delete(&state.db, pack.id).await; let _ = PackRepository::delete(&state.db, pack.id).await;
} }
return Err(ApiError::BadRequest(format!( return Err(ApiError::BadRequest("Pack registration failed: tests did not pass. Use force=true to register anyway.".to_string()));
"Pack registration failed: tests did not pass. Use force=true to register anyway."
)));
} }
if !test_passed && force { if !test_passed && force {
@@ -1359,10 +1355,9 @@ pub async fn install_pack(
request.skip_tests, request.skip_tests,
) )
.await .await
.map_err(|e| { .inspect_err(|_e| {
// Clean up the permanent storage if registration fails // Clean up the permanent storage if registration fails
let _ = std::fs::remove_dir_all(&final_path); let _ = std::fs::remove_dir_all(&final_path);
e
})?; })?;
// Fetch the registered pack // Fetch the registered pack

View File

@@ -290,7 +290,10 @@ pub async fn create_rule(
request.validate()?; request.validate()?;
// Check if rule with same ref already exists // Check if rule with same ref already exists
if let Some(_) = RuleRepository::find_by_ref(&state.db, &request.r#ref).await? { if RuleRepository::find_by_ref(&state.db, &request.r#ref)
.await?
.is_some()
{
return Err(ApiError::Conflict(format!( return Err(ApiError::Conflict(format!(
"Rule with ref '{}' already exists", "Rule with ref '{}' already exists",
request.r#ref request.r#ref

View File

@@ -198,7 +198,10 @@ pub async fn create_trigger(
request.validate()?; request.validate()?;
// Check if trigger with same ref already exists // Check if trigger with same ref already exists
if let Some(_) = TriggerRepository::find_by_ref(&state.db, &request.r#ref).await? { if TriggerRepository::find_by_ref(&state.db, &request.r#ref)
.await?
.is_some()
{
return Err(ApiError::Conflict(format!( return Err(ApiError::Conflict(format!(
"Trigger with ref '{}' already exists", "Trigger with ref '{}' already exists",
request.r#ref request.r#ref
@@ -623,7 +626,10 @@ pub async fn create_sensor(
request.validate()?; request.validate()?;
// Check if sensor with same ref already exists // Check if sensor with same ref already exists
if let Some(_) = SensorRepository::find_by_ref(&state.db, &request.r#ref).await? { if SensorRepository::find_by_ref(&state.db, &request.r#ref)
.await?
.is_some()
{
return Err(ApiError::Conflict(format!( return Err(ApiError::Conflict(format!(
"Sensor with ref '{}' already exists", "Sensor with ref '{}' already exists",
request.r#ref request.r#ref

View File

@@ -714,6 +714,7 @@ pub async fn receive_webhook(
} }
// Helper function to log webhook events // Helper function to log webhook events
#[allow(clippy::too_many_arguments)]
async fn log_webhook_event( async fn log_webhook_event(
state: &AppState, state: &AppState,
trigger: &attune_common::models::trigger::Trigger, trigger: &attune_common::models::trigger::Trigger,
@@ -753,6 +754,7 @@ async fn log_webhook_event(
} }
// Helper function to log failures when trigger is not found // Helper function to log failures when trigger is not found
#[allow(clippy::too_many_arguments)]
async fn log_webhook_failure( async fn log_webhook_failure(
_state: &AppState, _state: &AppState,
webhook_key: String, webhook_key: String,

View File

@@ -181,7 +181,10 @@ pub async fn create_workflow(
request.validate()?; request.validate()?;
// Check if workflow with same ref already exists // Check if workflow with same ref already exists
if let Some(_) = WorkflowDefinitionRepository::find_by_ref(&state.db, &request.r#ref).await? { if WorkflowDefinitionRepository::find_by_ref(&state.db, &request.r#ref)
.await?
.is_some()
{
return Err(ApiError::Conflict(format!( return Err(ApiError::Conflict(format!(
"Workflow with ref '{}' already exists", "Workflow with ref '{}' already exists",
request.r#ref request.r#ref
@@ -519,7 +522,7 @@ pub async fn update_workflow_file(
/// Write a workflow definition to disk as YAML /// Write a workflow definition to disk as YAML
async fn write_workflow_yaml( async fn write_workflow_yaml(
packs_base_dir: &PathBuf, packs_base_dir: &std::path::Path,
pack_ref: &str, pack_ref: &str,
request: &SaveWorkflowFileRequest, request: &SaveWorkflowFileRequest,
) -> Result<(), ApiError> { ) -> Result<(), ApiError> {
@@ -630,9 +633,7 @@ fn build_action_yaml(pack_ref: &str, request: &SaveWorkflowFileRequest) -> Strin
"# Action definition for workflow {}.{}", "# Action definition for workflow {}.{}",
pack_ref, request.name pack_ref, request.name
)); ));
lines.push(format!( lines.push("# The workflow graph (tasks, transitions, variables) is in:".to_string());
"# The workflow graph (tasks, transitions, variables) is in:"
));
lines.push(format!( lines.push(format!(
"# actions/workflows/{}.workflow.yaml", "# actions/workflows/{}.workflow.yaml",
request.name request.name
@@ -646,7 +647,7 @@ fn build_action_yaml(pack_ref: &str, request: &SaveWorkflowFileRequest) -> Strin
lines.push(format!("description: \"{}\"", desc.replace('"', "\\\""))); lines.push(format!("description: \"{}\"", desc.replace('"', "\\\"")));
} }
} }
lines.push(format!("enabled: true")); lines.push("enabled: true".to_string());
lines.push(format!( lines.push(format!(
"workflow_file: workflows/{}.workflow.yaml", "workflow_file: workflows/{}.workflow.yaml",
request.name request.name
@@ -658,7 +659,7 @@ fn build_action_yaml(pack_ref: &str, request: &SaveWorkflowFileRequest) -> Strin
if !obj.is_empty() { if !obj.is_empty() {
lines.push(String::new()); lines.push(String::new());
let params_yaml = serde_yaml_ng::to_string(params).unwrap_or_default(); let params_yaml = serde_yaml_ng::to_string(params).unwrap_or_default();
lines.push(format!("parameters:")); lines.push("parameters:".to_string());
// Indent the YAML output under `parameters:` // Indent the YAML output under `parameters:`
for line in params_yaml.lines() { for line in params_yaml.lines() {
lines.push(format!(" {}", line)); lines.push(format!(" {}", line));
@@ -673,7 +674,7 @@ fn build_action_yaml(pack_ref: &str, request: &SaveWorkflowFileRequest) -> Strin
if !obj.is_empty() { if !obj.is_empty() {
lines.push(String::new()); lines.push(String::new());
let output_yaml = serde_yaml_ng::to_string(output).unwrap_or_default(); let output_yaml = serde_yaml_ng::to_string(output).unwrap_or_default();
lines.push(format!("output:")); lines.push("output:".to_string());
for line in output_yaml.lines() { for line in output_yaml.lines() {
lines.push(format!(" {}", line)); lines.push(format!(" {}", line));
} }
@@ -685,7 +686,7 @@ fn build_action_yaml(pack_ref: &str, request: &SaveWorkflowFileRequest) -> Strin
if let Some(ref tags) = request.tags { if let Some(ref tags) = request.tags {
if !tags.is_empty() { if !tags.is_empty() {
lines.push(String::new()); lines.push(String::new());
lines.push(format!("tags:")); lines.push("tags:".to_string());
for tag in tags { for tag in tags {
lines.push(format!(" - {}", tag)); lines.push(format!(" - {}", tag));
} }
@@ -701,6 +702,7 @@ fn build_action_yaml(pack_ref: &str, request: &SaveWorkflowFileRequest) -> Strin
/// This ensures the workflow appears in action lists and the action palette in the /// This ensures the workflow appears in action lists and the action palette in the
/// workflow builder. The action is linked to the workflow definition via the /// workflow builder. The action is linked to the workflow definition via the
/// `workflow_def` FK. /// `workflow_def` FK.
#[allow(clippy::too_many_arguments)]
async fn create_companion_action( async fn create_companion_action(
db: &sqlx::PgPool, db: &sqlx::PgPool,
workflow_ref: &str, workflow_ref: &str,
@@ -835,6 +837,7 @@ async fn update_companion_action(
/// ///
/// If the action already exists, update it. If it doesn't exist (e.g., for workflows /// If the action already exists, update it. If it doesn't exist (e.g., for workflows
/// created before the companion-action fix), create it. /// created before the companion-action fix), create it.
#[allow(clippy::too_many_arguments)]
async fn ensure_companion_action( async fn ensure_companion_action(
db: &sqlx::PgPool, db: &sqlx::PgPool,
workflow_def_id: i64, workflow_def_id: i64,

View File

@@ -362,11 +362,11 @@ impl Drop for TestContext {
let test_packs_dir = self.test_packs_dir.clone(); let test_packs_dir = self.test_packs_dir.clone();
// Spawn cleanup task in background // Spawn cleanup task in background
let _ = tokio::spawn(async move { drop(tokio::spawn(async move {
if let Err(e) = cleanup_test_schema(&schema).await { if let Err(e) = cleanup_test_schema(&schema).await {
eprintln!("Failed to cleanup test schema {}: {}", schema, e); eprintln!("Failed to cleanup test schema {}: {}", schema, e);
} }
}); }));
// Cleanup the test packs directory synchronously // Cleanup the test packs directory synchronously
let _ = std::fs::remove_dir_all(&test_packs_dir); let _ = std::fs::remove_dir_all(&test_packs_dir);

View File

@@ -64,7 +64,7 @@ async fn test_sync_pack_workflows_endpoint() {
// Use unique pack name to avoid conflicts in parallel tests // Use unique pack name to avoid conflicts in parallel tests
let pack_name = format!( let pack_name = format!(
"test_pack_{}", "test_pack_{}",
uuid::Uuid::new_v4().to_string().replace("-", "")[..8].to_string() &uuid::Uuid::new_v4().to_string().replace("-", "")[..8]
); );
// Create temporary directory for pack workflows // Create temporary directory for pack workflows
@@ -100,7 +100,7 @@ async fn test_validate_pack_workflows_endpoint() {
// Use unique pack name to avoid conflicts in parallel tests // Use unique pack name to avoid conflicts in parallel tests
let pack_name = format!( let pack_name = format!(
"test_pack_{}", "test_pack_{}",
uuid::Uuid::new_v4().to_string().replace("-", "")[..8].to_string() &uuid::Uuid::new_v4().to_string().replace("-", "")[..8]
); );
// Create pack in database // Create pack in database
@@ -158,7 +158,7 @@ async fn test_sync_workflows_requires_authentication() {
// Use unique pack name to avoid conflicts in parallel tests // Use unique pack name to avoid conflicts in parallel tests
let pack_name = format!( let pack_name = format!(
"test_pack_{}", "test_pack_{}",
uuid::Uuid::new_v4().to_string().replace("-", "")[..8].to_string() &uuid::Uuid::new_v4().to_string().replace("-", "")[..8]
); );
// Create pack in database // Create pack in database
@@ -185,7 +185,7 @@ async fn test_validate_workflows_requires_authentication() {
// Use unique pack name to avoid conflicts in parallel tests // Use unique pack name to avoid conflicts in parallel tests
let pack_name = format!( let pack_name = format!(
"test_pack_{}", "test_pack_{}",
uuid::Uuid::new_v4().to_string().replace("-", "")[..8].to_string() &uuid::Uuid::new_v4().to_string().replace("-", "")[..8]
); );
// Create pack in database // Create pack in database

View File

@@ -14,7 +14,7 @@ use helpers::*;
fn unique_pack_name() -> String { fn unique_pack_name() -> String {
format!( format!(
"test_pack_{}", "test_pack_{}",
uuid::Uuid::new_v4().to_string().replace("-", "")[..8].to_string() &uuid::Uuid::new_v4().to_string().replace("-", "")[..8]
) )
} }

View File

@@ -180,8 +180,8 @@ impl ApiClient {
let req = self.attach_body(self.build_request(method.clone(), path), body); let req = self.attach_body(self.build_request(method.clone(), path), body);
let response = req.send().await.context("Failed to send request to API")?; let response = req.send().await.context("Failed to send request to API")?;
if response.status() == StatusCode::UNAUTHORIZED && self.refresh_token.is_some() { if response.status() == StatusCode::UNAUTHORIZED && self.refresh_token.is_some()
if self.refresh_auth_token().await? { && self.refresh_auth_token().await? {
// Retry with new token // Retry with new token
let req = self.attach_body(self.build_request(method, path), body); let req = self.attach_body(self.build_request(method, path), body);
let response = req let response = req
@@ -190,7 +190,6 @@ impl ApiClient {
.context("Failed to send request to API (retry)")?; .context("Failed to send request to API (retry)")?;
return self.handle_response(response).await; return self.handle_response(response).await;
} }
}
self.handle_response(response).await self.handle_response(response).await
} }
@@ -205,8 +204,8 @@ impl ApiClient {
let req = self.attach_body(self.build_request(method.clone(), path), body); let req = self.attach_body(self.build_request(method.clone(), path), body);
let response = req.send().await.context("Failed to send request to API")?; let response = req.send().await.context("Failed to send request to API")?;
if response.status() == StatusCode::UNAUTHORIZED && self.refresh_token.is_some() { if response.status() == StatusCode::UNAUTHORIZED && self.refresh_token.is_some()
if self.refresh_auth_token().await? { && self.refresh_auth_token().await? {
let req = self.attach_body(self.build_request(method, path), body); let req = self.attach_body(self.build_request(method, path), body);
let response = req let response = req
.send() .send()
@@ -214,7 +213,6 @@ impl ApiClient {
.context("Failed to send request to API (retry)")?; .context("Failed to send request to API (retry)")?;
return self.handle_empty_response(response).await; return self.handle_empty_response(response).await;
} }
}
self.handle_empty_response(response).await self.handle_empty_response(response).await
} }
@@ -393,8 +391,8 @@ impl ApiClient {
.await .await
.context("Failed to send multipart request to API")?; .context("Failed to send multipart request to API")?;
if response.status() == StatusCode::UNAUTHORIZED && self.refresh_token.is_some() { if response.status() == StatusCode::UNAUTHORIZED && self.refresh_token.is_some()
if self.refresh_auth_token().await? { && self.refresh_auth_token().await? {
// Retry with new token // Retry with new token
let req = build_multipart_request(self, &file_bytes)?; let req = build_multipart_request(self, &file_bytes)?;
let response = req let response = req
@@ -403,7 +401,6 @@ impl ApiClient {
.context("Failed to send multipart request to API (retry)")?; .context("Failed to send multipart request to API (retry)")?;
return self.handle_response(response).await; return self.handle_response(response).await;
} }
}
self.handle_response(response).await self.handle_response(response).await
} }

View File

@@ -314,6 +314,7 @@ async fn handle_show(
Ok(()) Ok(())
} }
#[allow(clippy::too_many_arguments)]
async fn handle_update( async fn handle_update(
action_ref: String, action_ref: String,
label: Option<String>, label: Option<String>,
@@ -415,6 +416,7 @@ async fn handle_delete(
Ok(()) Ok(())
} }
#[allow(clippy::too_many_arguments)]
async fn handle_execute( async fn handle_execute(
action_ref: String, action_ref: String,
params: Vec<String>, params: Vec<String>,
@@ -454,11 +456,8 @@ async fn handle_execute(
parameters, parameters,
}; };
match output_format { if output_format == OutputFormat::Table {
OutputFormat::Table => { output::print_info(&format!("Executing action: {}", action_ref));
output::print_info(&format!("Executing action: {}", action_ref));
}
_ => {}
} }
let path = "/executions/execute".to_string(); let path = "/executions/execute".to_string();
@@ -481,14 +480,11 @@ async fn handle_execute(
return Ok(()); return Ok(());
} }
match output_format { if output_format == OutputFormat::Table {
OutputFormat::Table => { output::print_info(&format!(
output::print_info(&format!( "Waiting for execution {} to complete...",
"Waiting for execution {} to complete...", execution.id
execution.id ));
));
}
_ => {}
} }
let verbose = matches!(output_format, OutputFormat::Table); let verbose = matches!(output_format, OutputFormat::Table);

View File

@@ -163,6 +163,7 @@ pub async fn handle_execution_command(
} }
} }
#[allow(clippy::too_many_arguments)]
async fn handle_list( async fn handle_list(
profile: &Option<String>, profile: &Option<String>,
pack: Option<String>, pack: Option<String>,

View File

@@ -468,7 +468,7 @@ pub async fn handle_pack_command(
/// ///
/// Splits on `_`, `-`, or `.` and title-cases each word. /// Splits on `_`, `-`, or `.` and title-cases each word.
fn label_from_ref(r: &str) -> String { fn label_from_ref(r: &str) -> String {
r.split(|c| c == '_' || c == '-' || c == '.') r.split(['_', '-', '.'])
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.map(|word| { .map(|word| {
let mut chars = word.chars(); let mut chars = word.chars();
@@ -484,6 +484,7 @@ fn label_from_ref(r: &str) -> String {
.join(" ") .join(" ")
} }
#[allow(clippy::too_many_arguments)]
async fn handle_create( async fn handle_create(
profile: &Option<String>, profile: &Option<String>,
ref_flag: Option<String>, ref_flag: Option<String>,
@@ -725,6 +726,7 @@ async fn handle_show(
Ok(()) Ok(())
} }
#[allow(clippy::too_many_arguments)]
async fn handle_install( async fn handle_install(
profile: &Option<String>, profile: &Option<String>,
source: String, source: String,
@@ -742,18 +744,15 @@ async fn handle_install(
// Detect source type // Detect source type
let source_type = detect_source_type(&source, ref_spec.as_deref(), no_registry); let source_type = detect_source_type(&source, ref_spec.as_deref(), no_registry);
match output_format { if output_format == OutputFormat::Table {
OutputFormat::Table => { output::print_info(&format!(
output::print_info(&format!( "Installing pack from: {} ({})",
"Installing pack from: {} ({})", source, source_type
source, source_type ));
)); output::print_info("Starting installation...");
output::print_info("Starting installation..."); if skip_deps {
if skip_deps { output::print_info("⚠ Dependency validation will be skipped");
output::print_info("⚠ Dependency validation will be skipped");
}
} }
_ => {}
} }
let request = InstallPackRequest { let request = InstallPackRequest {
@@ -880,12 +879,9 @@ async fn handle_upload(
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or("unknown"); .unwrap_or("unknown");
match output_format { if output_format == OutputFormat::Table {
OutputFormat::Table => { output::print_info(&format!("Uploading pack '{}' from: {}", pack_ref, path));
output::print_info(&format!("Uploading pack '{}' from: {}", pack_ref, path)); output::print_info("Creating archive...");
output::print_info("Creating archive...");
}
_ => {}
} }
// Build an in-memory tar.gz of the pack directory // Build an in-memory tar.gz of the pack directory
@@ -908,14 +904,11 @@ async fn handle_upload(
let archive_size_kb = tar_gz_bytes.len() / 1024; let archive_size_kb = tar_gz_bytes.len() / 1024;
match output_format { if output_format == OutputFormat::Table {
OutputFormat::Table => { output::print_info(&format!(
output::print_info(&format!( "Archive ready ({} KB), uploading...",
"Archive ready ({} KB), uploading...", archive_size_kb
archive_size_kb ));
));
}
_ => {}
} }
let config = CliConfig::load_with_profile(profile.as_deref())?; let config = CliConfig::load_with_profile(profile.as_deref())?;
@@ -1014,25 +1007,17 @@ async fn handle_register(
&& !path.starts_with("/app/") && !path.starts_with("/app/")
&& !path.starts_with("/packs"); && !path.starts_with("/packs");
if looks_local { if looks_local {
match output_format { if output_format == OutputFormat::Table {
OutputFormat::Table => { output::print_info(&format!("Registering pack from: {}", path));
output::print_info(&format!("Registering pack from: {}", path)); eprintln!(
eprintln!( "⚠ Warning: '{}' looks like a local path. If the API is running in \
"⚠ Warning: '{}' looks like a local path. If the API is running in \ Docker it may not be able to access this path.\n \
Docker it may not be able to access this path.\n \ Use `attune pack upload {}` instead to upload the pack directly.",
Use `attune pack upload {}` instead to upload the pack directly.", path, path
path, path );
);
}
_ => {}
}
} else {
match output_format {
OutputFormat::Table => {
output::print_info(&format!("Registering pack from: {}", path));
}
_ => {}
} }
} else if output_format == OutputFormat::Table {
output::print_info(&format!("Registering pack from: {}", path));
} }
let request = RegisterPackRequest { let request = RegisterPackRequest {
@@ -1173,13 +1158,10 @@ async fn handle_test(
let executor = TestExecutor::new(pack_base_dir); let executor = TestExecutor::new(pack_base_dir);
// Print test start message // Print test start message
match output_format { if output_format == OutputFormat::Table {
OutputFormat::Table => { println!();
println!(); output::print_section(&format!("🧪 Testing Pack: {} v{}", pack_ref, pack_version));
output::print_section(&format!("🧪 Testing Pack: {} v{}", pack_ref, pack_version)); println!();
println!();
}
_ => {}
} }
// Execute tests // Execute tests
@@ -1688,7 +1670,7 @@ async fn handle_index_entry(
if let Some(ref git) = git_url { if let Some(ref git) = git_url {
let default_ref = format!("v{}", version); let default_ref = format!("v{}", version);
let ref_value = git_ref.as_ref().map(|s| s.as_str()).unwrap_or(&default_ref); let ref_value = git_ref.as_deref().unwrap_or(&default_ref);
let git_source = serde_json::json!({ let git_source = serde_json::json!({
"type": "git", "type": "git",
"url": git, "url": git,
@@ -1790,6 +1772,7 @@ async fn handle_index_entry(
Ok(()) Ok(())
} }
#[allow(clippy::too_many_arguments)]
async fn handle_update( async fn handle_update(
profile: &Option<String>, profile: &Option<String>,
pack_ref: String, pack_ref: String,

View File

@@ -76,10 +76,8 @@ pub async fn handle_index_update(
if output_format == OutputFormat::Table { if output_format == OutputFormat::Table {
output::print_info(&format!("Updating existing entry for '{}'", pack_ref)); output::print_info(&format!("Updating existing entry for '{}'", pack_ref));
} }
} else { } else if output_format == OutputFormat::Table {
if output_format == OutputFormat::Table { output::print_info(&format!("Adding new entry for '{}'", pack_ref));
output::print_info(&format!("Adding new entry for '{}'", pack_ref));
}
} }
// Calculate checksum // Calculate checksum
@@ -93,7 +91,7 @@ pub async fn handle_index_update(
if let Some(ref git) = git_url { if let Some(ref git) = git_url {
let default_ref = format!("v{}", version); let default_ref = format!("v{}", version);
let ref_value = git_ref.as_ref().map(|s| s.as_str()).unwrap_or(&default_ref); let ref_value = git_ref.as_deref().unwrap_or(&default_ref);
install_sources.push(serde_json::json!({ install_sources.push(serde_json::json!({
"type": "git", "type": "git",
"url": git, "url": git,
@@ -318,13 +316,11 @@ pub async fn handle_index_merge(
)); ));
} }
packs_map.insert(pack_ref.to_string(), pack.clone()); packs_map.insert(pack_ref.to_string(), pack.clone());
} else { } else if output_format == OutputFormat::Table {
if output_format == OutputFormat::Table { output::print_info(&format!(
output::print_info(&format!( " Keeping '{}' at {} (newer than {})",
" Keeping '{}' at {} (newer than {})", pack_ref, existing_version, new_version
pack_ref, existing_version, new_version ));
));
}
} }
duplicates_resolved += 1; duplicates_resolved += 1;
} else { } else {

View File

@@ -354,6 +354,7 @@ async fn handle_show(
Ok(()) Ok(())
} }
#[allow(clippy::too_many_arguments)]
async fn handle_update( async fn handle_update(
profile: &Option<String>, profile: &Option<String>,
rule_ref: String, rule_ref: String,
@@ -477,6 +478,7 @@ async fn handle_toggle(
Ok(()) Ok(())
} }
#[allow(clippy::too_many_arguments)]
async fn handle_create( async fn handle_create(
profile: &Option<String>, profile: &Option<String>,
name: String, name: String,

View File

@@ -472,7 +472,7 @@ fn resolve_ws_url(opts: &WaitOptions<'_>) -> Option<String> {
let api_url = opts.api_client.base_url(); let api_url = opts.api_client.base_url();
// Transform http(s)://host:PORT/... → ws(s)://host:8081 // Transform http(s)://host:PORT/... → ws(s)://host:8081
let ws_url = derive_notifier_url(&api_url)?; let ws_url = derive_notifier_url(api_url)?;
Some(ws_url) Some(ws_url)
} }

View File

@@ -192,7 +192,7 @@ fn test_pack_index_entry_generates_valid_json() {
// Verify metadata // Verify metadata
assert_eq!(json["author"], "Test Author"); assert_eq!(json["author"], "Test Author");
assert_eq!(json["license"], "Apache-2.0"); assert_eq!(json["license"], "Apache-2.0");
assert!(json["keywords"].as_array().unwrap().len() > 0); assert!(!json["keywords"].as_array().unwrap().is_empty());
} }
#[test] #[test]
@@ -212,7 +212,7 @@ fn test_pack_index_entry_with_archive_url() {
let stdout = String::from_utf8(output.get_output().stdout.clone()).unwrap(); let stdout = String::from_utf8(output.get_output().stdout.clone()).unwrap();
let json: Value = serde_json::from_str(&stdout).unwrap(); let json: Value = serde_json::from_str(&stdout).unwrap();
assert!(json["install_sources"].as_array().unwrap().len() > 0); assert!(!json["install_sources"].as_array().unwrap().is_empty());
let archive_source = &json["install_sources"][0]; let archive_source = &json["install_sources"][0];
assert_eq!(archive_source["type"], "archive"); assert_eq!(archive_source["type"], "archive");

View File

@@ -45,21 +45,16 @@ pub struct Claims {
pub metadata: Option<serde_json::Value>, pub metadata: Option<serde_json::Value>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum TokenType { pub enum TokenType {
#[default]
Access, Access,
Refresh, Refresh,
Sensor, Sensor,
Execution, Execution,
} }
impl Default for TokenType {
fn default() -> Self {
Self::Access
}
}
/// Configuration for JWT tokens /// Configuration for JWT tokens
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct JwtConfig { pub struct JwtConfig {
@@ -247,11 +242,7 @@ pub fn validate_token(token: &str, config: &JwtConfig) -> Result<Claims, JwtErro
/// Extract token from Authorization header /// Extract token from Authorization header
pub fn extract_token_from_header(auth_header: &str) -> Option<&str> { pub fn extract_token_from_header(auth_header: &str) -> Option<&str> {
if auth_header.starts_with("Bearer ") { auth_header.strip_prefix("Bearer ")
Some(&auth_header[7..])
} else {
None
}
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -45,21 +45,16 @@ pub mod enums {
use utoipa::ToSchema; use utoipa::ToSchema;
/// How parameters should be delivered to an action /// How parameters should be delivered to an action
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum ParameterDelivery { pub enum ParameterDelivery {
/// Pass parameters via stdin (secure, recommended for most cases) /// Pass parameters via stdin (secure, recommended for most cases)
#[default]
Stdin, Stdin,
/// Pass parameters via temporary file (secure, best for large payloads) /// Pass parameters via temporary file (secure, best for large payloads)
File, File,
} }
impl Default for ParameterDelivery {
fn default() -> Self {
Self::Stdin
}
}
impl fmt::Display for ParameterDelivery { impl fmt::Display for ParameterDelivery {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
@@ -99,31 +94,23 @@ pub mod enums {
&self, &self,
buf: &mut sqlx::postgres::PgArgumentBuffer, buf: &mut sqlx::postgres::PgArgumentBuffer,
) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> { ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
Ok(<String as sqlx::Encode<sqlx::Postgres>>::encode( <String as sqlx::Encode<sqlx::Postgres>>::encode(self.to_string(), buf)
self.to_string(),
buf,
)?)
} }
} }
/// Format for parameter serialization /// Format for parameter serialization
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum ParameterFormat { pub enum ParameterFormat {
/// KEY='VALUE' format (one per line) /// KEY='VALUE' format (one per line)
Dotenv, Dotenv,
/// JSON object /// JSON object
#[default]
Json, Json,
/// YAML format /// YAML format
Yaml, Yaml,
} }
impl Default for ParameterFormat {
fn default() -> Self {
Self::Json
}
}
impl fmt::Display for ParameterFormat { impl fmt::Display for ParameterFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
@@ -165,18 +152,16 @@ pub mod enums {
&self, &self,
buf: &mut sqlx::postgres::PgArgumentBuffer, buf: &mut sqlx::postgres::PgArgumentBuffer,
) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> { ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
Ok(<String as sqlx::Encode<sqlx::Postgres>>::encode( <String as sqlx::Encode<sqlx::Postgres>>::encode(self.to_string(), buf)
self.to_string(),
buf,
)?)
} }
} }
/// Format for action output parsing /// Format for action output parsing
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum OutputFormat { pub enum OutputFormat {
/// Plain text (no parsing) /// Plain text (no parsing)
#[default]
Text, Text,
/// Parse as JSON /// Parse as JSON
Json, Json,
@@ -186,12 +171,6 @@ pub mod enums {
Jsonl, Jsonl,
} }
impl Default for OutputFormat {
fn default() -> Self {
Self::Text
}
}
impl fmt::Display for OutputFormat { impl fmt::Display for OutputFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
@@ -235,10 +214,7 @@ pub mod enums {
&self, &self,
buf: &mut sqlx::postgres::PgArgumentBuffer, buf: &mut sqlx::postgres::PgArgumentBuffer,
) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> { ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
Ok(<String as sqlx::Encode<sqlx::Postgres>>::encode( <String as sqlx::Encode<sqlx::Postgres>>::encode(self.to_string(), buf)
self.to_string(),
buf,
)?)
} }
} }
@@ -371,20 +347,17 @@ pub mod enums {
/// - `Public`: viewable by all authenticated users on the platform. /// - `Public`: viewable by all authenticated users on the platform.
/// - `Private`: restricted based on the artifact's `scope` and `owner` fields. /// - `Private`: restricted based on the artifact's `scope` and `owner` fields.
/// Full RBAC enforcement is deferred; for now the field enables filtering. /// Full RBAC enforcement is deferred; for now the field enables filtering.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type, ToSchema)] #[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Type, ToSchema,
)]
#[sqlx(type_name = "artifact_visibility_enum", rename_all = "lowercase")] #[sqlx(type_name = "artifact_visibility_enum", rename_all = "lowercase")]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum ArtifactVisibility { pub enum ArtifactVisibility {
Public, Public,
#[default]
Private, Private,
} }
impl Default for ArtifactVisibility {
fn default() -> Self {
Self::Private
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type, ToSchema)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type, ToSchema)]
#[sqlx(type_name = "workflow_task_status_enum", rename_all = "lowercase")] #[sqlx(type_name = "workflow_task_status_enum", rename_all = "lowercase")]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]

View File

@@ -141,7 +141,7 @@ mod tests {
fn test_message_queue_creation() { fn test_message_queue_creation() {
// This test just verifies the struct can be instantiated // This test just verifies the struct can be instantiated
// Actual connection tests require a running RabbitMQ instance // Actual connection tests require a running RabbitMQ instance
assert!(true); // (nothing to assert without a live broker)
} }
#[tokio::test] #[tokio::test]

View File

@@ -69,20 +69,15 @@ use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
/// Message delivery mode /// Message delivery mode
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeliveryMode { pub enum DeliveryMode {
/// Non-persistent messages (faster, but may be lost on broker restart) /// Non-persistent messages (faster, but may be lost on broker restart)
NonPersistent = 1, NonPersistent = 1,
/// Persistent messages (slower, but survive broker restart) /// Persistent messages (slower, but survive broker restart)
#[default]
Persistent = 2, Persistent = 2,
} }
impl Default for DeliveryMode {
fn default() -> Self {
Self::Persistent
}
}
/// Message priority (0-9, higher is more urgent) /// Message priority (0-9, higher is more urgent)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Priority(u8); pub struct Priority(u8);
@@ -125,25 +120,21 @@ impl fmt::Display for Priority {
} }
/// Message acknowledgment mode /// Message acknowledgment mode
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum AckMode { pub enum AckMode {
/// Automatically acknowledge messages after delivery /// Automatically acknowledge messages after delivery
Auto, Auto,
/// Manually acknowledge messages after processing /// Manually acknowledge messages after processing
#[default]
Manual, Manual,
} }
impl Default for AckMode {
fn default() -> Self {
Self::Manual
}
}
/// Exchange type /// Exchange type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum ExchangeType { pub enum ExchangeType {
/// Direct exchange - routes messages with exact routing key match /// Direct exchange - routes messages with exact routing key match
#[default]
Direct, Direct,
/// Topic exchange - routes messages using pattern matching /// Topic exchange - routes messages using pattern matching
Topic, Topic,
@@ -165,12 +156,6 @@ impl ExchangeType {
} }
} }
impl Default for ExchangeType {
fn default() -> Self {
Self::Direct
}
}
impl fmt::Display for ExchangeType { impl fmt::Display for ExchangeType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str()) write!(f, "{}", self.as_str())

View File

@@ -41,7 +41,7 @@ impl PackEnvironmentStatus {
} }
} }
pub fn from_str(s: &str) -> Option<Self> { pub fn parse_status(s: &str) -> Option<Self> {
match s { match s {
"pending" => Some(Self::Pending), "pending" => Some(Self::Pending),
"installing" => Some(Self::Installing), "installing" => Some(Self::Installing),
@@ -194,7 +194,7 @@ impl PackEnvironmentManager {
if let Some(row) = row { if let Some(row) = row {
let status_str: String = row.try_get("status")?; let status_str: String = row.try_get("status")?;
let status = PackEnvironmentStatus::from_str(&status_str) let status = PackEnvironmentStatus::parse_status(&status_str)
.unwrap_or(PackEnvironmentStatus::Failed); .unwrap_or(PackEnvironmentStatus::Failed);
Ok(Some(PackEnvironment { Ok(Some(PackEnvironment {
@@ -343,7 +343,7 @@ impl PackEnvironmentManager {
let mut environments = Vec::new(); let mut environments = Vec::new();
for row in rows { for row in rows {
let status_str: String = row.try_get("status")?; let status_str: String = row.try_get("status")?;
let status = PackEnvironmentStatus::from_str(&status_str) let status = PackEnvironmentStatus::parse_status(&status_str)
.unwrap_or(PackEnvironmentStatus::Failed); .unwrap_or(PackEnvironmentStatus::Failed);
environments.push(PackEnvironment { environments.push(PackEnvironment {
@@ -452,8 +452,8 @@ impl PackEnvironmentManager {
.await?; .await?;
let status_str: String = row.try_get("status")?; let status_str: String = row.try_get("status")?;
let status = let status = PackEnvironmentStatus::parse_status(&status_str)
PackEnvironmentStatus::from_str(&status_str).unwrap_or(PackEnvironmentStatus::Pending); .unwrap_or(PackEnvironmentStatus::Pending);
Ok(PackEnvironment { Ok(PackEnvironment {
id: row.try_get("id")?, id: row.try_get("id")?,
@@ -496,8 +496,8 @@ impl PackEnvironmentManager {
.await?; .await?;
let status_str: String = row.try_get("status")?; let status_str: String = row.try_get("status")?;
let status = let status = PackEnvironmentStatus::parse_status(&status_str)
PackEnvironmentStatus::from_str(&status_str).unwrap_or(PackEnvironmentStatus::Ready); .unwrap_or(PackEnvironmentStatus::Ready);
Ok(PackEnvironment { Ok(PackEnvironment {
id: row.try_get("id")?, id: row.try_get("id")?,
@@ -580,7 +580,7 @@ impl PackEnvironmentManager {
Ok(output) => { Ok(output) => {
install_log.push_str(&format!("\n=== {} ===\n", action.name)); install_log.push_str(&format!("\n=== {} ===\n", action.name));
install_log.push_str(&output); install_log.push_str(&output);
install_log.push_str("\n"); install_log.push('\n');
} }
Err(e) => { Err(e) => {
let error_msg = format!("Installer '{}' failed: {}", action.name, e); let error_msg = format!("Installer '{}' failed: {}", action.name, e);
@@ -701,20 +701,18 @@ impl PackEnvironmentManager {
.map(|obj| { .map(|obj| {
obj.iter() obj.iter()
.filter_map(|(k, v)| { .filter_map(|(k, v)| {
v.as_str() v.as_str().and_then(|s| {
.map(|s| { let resolved = self
let resolved = self .resolve_template(
.resolve_template( s,
s, pack_ref,
pack_ref, runtime_ref,
runtime_ref, env_path,
env_path, &pack_path_str,
&pack_path_str, )
) .ok()?;
.ok()?; Some((k.clone(), resolved))
Some((k.clone(), resolved)) })
})
.flatten()
}) })
.collect::<HashMap<String, String>>() .collect::<HashMap<String, String>>()
}) })
@@ -877,9 +875,9 @@ mod tests {
fn test_environment_status_conversion() { fn test_environment_status_conversion() {
assert_eq!(PackEnvironmentStatus::Ready.as_str(), "ready"); assert_eq!(PackEnvironmentStatus::Ready.as_str(), "ready");
assert_eq!( assert_eq!(
PackEnvironmentStatus::from_str("ready"), PackEnvironmentStatus::parse_status("ready"),
Some(PackEnvironmentStatus::Ready) Some(PackEnvironmentStatus::Ready)
); );
assert_eq!(PackEnvironmentStatus::from_str("invalid"), None); assert_eq!(PackEnvironmentStatus::parse_status("invalid"), None);
} }
} }

View File

@@ -151,17 +151,15 @@ impl DependencyValidator {
}; };
let error = if !satisfied { let error = if !satisfied {
if detected_version.is_none() { if let Some(ref detected) = detected_version {
Some(format!("Runtime '{}' not found on system", runtime)) version_constraint.as_ref().map(|constraint| {
} else if let Some(ref constraint) = version_constraint { format!(
Some(format!( "Runtime '{}' version {} does not satisfy constraint '{}'",
"Runtime '{}' version {} does not satisfy constraint '{}'", runtime, detected, constraint
runtime, )
detected_version.as_ref().unwrap(), })
constraint
))
} else { } else {
None Some(format!("Runtime '{}' not found on system", runtime))
} }
} else { } else {
None None
@@ -192,15 +190,13 @@ impl DependencyValidator {
}; };
let error = if !satisfied { let error = if !satisfied {
if installed_version.is_none() { if let Some(ref installed) = installed_version {
Some(format!("Required pack '{}' is not installed", pack_ref))
} else {
Some(format!( Some(format!(
"Pack '{}' version {} does not satisfy constraint '{}'", "Pack '{}' version {} does not satisfy constraint '{}'",
pack_ref, pack_ref, installed, version_constraint
installed_version.as_ref().unwrap(),
version_constraint
)) ))
} else {
Some(format!("Required pack '{}' is not installed", pack_ref))
} }
} else { } else {
None None
@@ -335,30 +331,30 @@ fn match_version_constraint(version: &str, constraint: &str) -> Result<bool> {
} }
// Parse constraint // Parse constraint
if constraint.starts_with(">=") { if let Some(stripped) = constraint.strip_prefix(">=") {
let required = constraint[2..].trim(); let required = stripped.trim();
Ok(compare_versions(version, required)? >= 0) Ok(compare_versions(version, required)? >= 0)
} else if constraint.starts_with("<=") { } else if let Some(stripped) = constraint.strip_prefix("<=") {
let required = constraint[2..].trim(); let required = stripped.trim();
Ok(compare_versions(version, required)? <= 0) Ok(compare_versions(version, required)? <= 0)
} else if constraint.starts_with('>') { } else if let Some(stripped) = constraint.strip_prefix('>') {
let required = constraint[1..].trim(); let required = stripped.trim();
Ok(compare_versions(version, required)? > 0) Ok(compare_versions(version, required)? > 0)
} else if constraint.starts_with('<') { } else if let Some(stripped) = constraint.strip_prefix('<') {
let required = constraint[1..].trim(); let required = stripped.trim();
Ok(compare_versions(version, required)? < 0) Ok(compare_versions(version, required)? < 0)
} else if constraint.starts_with('=') { } else if let Some(stripped) = constraint.strip_prefix('=') {
let required = constraint[1..].trim(); let required = stripped.trim();
Ok(compare_versions(version, required)? == 0) Ok(compare_versions(version, required)? == 0)
} else if constraint.starts_with('^') { } else if let Some(stripped) = constraint.strip_prefix('^') {
// Caret: Compatible with version (major.minor.patch) // Caret: Compatible with version (major.minor.patch)
// ^1.2.3 := >=1.2.3 <2.0.0 // ^1.2.3 := >=1.2.3 <2.0.0
let required = constraint[1..].trim(); let required = stripped.trim();
match_caret_constraint(version, required) match_caret_constraint(version, required)
} else if constraint.starts_with('~') { } else if let Some(stripped) = constraint.strip_prefix('~') {
// Tilde: Approximately equivalent to version // Tilde: Approximately equivalent to version
// ~1.2.3 := >=1.2.3 <1.3.0 // ~1.2.3 := >=1.2.3 <1.3.0
let required = constraint[1..].trim(); let required = stripped.trim();
match_tilde_constraint(version, required) match_tilde_constraint(version, required)
} else { } else {
// Exact match // Exact match

View File

@@ -171,7 +171,7 @@ impl PackInstaller {
clone_cmd.arg("--depth").arg("1"); clone_cmd.arg("--depth").arg("1");
} }
clone_cmd.arg(&url).arg(&install_dir); clone_cmd.arg(url).arg(&install_dir);
let output = clone_cmd let output = clone_cmd
.output() .output()
@@ -421,7 +421,11 @@ impl PackInstaller {
} }
// Determine filename from URL // Determine filename from URL
let filename = url.split('/').last().unwrap_or("archive.zip").to_string(); let filename = url
.split('/')
.next_back()
.unwrap_or("archive.zip")
.to_string();
let archive_path = self.temp_dir.join(&filename); let archive_path = self.temp_dir.join(&filename);

View File

@@ -288,17 +288,15 @@ impl<'a> PackComponentLoader<'a> {
} }
Err(e) => { Err(e) => {
// Check for unique constraint violation (race condition) // Check for unique constraint violation (race condition)
if let Error::Database(ref db_err) = e { if let Error::Database(sqlx::Error::Database(ref inner)) = e {
if let sqlx::Error::Database(ref inner) = db_err { if inner.is_unique_violation() {
if inner.is_unique_violation() { info!(
info!( "Runtime '{}' already exists (concurrent creation), treating as update",
"Runtime '{}' already exists (concurrent creation), treating as update", runtime_ref
runtime_ref );
); loaded_refs.push(runtime_ref);
loaded_refs.push(runtime_ref); result.runtimes_updated += 1;
result.runtimes_updated += 1; continue;
continue;
}
} }
} }
let msg = format!("Failed to create runtime '{}': {}", runtime_ref, e); let msg = format!("Failed to create runtime '{}': {}", runtime_ref, e);
@@ -438,16 +436,14 @@ impl<'a> PackComponentLoader<'a> {
} }
Err(e) => { Err(e) => {
// Check for unique constraint violation (race condition) // Check for unique constraint violation (race condition)
if let Error::Database(ref db_err) = e { if let Error::Database(sqlx::Error::Database(ref inner)) = e {
if let sqlx::Error::Database(ref inner) = db_err { if inner.is_unique_violation() {
if inner.is_unique_violation() { info!(
info!( "Version '{}' for runtime '{}' already exists (concurrent), skipping",
"Version '{}' for runtime '{}' already exists (concurrent), skipping", version_str, runtime_ref
version_str, runtime_ref );
); loaded_versions.push(version_str);
loaded_versions.push(version_str); continue;
continue;
}
} }
} }
let msg = format!( let msg = format!(

View File

@@ -272,11 +272,6 @@ impl Checksum {
Ok(Self { algorithm, hash }) Ok(Self { algorithm, hash })
} }
/// Format as "algorithm:hash"
pub fn to_string(&self) -> String {
format!("{}:{}", self.algorithm, self.hash)
}
} }
impl std::fmt::Display for Checksum { impl std::fmt::Display for Checksum {

View File

@@ -295,7 +295,7 @@ impl Update for ActionRepository {
query.push(", updated = NOW() WHERE id = "); query.push(", updated = NOW() WHERE id = ");
query.push_bind(id); query.push_bind(id);
query.push(&format!(" RETURNING {}", ACTION_COLUMNS)); query.push(format!(" RETURNING {}", ACTION_COLUMNS));
let action = query let action = query
.build_query_as::<Action>() .build_query_as::<Action>()
@@ -554,7 +554,6 @@ impl ActionRepository {
} }
} }
/// Repository for Policy operations
// ============================================================================ // ============================================================================
// Policy Repository // Policy Repository
// ============================================================================ // ============================================================================

View File

@@ -47,7 +47,7 @@ pub struct HistoryQueryParams {
impl HistoryQueryParams { impl HistoryQueryParams {
/// Returns the effective limit, capped at 1000. /// Returns the effective limit, capped at 1000.
pub fn effective_limit(&self) -> i64 { pub fn effective_limit(&self) -> i64 {
self.limit.unwrap_or(100).min(1000).max(1) self.limit.unwrap_or(100).clamp(1, 1000)
} }
/// Returns the effective offset. /// Returns the effective offset.

View File

@@ -582,7 +582,7 @@ impl EnforcementRepository {
} }
if let Some(status) = &filters.status { if let Some(status) = &filters.status {
push_condition!("status = ", status.clone()); push_condition!("status = ", *status);
} }
if let Some(rule_id) = filters.rule { if let Some(rule_id) = filters.rule {
push_condition!("rule = ", rule_id); push_condition!("rule = ", rule_id);

View File

@@ -391,7 +391,7 @@ impl ExecutionRepository {
} }
if let Some(status) = &filters.status { if let Some(status) = &filters.status {
push_condition!("e.status = ", status.clone()); push_condition!("e.status = ", *status);
} }
if let Some(action_ref) = &filters.action_ref { if let Some(action_ref) = &filters.action_ref {
push_condition!("e.action_ref = ", action_ref.clone()); push_condition!("e.action_ref = ", action_ref.clone());

View File

@@ -129,7 +129,7 @@ impl Update for IdentityRepository {
.map_err(|e| { .map_err(|e| {
// Convert RowNotFound to NotFound error // Convert RowNotFound to NotFound error
if matches!(e, sqlx::Error::RowNotFound) { if matches!(e, sqlx::Error::RowNotFound) {
return crate::Error::not_found("identity", "id", &id.to_string()); return crate::Error::not_found("identity", "id", id.to_string());
} }
e.into() e.into()
}) })

View File

@@ -211,7 +211,7 @@ impl InquiryRepository {
} }
if let Some(status) = &filters.status { if let Some(status) = &filters.status {
push_condition!("status = ", status.clone()); push_condition!("status = ", *status);
} }
if let Some(execution_id) = filters.execution { if let Some(execution_id) = filters.execution {
push_condition!("execution = ", execution_id); push_condition!("execution = ", execution_id);

View File

@@ -218,7 +218,7 @@ impl KeyRepository {
} }
if let Some(ref owner_type) = filters.owner_type { if let Some(ref owner_type) = filters.owner_type {
push_condition!("owner_type = ", owner_type.clone()); push_condition!("owner_type = ", *owner_type);
} }
if let Some(ref owner) = filters.owner { if let Some(ref owner) = filters.owner {
push_condition!("owner = ", owner.clone()); push_condition!("owner = ", owner.clone());

View File

@@ -408,6 +408,7 @@ impl PackRepository {
} }
/// Update installation metadata for a pack /// Update installation metadata for a pack
#[allow(clippy::too_many_arguments)]
pub async fn update_installation_metadata<'e, E>( pub async fn update_installation_metadata<'e, E>(
executor: E, executor: E,
id: i64, id: i64,

View File

@@ -239,7 +239,7 @@ impl Update for RuntimeVersionRepository {
query.push(" WHERE id = "); query.push(" WHERE id = ");
query.push_bind(id); query.push_bind(id);
query.push(&format!(" RETURNING {}", SELECT_COLUMNS)); query.push(format!(" RETURNING {}", SELECT_COLUMNS));
let row = query let row = query
.build_query_as::<RuntimeVersion>() .build_query_as::<RuntimeVersion>()

View File

@@ -276,7 +276,7 @@ impl Update for TriggerRepository {
.map_err(|e| { .map_err(|e| {
// Convert RowNotFound to NotFound error // Convert RowNotFound to NotFound error
if matches!(e, sqlx::Error::RowNotFound) { if matches!(e, sqlx::Error::RowNotFound) {
return crate::Error::not_found("trigger", "id", &id.to_string()); return crate::Error::not_found("trigger", "id", id.to_string());
} }
e.into() e.into()
})?; })?;

View File

@@ -423,14 +423,11 @@ mod tests {
"always_available": true "always_available": true
}); });
assert_eq!( assert!(verification
verification .get("always_available")
.get("always_available") .unwrap()
.unwrap() .as_bool()
.as_bool() .unwrap());
.unwrap(),
true
);
} }
#[tokio::test] #[tokio::test]

View File

@@ -106,7 +106,7 @@ impl TestExecutor {
); );
match self match self
.execute_test_suite(&pack_dir, runner_name, runner_config) .execute_test_suite(pack_dir, runner_name, runner_config)
.await .await
{ {
Ok(suite_result) => { Ok(suite_result) => {
@@ -369,7 +369,7 @@ impl TestExecutor {
let total = self.extract_number(&text, "Total Tests:"); let total = self.extract_number(&text, "Total Tests:");
let passed = self.extract_number(&text, "Passed:"); let passed = self.extract_number(&text, "Passed:");
let failed = self.extract_number(&text, "Failed:"); let failed = self.extract_number(&text, "Failed:");
let skipped = self.extract_number(&text, "Skipped:").or_else(|| Some(0)); let skipped = self.extract_number(&text, "Skipped:").or(Some(0));
// If we couldn't parse counts, use exit code // If we couldn't parse counts, use exit code
let (total, passed, failed, skipped) = if total.is_none() || passed.is_none() { let (total, passed, failed, skipped) = if total.is_none() || passed.is_none() {
@@ -441,7 +441,6 @@ impl TestExecutor {
.and_then(|line| { .and_then(|line| {
line.split(label) line.split(label)
.nth(1)? .nth(1)?
.trim()
.split_whitespace() .split_whitespace()
.next()? .next()?
.parse::<i32>() .parse::<i32>()

View File

@@ -278,7 +278,7 @@ fn json_eq(a: &JsonValue, b: &JsonValue) -> bool {
return false; return false;
} }
a.iter() a.iter()
.all(|(k, v)| b.get(k).map_or(false, |bv| json_eq(v, bv))) .all(|(k, v)| b.get(k).is_some_and(|bv| json_eq(v, bv)))
} }
// Different types (other than number cross-compare) are never equal // Different types (other than number cross-compare) are never equal
_ => false, _ => false,
@@ -680,10 +680,8 @@ impl NumericValue {
fn as_numeric(v: &JsonValue) -> Option<NumericValue> { fn as_numeric(v: &JsonValue) -> Option<NumericValue> {
if let Some(i) = v.as_i64() { if let Some(i) = v.as_i64() {
Some(NumericValue::Int(i)) Some(NumericValue::Int(i))
} else if let Some(f) = v.as_f64() {
Some(NumericValue::Float(f))
} else { } else {
None v.as_f64().map(NumericValue::Float)
} }
} }

View File

@@ -363,8 +363,8 @@ mod tests {
let ctx = TestContext::new(); let ctx = TestContext::new();
assert_eq!(eval_expression("string(42)", &ctx).unwrap(), json!("42")); assert_eq!(eval_expression("string(42)", &ctx).unwrap(), json!("42"));
assert_eq!( assert_eq!(
eval_expression("number(\"3.14\")", &ctx).unwrap(), eval_expression("number(\"3.15\")", &ctx).unwrap(),
json!(3.14) json!(3.15)
); );
assert_eq!(eval_expression("int(3.9)", &ctx).unwrap(), json!(3)); assert_eq!(eval_expression("int(3.9)", &ctx).unwrap(), json!(3));
assert_eq!(eval_expression("int(\"42\")", &ctx).unwrap(), json!(42)); assert_eq!(eval_expression("int(\"42\")", &ctx).unwrap(), json!(42));

View File

@@ -431,8 +431,8 @@ mod tests {
#[test] #[test]
fn test_float() { fn test_float() {
let kinds = tokenize("3.14"); let kinds = tokenize("3.15");
assert_eq!(kinds, vec![TokenKind::Float(3.14), TokenKind::Eof]); assert_eq!(kinds, vec![TokenKind::Float(3.15), TokenKind::Eof]);
} }
#[test] #[test]

View File

@@ -187,11 +187,13 @@ impl WorkflowLoader {
.map(|e| e.to_string()) .map(|e| e.to_string())
}; };
if validation_error.is_some() && !self.config.skip_validation { if let Some(ref err) = validation_error {
return Err(Error::validation(format!( if !self.config.skip_validation {
"Workflow validation failed: {}", return Err(Error::validation(format!(
validation_error.as_ref().unwrap() "Workflow validation failed: {}",
))); err
)));
}
} }
Ok(LoadedWorkflow { Ok(LoadedWorkflow {

View File

@@ -1449,7 +1449,7 @@ tasks:
publish: publish:
- validation_passed: true - validation_passed: true
- count: 42 - count: 42
- ratio: 3.14 - ratio: 3.15
- label: "hello" - label: "hello"
- template_val: "{{ result().data }}" - template_val: "{{ result().data }}"
- nothing: null - nothing: null
@@ -1486,7 +1486,7 @@ tasks:
} else if let Some(val) = map.get("count") { } else if let Some(val) = map.get("count") {
assert_eq!(val, &serde_json::json!(42), "integer"); assert_eq!(val, &serde_json::json!(42), "integer");
} else if let Some(val) = map.get("ratio") { } else if let Some(val) = map.get("ratio") {
assert_eq!(val, &serde_json::json!(3.14), "float"); assert_eq!(val, &serde_json::json!(3.15), "float");
} else if let Some(val) = map.get("label") { } else if let Some(val) = map.get("label") {
assert_eq!(val, &serde_json::json!("hello"), "string"); assert_eq!(val, &serde_json::json!("hello"), "string");
} else if let Some(val) = map.get("template_val") { } else if let Some(val) = map.get("template_val") {

View File

@@ -97,11 +97,11 @@ impl WorkflowRegistrar {
debug!("Registering workflow: {}", loaded.file.ref_name); debug!("Registering workflow: {}", loaded.file.ref_name);
// Check for validation errors // Check for validation errors
if loaded.validation_error.is_some() { if let Some(ref err) = loaded.validation_error {
if self.options.skip_invalid { if self.options.skip_invalid {
return Err(Error::validation(format!( return Err(Error::validation(format!(
"Workflow has validation errors: {}", "Workflow has validation errors: {}",
loaded.validation_error.as_ref().unwrap() err
))); )));
} }
} }
@@ -252,6 +252,7 @@ impl WorkflowRegistrar {
/// ///
/// `effective_ref` and `effective_label` are the resolved values (which may /// `effective_ref` and `effective_label` are the resolved values (which may
/// have been derived from the filename when the workflow YAML omits them). /// have been derived from the filename when the workflow YAML omits them).
#[allow(clippy::too_many_arguments)]
async fn create_companion_action( async fn create_companion_action(
&self, &self,
workflow_def_id: i64, workflow_def_id: i64,
@@ -298,6 +299,7 @@ impl WorkflowRegistrar {
/// ///
/// `effective_ref` and `effective_label` are the resolved values (which may /// `effective_ref` and `effective_label` are the resolved values (which may
/// have been derived from the filename when the workflow YAML omits them). /// have been derived from the filename when the workflow YAML omits them).
#[allow(clippy::too_many_arguments)]
async fn ensure_companion_action( async fn ensure_companion_action(
&self, &self,
workflow_def_id: i64, workflow_def_id: i64,
@@ -425,8 +427,8 @@ mod tests {
#[test] #[test]
fn test_registration_options_default() { fn test_registration_options_default() {
let options = RegistrationOptions::default(); let options = RegistrationOptions::default();
assert_eq!(options.update_existing, true); assert!(options.update_existing);
assert_eq!(options.skip_invalid, true); assert!(options.skip_invalid);
} }
#[test] #[test]
@@ -439,7 +441,7 @@ mod tests {
}; };
assert_eq!(result.ref_name, "test.workflow"); assert_eq!(result.ref_name, "test.workflow");
assert_eq!(result.created, true); assert!(result.created);
assert_eq!(result.workflow_def_id, 123); assert_eq!(result.workflow_def_id, 123);
assert_eq!(result.warnings.len(), 0); assert_eq!(result.warnings.len(), 0);
} }

View File

@@ -308,7 +308,7 @@ impl WorkflowValidator {
reachable reachable
} }
/// Detect cycles using DFS // Detect cycles using DFS
// Cycle detection removed - cycles are now valid in workflow graphs // Cycle detection removed - cycles are now valid in workflow graphs
// Workflows are directed graphs (not DAGs) and cycles are supported // Workflows are directed graphs (not DAGs) and cycles are supported
// for use cases like monitoring loops, retry patterns, etc. // for use cases like monitoring loops, retry patterns, etc.
@@ -328,7 +328,7 @@ impl WorkflowValidator {
} }
// Validate variable names in vars // Validate variable names in vars
for (key, _) in &workflow.vars { for key in workflow.vars.keys() {
if !Self::is_valid_variable_name(key) { if !Self::is_valid_variable_name(key) {
return Err(ValidationError::SemanticError(format!( return Err(ValidationError::SemanticError(format!(
"Invalid variable name: {}", "Invalid variable name: {}",

View File

@@ -781,7 +781,7 @@ async fn test_find_executions_by_enforcement() {
config: None, config: None,
env_vars: None, env_vars: None,
parent: None, parent: None,
enforcement: if i == 2 { None } else { None }, // Can't reference non-existent enforcement enforcement: None, // Can't reference non-existent enforcement
executor: None, executor: None,
status: ExecutionStatus::Requested, status: ExecutionStatus::Requested,
result: None, result: None,

View File

@@ -35,7 +35,7 @@ async fn test_create_key_system_owner() {
assert_eq!(key.owner_pack, None); assert_eq!(key.owner_pack, None);
assert_eq!(key.owner_action, None); assert_eq!(key.owner_action, None);
assert_eq!(key.owner_sensor, None); assert_eq!(key.owner_sensor, None);
assert_eq!(key.encrypted, false); assert!(!key.encrypted);
assert_eq!(key.value, "test_value"); assert_eq!(key.value, "test_value");
assert!(key.created.timestamp() > 0); assert!(key.created.timestamp() > 0);
assert!(key.updated.timestamp() > 0); assert!(key.updated.timestamp() > 0);
@@ -52,7 +52,7 @@ async fn test_create_key_system_encrypted() {
.await .await
.unwrap(); .unwrap();
assert_eq!(key.encrypted, true); assert!(key.encrypted);
assert_eq!(key.encryption_key_hash, Some("sha256:abc123".to_string())); assert_eq!(key.encryption_key_hash, Some("sha256:abc123".to_string()));
} }
@@ -427,7 +427,7 @@ async fn test_update_encrypted_status() {
.await .await
.unwrap(); .unwrap();
assert_eq!(key.encrypted, false); assert!(!key.encrypted);
let input = UpdateKeyInput { let input = UpdateKeyInput {
encrypted: Some(true), encrypted: Some(true),
@@ -438,7 +438,7 @@ async fn test_update_encrypted_status() {
let updated = KeyRepository::update(&pool, key.id, input).await.unwrap(); let updated = KeyRepository::update(&pool, key.id, input).await.unwrap();
assert_eq!(updated.encrypted, true); assert!(updated.encrypted);
assert_eq!( assert_eq!(
updated.encryption_key_hash, updated.encryption_key_hash,
Some("sha256:xyz789".to_string()) Some("sha256:xyz789".to_string())
@@ -468,7 +468,7 @@ async fn test_update_multiple_fields() {
assert_eq!(updated.name, new_name); assert_eq!(updated.name, new_name);
assert_eq!(updated.value, "updated_value"); assert_eq!(updated.value, "updated_value");
assert_eq!(updated.encrypted, true); assert!(updated.encrypted);
assert_eq!(updated.encryption_key_hash, Some("hash123".to_string())); assert_eq!(updated.encryption_key_hash, Some("hash123".to_string()));
} }
@@ -768,10 +768,10 @@ async fn test_key_encrypted_flag() {
.await .await
.unwrap(); .unwrap();
assert_eq!(plain_key.encrypted, false); assert!(!plain_key.encrypted);
assert_eq!(plain_key.encryption_key_hash, None); assert_eq!(plain_key.encryption_key_hash, None);
assert_eq!(encrypted_key.encrypted, true); assert!(encrypted_key.encrypted);
assert_eq!( assert_eq!(
encrypted_key.encryption_key_hash, encrypted_key.encryption_key_hash,
Some("sha256:abc".to_string()) Some("sha256:abc".to_string())
@@ -788,7 +788,7 @@ async fn test_update_encryption_status() {
.await .await
.unwrap(); .unwrap();
assert_eq!(key.encrypted, false); assert!(!key.encrypted);
// Encrypt it // Encrypt it
let input = UpdateKeyInput { let input = UpdateKeyInput {
@@ -800,7 +800,7 @@ async fn test_update_encryption_status() {
let encrypted = KeyRepository::update(&pool, key.id, input).await.unwrap(); let encrypted = KeyRepository::update(&pool, key.id, input).await.unwrap();
assert_eq!(encrypted.encrypted, true); assert!(encrypted.encrypted);
assert_eq!( assert_eq!(
encrypted.encryption_key_hash, encrypted.encryption_key_hash,
Some("sha256:newkey".to_string()) Some("sha256:newkey".to_string())
@@ -817,7 +817,7 @@ async fn test_update_encryption_status() {
let decrypted = KeyRepository::update(&pool, key.id, input).await.unwrap(); let decrypted = KeyRepository::update(&pool, key.id, input).await.unwrap();
assert_eq!(decrypted.encrypted, false); assert!(!decrypted.encrypted);
assert_eq!(decrypted.value, "plain_value"); assert_eq!(decrypted.value, "plain_value");
} }

View File

@@ -892,7 +892,7 @@ async fn test_port_range() {
let worker = WorkerRepository::create(&pool, input) let worker = WorkerRepository::create(&pool, input)
.await .await
.expect(&format!("Failed to create worker with port {}", port)); .unwrap_or_else(|_| panic!("Failed to create worker with port {}", port));
assert_eq!(worker.port, Some(port)); assert_eq!(worker.port, Some(port));
} }

View File

@@ -74,7 +74,7 @@ async fn test_create_rule() {
rule.conditions, rule.conditions,
json!({"equals": {"event.status": "success"}}) json!({"equals": {"event.status": "success"}})
); );
assert_eq!(rule.enabled, true); assert!(rule.enabled);
assert!(rule.created.timestamp() > 0); assert!(rule.created.timestamp() > 0);
assert!(rule.updated.timestamp() > 0); assert!(rule.updated.timestamp() > 0);
} }
@@ -117,7 +117,7 @@ async fn test_create_rule_disabled() {
let rule = RuleRepository::create(&pool, input).await.unwrap(); let rule = RuleRepository::create(&pool, input).await.unwrap();
assert_eq!(rule.enabled, false); assert!(!rule.enabled);
} }
#[tokio::test] #[tokio::test]
@@ -759,7 +759,7 @@ async fn test_update_rule_enabled() {
.await .await
.unwrap(); .unwrap();
assert_eq!(updated.enabled, false); assert!(!updated.enabled);
} }
#[tokio::test] #[tokio::test]
@@ -816,7 +816,7 @@ async fn test_update_rule_multiple_fields() {
assert_eq!(updated.label, "New Label"); assert_eq!(updated.label, "New Label");
assert_eq!(updated.description, "New Description"); assert_eq!(updated.description, "New Description");
assert_eq!(updated.conditions, json!({"updated": true})); assert_eq!(updated.conditions, json!({"updated": true}));
assert_eq!(updated.enabled, false); assert!(!updated.enabled);
} }
#[tokio::test] #[tokio::test]

View File

@@ -61,7 +61,7 @@ async fn test_create_sensor_minimal() {
assert_eq!(sensor.runtime_ref, runtime.r#ref); assert_eq!(sensor.runtime_ref, runtime.r#ref);
assert_eq!(sensor.trigger, trigger.id); assert_eq!(sensor.trigger, trigger.id);
assert_eq!(sensor.trigger_ref, trigger.r#ref); assert_eq!(sensor.trigger_ref, trigger.r#ref);
assert_eq!(sensor.enabled, true); assert!(sensor.enabled);
assert_eq!(sensor.param_schema, None); assert_eq!(sensor.param_schema, None);
assert!(sensor.created.timestamp() > 0); assert!(sensor.created.timestamp() > 0);
assert!(sensor.updated.timestamp() > 0); assert!(sensor.updated.timestamp() > 0);
@@ -796,7 +796,7 @@ async fn test_update_enabled_status() {
.await .await
.unwrap(); .unwrap();
assert_eq!(sensor.enabled, true); assert!(sensor.enabled);
let input = UpdateSensorInput { let input = UpdateSensorInput {
enabled: Some(false), enabled: Some(false),
@@ -807,7 +807,7 @@ async fn test_update_enabled_status() {
.await .await
.unwrap(); .unwrap();
assert_eq!(updated.enabled, false); assert!(!updated.enabled);
// Enable it again // Enable it again
let input = UpdateSensorInput { let input = UpdateSensorInput {
@@ -819,7 +819,7 @@ async fn test_update_enabled_status() {
.await .await
.unwrap(); .unwrap();
assert_eq!(updated.enabled, true); assert!(updated.enabled);
} }
#[tokio::test] #[tokio::test]
@@ -924,7 +924,7 @@ async fn test_update_multiple_fields() {
assert_eq!(updated.label, "Multi Update"); assert_eq!(updated.label, "Multi Update");
assert_eq!(updated.description, "Updated multiple fields"); assert_eq!(updated.description, "Updated multiple fields");
assert_eq!(updated.entrypoint, "sensors/multi.py"); assert_eq!(updated.entrypoint, "sensors/multi.py");
assert_eq!(updated.enabled, false); assert!(!updated.enabled);
assert_eq!(updated.param_schema, Some(json!({"type": "object"}))); assert_eq!(updated.param_schema, Some(json!({"type": "object"})));
} }

View File

@@ -42,7 +42,7 @@ async fn test_create_trigger() {
assert_eq!(trigger.pack, Some(pack.id)); assert_eq!(trigger.pack, Some(pack.id));
assert_eq!(trigger.pack_ref, Some(pack.r#ref)); assert_eq!(trigger.pack_ref, Some(pack.r#ref));
assert_eq!(trigger.label, "Webhook Trigger"); assert_eq!(trigger.label, "Webhook Trigger");
assert_eq!(trigger.enabled, true); assert!(trigger.enabled);
assert!(trigger.created.timestamp() > 0); assert!(trigger.created.timestamp() > 0);
assert!(trigger.updated.timestamp() > 0); assert!(trigger.updated.timestamp() > 0);
} }
@@ -134,7 +134,7 @@ async fn test_create_trigger_disabled() {
let trigger = TriggerRepository::create(&pool, input).await.unwrap(); let trigger = TriggerRepository::create(&pool, input).await.unwrap();
assert_eq!(trigger.enabled, false); assert!(!trigger.enabled);
} }
#[tokio::test] #[tokio::test]
@@ -478,7 +478,7 @@ async fn test_update_trigger() {
assert_eq!(updated.r#ref, trigger.r#ref); // Ref should not change assert_eq!(updated.r#ref, trigger.r#ref); // Ref should not change
assert_eq!(updated.label, "Updated Label"); assert_eq!(updated.label, "Updated Label");
assert_eq!(updated.description, Some("Updated description".to_string())); assert_eq!(updated.description, Some("Updated description".to_string()));
assert_eq!(updated.enabled, false); assert!(!updated.enabled);
assert!(updated.updated > original_updated); assert!(updated.updated > original_updated);
} }

View File

@@ -546,7 +546,7 @@ mod tests {
let manager = TimerManager::new(api_client).await.unwrap(); let manager = TimerManager::new(api_client).await.unwrap();
// Test various valid cron expressions // Test various valid cron expressions
let expressions = vec![ let expressions = [
"0 0 * * * *", // Every hour "0 0 * * * *", // Every hour
"0 */15 * * * *", // Every 15 minutes "0 */15 * * * *", // Every 15 minutes
"0 0 0 * * *", // Daily at midnight "0 0 0 * * *", // Daily at midnight

View File

@@ -150,6 +150,7 @@ impl TimerConfig {
/// Rule lifecycle event types /// Rule lifecycle event types
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "event_type", rename_all = "PascalCase")] #[serde(tag = "event_type", rename_all = "PascalCase")]
#[allow(clippy::enum_variant_names)]
pub enum RuleLifecycleEvent { pub enum RuleLifecycleEvent {
RuleCreated { RuleCreated {
rule_id: i64, rule_id: i64,

View File

@@ -116,13 +116,7 @@ impl CompletionListener {
// Verify execution exists in database // Verify execution exists in database
let execution = ExecutionRepository::find_by_id(pool, execution_id).await?; let execution = ExecutionRepository::find_by_id(pool, execution_id).await?;
if execution.is_none() { if let Some(ref exec) = execution {
warn!(
"Execution {} not found in database, but still releasing queue slot",
execution_id
);
} else {
let exec = execution.as_ref().unwrap();
debug!( debug!(
"Execution {} found with status: {:?}", "Execution {} found with status: {:?}",
execution_id, exec.status execution_id, exec.status
@@ -180,6 +174,11 @@ impl CompletionListener {
} }
} }
} }
} else {
warn!(
"Execution {} not found in database, but still releasing queue slot",
execution_id
);
} }
// Release queue slot for this action // Release queue slot for this action

View File

@@ -77,8 +77,7 @@ impl DeadLetterHandler {
info!("Dead letter handler stopping, rejecting message"); info!("Dead letter handler stopping, rejecting message");
return Err(attune_common::mq::MqError::Consume( return Err(attune_common::mq::MqError::Consume(
"Handler is shutting down".to_string(), "Handler is shutting down".to_string(),
) ));
.into());
} }
} }

View File

@@ -189,7 +189,7 @@ impl EventProcessor {
let payload_dict = payload let payload_dict = payload
.as_object() .as_object()
.cloned() .cloned()
.unwrap_or_else(|| serde_json::Map::new()); .unwrap_or_else(serde_json::Map::new);
// Resolve action parameters using the template resolver // Resolve action parameters using the template resolver
let resolved_params = Self::resolve_action_params(pool, rule, event, &payload).await?; let resolved_params = Self::resolve_action_params(pool, rule, event, &payload).await?;
@@ -248,7 +248,7 @@ impl EventProcessor {
}; };
// If rule has no conditions, it always matches // If rule has no conditions, it always matches
if rule.conditions.is_null() || rule.conditions.as_array().map_or(true, |a| a.is_empty()) { if rule.conditions.is_null() || rule.conditions.as_array().is_none_or(|a| a.is_empty()) {
debug!("Rule {} has no conditions, matching by default", rule.r#ref); debug!("Rule {} has no conditions, matching by default", rule.r#ref);
return Ok(true); return Ok(true);
} }
@@ -364,7 +364,7 @@ impl EventProcessor {
let action_params = &rule.action_params; let action_params = &rule.action_params;
// If there are no action params, return empty // If there are no action params, return empty
if action_params.is_null() || action_params.as_object().map_or(true, |o| o.is_empty()) { if action_params.is_null() || action_params.as_object().is_none_or(|o| o.is_empty()) {
return Ok(serde_json::Map::new()); return Ok(serde_json::Map::new());
} }

View File

@@ -257,7 +257,6 @@ mod tests {
fn test_execution_manager_creation() { fn test_execution_manager_creation() {
// This is a placeholder test // This is a placeholder test
// Real tests will require database and message queue setup // Real tests will require database and message queue setup
assert!(true);
} }
#[test] #[test]

View File

@@ -21,6 +21,7 @@ use crate::queue_manager::ExecutionQueueManager;
/// Policy violation type /// Policy violation type
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[allow(clippy::enum_variant_names)]
pub enum PolicyViolation { pub enum PolicyViolation {
/// Rate limit exceeded /// Rate limit exceeded
RateLimitExceeded { RateLimitExceeded {
@@ -78,7 +79,7 @@ impl std::fmt::Display for PolicyViolation {
} }
/// Execution policy configuration /// Execution policy configuration
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ExecutionPolicy { pub struct ExecutionPolicy {
/// Rate limit: maximum executions per time window /// Rate limit: maximum executions per time window
pub rate_limit: Option<RateLimit>, pub rate_limit: Option<RateLimit>,
@@ -88,16 +89,6 @@ pub struct ExecutionPolicy {
pub quotas: Option<HashMap<String, u64>>, pub quotas: Option<HashMap<String, u64>>,
} }
impl Default for ExecutionPolicy {
fn default() -> Self {
Self {
rate_limit: None,
concurrency_limit: None,
quotas: None,
}
}
}
/// Rate limit configuration /// Rate limit configuration
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimit { pub struct RateLimit {

View File

@@ -78,11 +78,7 @@ fn apply_param_defaults(params: JsonValue, param_schema: &Option<JsonValue>) ->
if let Some(schema_obj) = schema.as_object() { if let Some(schema_obj) = schema.as_object() {
for (key, prop) in schema_obj { for (key, prop) in schema_obj {
// Only fill in missing / null parameters // Only fill in missing / null parameters
let needs_default = match obj.get(key) { let needs_default = matches!(obj.get(key), None | Some(JsonValue::Null));
None => true,
Some(JsonValue::Null) => true,
_ => false,
};
if needs_default { if needs_default {
if let Some(default_val) = prop.get("default") { if let Some(default_val) = prop.get("default") {
debug!("Applying default for parameter '{}': {}", key, default_val); debug!("Applying default for parameter '{}': {}", key, default_val);
@@ -411,6 +407,7 @@ impl ExecutionScheduler {
/// `triggered_by` is the name of the predecessor task whose completion /// `triggered_by` is the name of the predecessor task whose completion
/// caused this task to be scheduled. Pass `None` for entry-point tasks /// caused this task to be scheduled. Pass `None` for entry-point tasks
/// dispatched at workflow start. /// dispatched at workflow start.
#[allow(clippy::too_many_arguments)]
async fn dispatch_workflow_task( async fn dispatch_workflow_task(
pool: &PgPool, pool: &PgPool,
publisher: &Publisher, publisher: &Publisher,
@@ -496,10 +493,8 @@ impl ExecutionScheduler {
let task_config: Option<JsonValue> = let task_config: Option<JsonValue> =
if rendered_input.is_object() && !rendered_input.as_object().unwrap().is_empty() { if rendered_input.is_object() && !rendered_input.as_object().unwrap().is_empty() {
Some(rendered_input.clone()) Some(rendered_input.clone())
} else if let Some(parent_config) = &parent_execution.config {
Some(parent_config.clone())
} else { } else {
None parent_execution.config.clone()
}; };
// Build workflow task metadata // Build workflow task metadata
@@ -660,10 +655,8 @@ impl ExecutionScheduler {
let task_config: Option<JsonValue> = let task_config: Option<JsonValue> =
if rendered_input.is_object() && !rendered_input.as_object().unwrap().is_empty() { if rendered_input.is_object() && !rendered_input.as_object().unwrap().is_empty() {
Some(rendered_input.clone()) Some(rendered_input.clone())
} else if let Some(parent_config) = &parent_execution.config {
Some(parent_config.clone())
} else { } else {
None parent_execution.config.clone()
}; };
let workflow_task = WorkflowTaskMetadata { let workflow_task = WorkflowTaskMetadata {
@@ -1108,16 +1101,16 @@ impl ExecutionScheduler {
let mut task_results_map: HashMap<String, JsonValue> = HashMap::new(); let mut task_results_map: HashMap<String, JsonValue> = HashMap::new();
for child in &child_executions { for child in &child_executions {
if let Some(ref wt) = child.workflow_task { if let Some(ref wt) = child.workflow_task {
if wt.workflow_execution == workflow_execution_id { if wt.workflow_execution == workflow_execution_id
if matches!( && matches!(
child.status, child.status,
ExecutionStatus::Completed ExecutionStatus::Completed
| ExecutionStatus::Failed | ExecutionStatus::Failed
| ExecutionStatus::Timeout | ExecutionStatus::Timeout
) { )
let result_val = child.result.clone().unwrap_or(serde_json::json!({})); {
task_results_map.insert(wt.task_name.clone(), result_val); let result_val = child.result.clone().unwrap_or(serde_json::json!({}));
} task_results_map.insert(wt.task_name.clone(), result_val);
} }
} }
} }
@@ -1514,7 +1507,7 @@ impl ExecutionScheduler {
// Filter by heartbeat freshness (only workers with recent heartbeats) // Filter by heartbeat freshness (only workers with recent heartbeats)
let fresh_workers: Vec<_> = active_workers let fresh_workers: Vec<_> = active_workers
.into_iter() .into_iter()
.filter(|w| Self::is_worker_heartbeat_fresh(w)) .filter(Self::is_worker_heartbeat_fresh)
.collect(); .collect();
if fresh_workers.is_empty() { if fresh_workers.is_empty() {
@@ -1749,27 +1742,23 @@ mod tests {
fn test_scheduler_creation() { fn test_scheduler_creation() {
// This is a placeholder test // This is a placeholder test
// Real tests will require database and message queue setup // Real tests will require database and message queue setup
assert!(true);
} }
#[test] #[test]
fn test_concurrency_limit_dispatch_count() { fn test_concurrency_limit_dispatch_count() {
// Verify the dispatch_count calculation used by dispatch_with_items_task // Verify the dispatch_count calculation used by dispatch_with_items_task
let total = 20usize; let total = 20usize;
let concurrency: Option<usize> = Some(3); let concurrency_limit = 3usize;
let concurrency_limit = concurrency.unwrap_or(total);
let dispatch_count = total.min(concurrency_limit); let dispatch_count = total.min(concurrency_limit);
assert_eq!(dispatch_count, 3); assert_eq!(dispatch_count, 3);
// No concurrency limit → default to serial (1 at a time) // No concurrency limit → default to serial (1 at a time)
let concurrency: Option<usize> = None; let concurrency_limit = 1usize;
let concurrency_limit = concurrency.unwrap_or(1);
let dispatch_count = total.min(concurrency_limit); let dispatch_count = total.min(concurrency_limit);
assert_eq!(dispatch_count, 1); assert_eq!(dispatch_count, 1);
// Concurrency exceeds total → dispatch all // Concurrency exceeds total → dispatch all
let concurrency: Option<usize> = Some(50); let concurrency_limit = 50usize;
let concurrency_limit = concurrency.unwrap_or(total);
let dispatch_count = total.min(concurrency_limit); let dispatch_count = total.min(concurrency_limit);
assert_eq!(dispatch_count, 20); assert_eq!(dispatch_count, 20);
} }

View File

@@ -406,7 +406,7 @@ impl WorkerHealthProbe {
runtime_array.iter().any(|v| { runtime_array.iter().any(|v| {
v.as_str() v.as_str()
.map_or(false, |s| s.eq_ignore_ascii_case(runtime_name)) .is_some_and(|s| s.eq_ignore_ascii_case(runtime_name))
}) })
} }
} }

View File

@@ -1129,7 +1129,7 @@ mod tests {
let mut publish_map = HashMap::new(); let mut publish_map = HashMap::new();
publish_map.insert("flag".to_string(), JsonValue::Bool(true)); publish_map.insert("flag".to_string(), JsonValue::Bool(true));
publish_map.insert("count".to_string(), json!(42)); publish_map.insert("count".to_string(), json!(42));
publish_map.insert("ratio".to_string(), json!(3.14)); publish_map.insert("ratio".to_string(), json!(3.15));
publish_map.insert("nothing".to_string(), JsonValue::Null); publish_map.insert("nothing".to_string(), JsonValue::Null);
publish_map.insert( publish_map.insert(
"template".to_string(), "template".to_string(),
@@ -1152,7 +1152,7 @@ mod tests {
assert!(ctx.get_var("count").unwrap().is_number()); assert!(ctx.get_var("count").unwrap().is_number());
// Float preserved // Float preserved
assert_eq!(ctx.get_var("ratio").unwrap(), json!(3.14)); assert_eq!(ctx.get_var("ratio").unwrap(), json!(3.15));
// Null preserved // Null preserved
assert_eq!(ctx.get_var("nothing").unwrap(), json!(null)); assert_eq!(ctx.get_var("nothing").unwrap(), json!(null));

View File

@@ -965,7 +965,7 @@ tasks:
publish: publish:
- validation_passed: true - validation_passed: true
- count: 42 - count: 42
- ratio: 3.14 - ratio: 3.15
- label: "hello" - label: "hello"
- template_val: "{{ result().data }}" - template_val: "{{ result().data }}"
- nothing: null - nothing: null
@@ -1003,7 +1003,7 @@ tasks:
assert_eq!(publish_map["count"], &serde_json::json!(42)); assert_eq!(publish_map["count"], &serde_json::json!(42));
// Float is preserved as a JSON number // Float is preserved as a JSON number
assert_eq!(publish_map["ratio"], &serde_json::json!(3.14)); assert_eq!(publish_map["ratio"], &serde_json::json!(3.15));
// Plain string stays as a string // Plain string stays as a string
assert_eq!( assert_eq!(

View File

@@ -357,7 +357,7 @@ impl SensorManager {
let mut child = cmd let mut child = cmd
.env("ATTUNE_API_URL", &self.inner.api_url) .env("ATTUNE_API_URL", &self.inner.api_url)
.env("ATTUNE_API_TOKEN", &token_response.token) .env("ATTUNE_API_TOKEN", &token_response.token)
.env("ATTUNE_SENSOR_ID", &sensor.id.to_string()) .env("ATTUNE_SENSOR_ID", sensor.id.to_string())
.env("ATTUNE_SENSOR_REF", &sensor.r#ref) .env("ATTUNE_SENSOR_REF", &sensor.r#ref)
.env("ATTUNE_SENSOR_TRIGGERS", &trigger_instances_json) .env("ATTUNE_SENSOR_TRIGGERS", &trigger_instances_json)
.env("ATTUNE_MQ_URL", &self.inner.mq_url) .env("ATTUNE_MQ_URL", &self.inner.mq_url)
@@ -731,6 +731,7 @@ impl SensorInstance {
/// Sensor status information /// Sensor status information
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[derive(Default)]
pub struct SensorStatus { pub struct SensorStatus {
/// Whether the sensor is running /// Whether the sensor is running
pub running: bool, pub running: bool,
@@ -745,16 +746,6 @@ pub struct SensorStatus {
pub last_poll: Option<chrono::DateTime<chrono::Utc>>, pub last_poll: Option<chrono::DateTime<chrono::Utc>>,
} }
impl Default for SensorStatus {
fn default() -> Self {
Self {
running: false,
failed: false,
failure_count: 0,
last_poll: None,
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View File

@@ -381,14 +381,12 @@ async fn setup_environments_for_pack(
) )
.await; .await;
// Also set up version-specific environments // Also set up version-specific environments
let versions = match RuntimeVersionRepository::find_available_by_runtime( let versions: Vec<attune_common::models::RuntimeVersion> =
db_pool, runtime_id, RuntimeVersionRepository::find_available_by_runtime(
) db_pool, runtime_id,
.await )
{ .await
Ok(v) => v, .unwrap_or_default();
Err(_) => Vec::new(),
};
setup_version_environments_from_list( setup_version_environments_from_list(
&versions, &versions,
&rt_name, &rt_name,

View File

@@ -63,6 +63,7 @@ fn normalize_api_url(raw_url: &str) -> String {
impl ActionExecutor { impl ActionExecutor {
/// Create a new action executor /// Create a new action executor
#[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
pool: PgPool, pool: PgPool,
runtime_registry: RuntimeRegistry, runtime_registry: RuntimeRegistry,
@@ -359,18 +360,16 @@ impl ActionExecutor {
// Add context data as environment variables from config // Add context data as environment variables from config
if let Some(config) = &execution.config { if let Some(config) = &execution.config {
if let Some(context) = config.get("context") { if let Some(JsonValue::Object(map)) = config.get("context") {
if let JsonValue::Object(map) = context { for (key, value) in map {
for (key, value) in map { let env_key = format!("ATTUNE_CONTEXT_{}", key.to_uppercase());
let env_key = format!("ATTUNE_CONTEXT_{}", key.to_uppercase()); let env_value = match value {
let env_value = match value { JsonValue::String(s) => s.clone(),
JsonValue::String(s) => s.clone(), JsonValue::Number(n) => n.to_string(),
JsonValue::Number(n) => n.to_string(), JsonValue::Bool(b) => b.to_string(),
JsonValue::Bool(b) => b.to_string(), _ => serde_json::to_string(value)?,
_ => serde_json::to_string(value)?, };
}; env.insert(env_key, env_value);
env.insert(env_key, env_value);
}
} }
} }
} }

View File

@@ -45,7 +45,7 @@ impl WorkerRegistration {
let worker_type = config let worker_type = config
.worker .worker
.as_ref() .as_ref()
.and_then(|w| w.worker_type.clone()) .and_then(|w| w.worker_type)
.unwrap_or(WorkerType::Local); .unwrap_or(WorkerType::Local);
let worker_role = WorkerRole::Action; let worker_role = WorkerRole::Action;
@@ -180,8 +180,8 @@ impl WorkerRegistration {
"#, "#,
) )
.bind(&self.worker_name) .bind(&self.worker_name)
.bind(&self.worker_type) .bind(self.worker_type)
.bind(&self.worker_role) .bind(self.worker_role)
.bind(self.runtime_id) .bind(self.runtime_id)
.bind(&self.host) .bind(&self.host)
.bind(self.port) .bind(self.port)

View File

@@ -35,6 +35,7 @@ impl NativeRuntime {
} }
/// Execute a native binary with parameters and environment variables /// Execute a native binary with parameters and environment variables
#[allow(clippy::too_many_arguments)]
async fn execute_binary( async fn execute_binary(
&self, &self,
binary_path: PathBuf, binary_path: PathBuf,

View File

@@ -117,7 +117,7 @@ pub fn create_parameter_file(
) -> Result<NamedTempFile, RuntimeError> { ) -> Result<NamedTempFile, RuntimeError> {
let formatted = format_parameters(parameters, format)?; let formatted = format_parameters(parameters, format)?;
let mut temp_file = NamedTempFile::new().map_err(|e| RuntimeError::IoError(e))?; let mut temp_file = NamedTempFile::new().map_err(RuntimeError::IoError)?;
// Set restrictive permissions (owner read-only) // Set restrictive permissions (owner read-only)
#[cfg(unix)] #[cfg(unix)]
@@ -126,20 +126,20 @@ pub fn create_parameter_file(
let mut perms = temp_file let mut perms = temp_file
.as_file() .as_file()
.metadata() .metadata()
.map_err(|e| RuntimeError::IoError(e))? .map_err(RuntimeError::IoError)?
.permissions(); .permissions();
perms.set_mode(0o400); // Read-only for owner perms.set_mode(0o400); // Read-only for owner
temp_file temp_file
.as_file() .as_file()
.set_permissions(perms) .set_permissions(perms)
.map_err(|e| RuntimeError::IoError(e))?; .map_err(RuntimeError::IoError)?;
} }
temp_file temp_file
.write_all(formatted.as_bytes()) .write_all(formatted.as_bytes())
.map_err(|e| RuntimeError::IoError(e))?; .map_err(RuntimeError::IoError)?;
temp_file.flush().map_err(|e| RuntimeError::IoError(e))?; temp_file.flush().map_err(RuntimeError::IoError)?;
debug!( debug!(
"Created parameter file at {:?} with format {:?}", "Created parameter file at {:?} with format {:?}",

View File

@@ -83,6 +83,7 @@ pub async fn execute_streaming(
/// * `max_stderr_bytes` - Maximum stderr size before truncation /// * `max_stderr_bytes` - Maximum stderr size before truncation
/// * `output_format` - How to parse stdout (Text, Json, Yaml, Jsonl) /// * `output_format` - How to parse stdout (Text, Json, Yaml, Jsonl)
/// * `cancel_token` - Optional cancellation token for graceful process termination /// * `cancel_token` - Optional cancellation token for graceful process termination
#[allow(clippy::too_many_arguments)]
pub async fn execute_streaming_cancellable( pub async fn execute_streaming_cancellable(
mut cmd: Command, mut cmd: Command,
secrets: &HashMap<String, String>, secrets: &HashMap<String, String>,

View File

@@ -61,6 +61,7 @@ impl ShellRuntime {
} }
/// Execute with streaming and bounded log collection /// Execute with streaming and bounded log collection
#[allow(clippy::too_many_arguments)]
async fn execute_with_streaming( async fn execute_with_streaming(
&self, &self,
mut cmd: Command, mut cmd: Command,
@@ -383,6 +384,7 @@ impl ShellRuntime {
} }
/// Execute shell script from file /// Execute shell script from file
#[allow(clippy::too_many_arguments)]
async fn execute_shell_file( async fn execute_shell_file(
&self, &self,
script_path: PathBuf, script_path: PathBuf,

View File

@@ -220,7 +220,7 @@ impl SecretManager {
.map_err(|e| Error::Internal(format!("Encryption failed: {}", e)))?; .map_err(|e| Error::Internal(format!("Encryption failed: {}", e)))?;
// Format: "nonce:ciphertext" (both base64-encoded) // Format: "nonce:ciphertext" (both base64-encoded)
let nonce_b64 = BASE64.encode(&nonce); let nonce_b64 = BASE64.encode(nonce);
let ciphertext_b64 = BASE64.encode(&ciphertext); let ciphertext_b64 = BASE64.encode(&ciphertext);
Ok(format!("{}:{}", nonce_b64, ciphertext_b64)) Ok(format!("{}:{}", nonce_b64, ciphertext_b64))

View File

@@ -443,6 +443,7 @@ impl WorkerService {
/// 3. Wait for in-flight tasks with timeout /// 3. Wait for in-flight tasks with timeout
/// 4. Close MQ connection /// 4. Close MQ connection
/// 5. Close DB connection /// 5. Close DB connection
///
/// Verify which runtime versions are available on this host/container. /// Verify which runtime versions are available on this host/container.
/// ///
/// Runs each version's verification commands (from `distributions` JSONB) /// Runs each version's verification commands (from `distributions` JSONB)
@@ -634,7 +635,7 @@ impl WorkerService {
shutdown_timeout shutdown_timeout
); );
let timeout_duration = Duration::from_secs(shutdown_timeout as u64); let timeout_duration = Duration::from_secs(shutdown_timeout);
match tokio::time::timeout(timeout_duration, self.wait_for_in_flight_tasks()).await { match tokio::time::timeout(timeout_duration, self.wait_for_in_flight_tasks()).await {
Ok(_) => info!("All in-flight tasks completed"), Ok(_) => info!("All in-flight tasks completed"),
Err(_) => warn!("Shutdown timeout reached - some tasks may have been interrupted"), Err(_) => warn!("Shutdown timeout reached - some tasks may have been interrupted"),

View File

@@ -90,7 +90,7 @@ pub async fn verify_all_runtime_versions(
let rt_base_name = version let rt_base_name = version
.runtime_ref .runtime_ref
.split('.') .split('.')
.last() .next_back()
.unwrap_or(&version.runtime_ref) .unwrap_or(&version.runtime_ref)
.to_lowercase(); .to_lowercase();

View File

@@ -1,6 +1,6 @@
{ {
"$schema": "https://unpkg.com/knip@latest/schema.json", "$schema": "https://unpkg.com/knip@latest/schema.json",
"entry": ["src/main.tsx", "vite.config.ts"], "entry": ["src/**/*.{ts,tsx}", "vite.config.ts"],
"project": ["src/**/*.{ts,tsx}", "scripts/**/*.js"], "project": ["src/**/*.{ts,tsx}", "scripts/**/*.js"],
"ignore": ["src/api/**", "dist/**", "node_modules/**"] "ignore": ["src/api/**", "dist/**", "node_modules/**"]
} }

272
web/package-lock.json generated
View File

@@ -15,8 +15,7 @@
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-router-dom": "^7.12.0", "react-router-dom": "^7.12.0"
"zustand": "^5.0.10"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
@@ -1100,9 +1099,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
"integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==", "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -1114,9 +1113,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
"integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==", "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1128,9 +1127,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
"integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1142,9 +1141,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
"integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1156,9 +1155,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
"integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==", "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1170,9 +1169,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
"integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==", "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1184,9 +1183,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
"integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==", "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -1198,9 +1197,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
"integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==", "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -1212,9 +1211,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
"integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1226,9 +1225,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
"integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==", "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1240,9 +1239,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-gnu": { "node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
"integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==", "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -1254,9 +1253,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-musl": { "node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
"integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==", "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -1268,9 +1267,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-gnu": { "node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
"integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==", "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -1282,9 +1281,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-musl": { "node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
"integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==", "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -1296,9 +1295,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
"integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==", "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -1310,9 +1309,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-musl": { "node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
"integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==", "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -1324,9 +1323,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
"integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==", "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -1338,9 +1337,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
"integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1352,9 +1351,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
"integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==", "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1366,9 +1365,9 @@
] ]
}, },
"node_modules/@rollup/rollup-openbsd-x64": { "node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
"integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==", "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1380,9 +1379,9 @@
] ]
}, },
"node_modules/@rollup/rollup-openharmony-arm64": { "node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
"integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==", "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1394,9 +1393,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
"integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1408,9 +1407,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
"integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==", "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -1422,9 +1421,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-gnu": { "node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
"integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==", "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1436,9 +1435,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
"integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1557,7 +1556,7 @@
"version": "19.2.10", "version": "19.2.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
"integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -1774,13 +1773,13 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.5", "version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.2"
}, },
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
@@ -1890,9 +1889,9 @@
} }
}, },
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.12.6", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2000,13 +1999,13 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.13.4", "version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.11",
"form-data": "^4.0.4", "form-data": "^4.0.5",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
@@ -2322,7 +2321,7 @@
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/date-fns": { "node_modules/date-fns": {
@@ -3430,9 +3429,9 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
@@ -4036,9 +4035,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.57.0", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4052,31 +4051,31 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm-eabi": "4.59.0",
"@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-android-arm64": "4.59.0",
"@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.59.0",
"@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-darwin-x64": "4.59.0",
"@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.59.0",
"@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.59.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
"@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
"@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0",
"@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.59.0",
"@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0",
"@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.59.0",
"@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
"@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0",
"@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
"@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0",
"@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0",
"@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.59.0",
"@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.59.0",
"@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openbsd-x64": "4.59.0",
"@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.59.0",
"@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0",
"@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0",
"@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.59.0",
"@rollup/rollup-win32-x64-msvc": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.59.0",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@@ -4699,35 +4698,6 @@
"peerDependencies": { "peerDependencies": {
"zod": "^3.25.0 || ^4.0.0" "zod": "^3.25.0 || ^4.0.0"
} }
},
"node_modules/zustand": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.10.tgz",
"integrity": "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
} }
} }
} }

View File

@@ -20,8 +20,7 @@
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-router-dom": "^7.12.0", "react-router-dom": "^7.12.0"
"zustand": "^5.0.10"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",

View File

@@ -3,7 +3,7 @@ import { createRoot } from "react-dom/client";
import "./index.css"; import "./index.css";
import "./lib/api-config"; import "./lib/api-config";
import { initializeApiWrapper } from "./lib/api-wrapper"; import { initializeApiWrapper } from "./lib/api-wrapper";
import App from "./App.tsx"; import App from "./App";
// Initialize API wrapper for token refresh // Initialize API wrapper for token refresh
initializeApiWrapper(); initializeApiWrapper();