diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index 96d94140..664eb692 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -273,9 +273,7 @@ for (const a of approvals) { if (seenApprovals.has(a.id)) continue; seenApprovals.add(a.id); - const verb = a.kind === 'spawn' ? 'spawn approval' - : a.kind === 'init_config' ? 'config-init approval' - : 'config commit'; + const verb = a.kind === 'spawn' ? 'spawn approval' : 'config commit'; NOTIF.show('◆ approval #' + a.id, `${verb} for ${a.agent}`, 'hyperhive:approval:' + a.id); } @@ -1229,7 +1227,6 @@ const ul = el('ul', { class: 'approvals' }); for (const a of pending) { const isApply = a.kind === 'apply_commit'; - const isInit = a.kind === 'init_config'; const li = el('li', { class: 'approval-card' }); // ── identity header ────────────────────────────────────────── @@ -1238,7 +1235,7 @@ el('span', { class: 'id' }, '#' + a.id), el('span', { class: 'agent' }, a.agent), el('span', { class: 'kind' + (isApply ? '' : ' kind-spawn') }, - isApply ? 'apply' : isInit ? 'init' : 'spawn'), + isApply ? 'apply' : 'spawn'), ); if (isApply && a.sha_short) head.append(el('code', {}, a.sha_short)); li.append(head); @@ -1264,9 +1261,7 @@ body.append(drill); } else { body.append(el('span', { class: 'meta' }, - isInit - ? 'scaffold proposed config repo — manager customises agent.nix before spawn' - : 'new sub-agent — container will be created on approve')); + 'new sub-agent — container will be created on approve')); } li.append(body); diff --git a/hive-c0re/src/approvals.rs b/hive-c0re/src/approvals.rs index ba9f9f2b..9b696c5c 100644 --- a/hive-c0re/src/approvals.rs +++ b/hive-c0re/src/approvals.rs @@ -137,7 +137,8 @@ impl Approvals { LIMIT ?1", )?; let rows = stmt.query_map([limit], row_to_approval)?; - Ok(collect_lenient(rows)) + rows.collect::>>() + .map_err(Into::into) } pub fn pending(&self) -> Result> { @@ -149,7 +150,8 @@ impl Approvals { ORDER BY id ASC", )?; let rows = stmt.query_map([], row_to_approval)?; - Ok(collect_lenient(rows)) + rows.collect::>>() + .map_err(Into::into) } pub fn get(&self, id: i64) -> Result> { @@ -259,32 +261,12 @@ impl Approvals { } } -/// Collect approval rows, dropping (and logging) any that fail to -/// deserialize. A single malformed / unknown-kind row must never blank -/// the whole list: `collect::>()` is all-or-nothing, so one -/// bad row used to make `pending()` / `recent_resolved()` error out -/// wholesale — the dashboard then rendered an empty approvals queue -/// (issue #160, an unhandled `init_config` kind poisoning every read). -fn collect_lenient( - rows: impl Iterator>, -) -> Vec { - rows.filter_map(|r| match r { - Ok(a) => Some(a), - Err(e) => { - tracing::warn!(error = ?e, "skipping unparseable approval row"); - None - } - }) - .collect() -} - fn row_to_approval(row: &rusqlite::Row<'_>) -> rusqlite::Result { // Column order: id, agent, kind, commit_ref, requested_at, status, resolved_at, note, fetched_sha, description. let kind: String = row.get(2)?; let kind = match kind.as_str() { "apply_commit" => ApprovalKind::ApplyCommit, "spawn" => ApprovalKind::Spawn, - "init_config" => ApprovalKind::InitConfig, other => { return Err(rusqlite::Error::FromSqlConversionFailure( 2, @@ -345,68 +327,3 @@ fn now_unix() -> i64 { .and_then(|d| i64::try_from(d.as_secs()).ok()) .unwrap_or(0) } - -#[cfg(test)] -mod tests { - use super::*; - use hive_sh4re::ApprovalKind; - - fn open_temp() -> (tempfile::TempDir, std::path::PathBuf, Approvals) { - let dir = tempfile::tempdir().expect("tempdir"); - let path = dir.path().join("approvals.sqlite"); - let db = Approvals::open(&path).expect("open approvals db"); - (dir, path, db) - } - - #[test] - fn init_config_approval_round_trips() { - // Regression for #160: an `init_config` row used to fail - // deserialization (row_to_approval matched only apply_commit + - // spawn), erroring out the whole `pending()` query — every - // approval then vanished from the dashboard. - let (_dir, _path, db) = open_temp(); - let id = db - .submit_kind("bitburner", ApprovalKind::InitConfig, "", Some("scaffold")) - .expect("submit init_config"); - let pending = db - .pending() - .expect("pending() must not error on an init_config row"); - assert_eq!(pending.len(), 1); - assert_eq!(pending[0].id, id); - assert!(matches!(pending[0].kind, ApprovalKind::InitConfig)); - } - - #[test] - fn mixed_kinds_all_listed() { - let (_dir, _path, db) = open_temp(); - db.submit_kind("a", ApprovalKind::ApplyCommit, "deadbeef", None) - .unwrap(); - db.submit_kind("b", ApprovalKind::Spawn, "", None).unwrap(); - db.submit_kind("c", ApprovalKind::InitConfig, "", None) - .unwrap(); - let pending = db.pending().expect("pending"); - assert_eq!(pending.len(), 3, "all three kinds must be visible"); - } - - #[test] - fn unknown_kind_row_is_skipped_not_fatal() { - // A single malformed / future-kind row must not blank the - // whole list — collect_lenient skips it instead of failing. - let (_dir, path, db) = open_temp(); - let good = db - .submit_kind("good", ApprovalKind::ApplyCommit, "cafe", None) - .unwrap(); - let raw = Connection::open(&path).unwrap(); - raw.execute( - "INSERT INTO approvals (agent, kind, commit_ref, requested_at, status) - VALUES ('weird', 'from_the_future', '', 0, 'pending')", - [], - ) - .unwrap(); - let pending = db - .pending() - .expect("pending() must survive an unparseable row"); - assert_eq!(pending.len(), 1); - assert_eq!(pending[0].id, good); - } -}