From 1c035228352bab93b698451c724233b83c1eef5e Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 30 Apr 2026 02:13:20 +0000 Subject: [PATCH] fix: expand empty packages to all available patches + refresh_listener UPSERT BUG-15: Empty patch_selection sent to agent as-is, causing apply nothing instead of apply all available patches per SPEC. When packages is empty, now query host_patch_data and expand to full package list. BUG-16: refresh_listener used INSERT instead of UPSERT for host_patch_data, causing duplicate key constraint errors. --- crates/pm-worker/src/job_executor.rs | 42 +++++++++++++++++++++++- crates/pm-worker/src/refresh_listener.rs | 6 ++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/crates/pm-worker/src/job_executor.rs b/crates/pm-worker/src/job_executor.rs index 192cc93..bbf2ca2 100644 --- a/crates/pm-worker/src/job_executor.rs +++ b/crates/pm-worker/src/job_executor.rs @@ -388,9 +388,49 @@ async fn execute_host_job( }, }; - let packages: Vec = + let mut packages: Vec = serde_json::from_value(patch_sel.patch_selection).unwrap_or_default(); + // ── 2b. Expand empty packages to all available patches ───────────────── + // Per SPEC: "empty = all available patches". The agent treats an empty + // list as "apply nothing", so we must expand it here. + if packages.is_empty() { + match sqlx::query_scalar::<_, serde_json::Value>( + r#" + SELECT available_patches + FROM host_patch_data + WHERE host_id = $1 + ORDER BY polled_at DESC + LIMIT 1 + "#, + ) + .bind(host_id) + .fetch_optional(&pool) + .await + { + Ok(Some(val)) => { + if let Ok(patches) = serde_json::from_value::>(val) { + for p in &patches { + if let Some(name) = p.get("name").and_then(|n| n.as_str()) { + packages.push(name.to_string()); + } + } + tracing::info!( + %pjh_id, + count = packages.len(), + "execute_host_job: expanded empty packages to all available patches" + ); + } + }, + Ok(None) => { + tracing::warn!(%pjh_id, "execute_host_job: no patch data for host, sending empty packages"); + }, + Err(e) => { + tracing::error!(%pjh_id, error = %e, "execute_host_job: failed to fetch patch data for expansion"); + }, + } + } + // ── 3. Load mTLS certs ─────────────────────────────────────────────────── let certs = match load_agent_certs(&config.security) { Ok(c) => c, diff --git a/crates/pm-worker/src/refresh_listener.rs b/crates/pm-worker/src/refresh_listener.rs index cf89ab2..f823828 100644 --- a/crates/pm-worker/src/refresh_listener.rs +++ b/crates/pm-worker/src/refresh_listener.rs @@ -181,6 +181,12 @@ async fn refresh_host( INSERT INTO host_patch_data (host_id, available_patches, installed_packages, patch_count, cve_count) VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (host_id) DO UPDATE SET + available_patches = EXCLUDED.available_patches, + installed_packages = EXCLUDED.installed_packages, + patch_count = EXCLUDED.patch_count, + cve_count = EXCLUDED.cve_count, + polled_at = NOW() "#, ) .bind(host.id)