|
1 | | -use super::*; |
| 1 | +use super::{heartbeat_patch::apply_heartbeat_patch, *}; |
2 | 2 |
|
3 | 3 | /// Strip gateway-owned routing and trust markers from RPC-supplied params. |
4 | 4 | /// |
@@ -42,39 +42,6 @@ async fn prepare_chat_send_params(ctx: &MethodContext) -> serde_json::Value { |
42 | 42 | params |
43 | 43 | } |
44 | 44 |
|
45 | | -/// Overlay `patch` onto `base`, recursing into objects so a partial nested |
46 | | -/// object updates the keys it carries instead of replacing the whole thing. |
47 | | -fn overlay_json(base: &mut serde_json::Value, patch: &serde_json::Value) { |
48 | | - match (base, patch) { |
49 | | - (serde_json::Value::Object(base), serde_json::Value::Object(patch)) => { |
50 | | - for (key, value) in patch { |
51 | | - overlay_json( |
52 | | - base.entry(key.clone()).or_insert(serde_json::Value::Null), |
53 | | - value, |
54 | | - ); |
55 | | - } |
56 | | - }, |
57 | | - (base, patch) => *base = patch.clone(), |
58 | | - } |
59 | | -} |
60 | | - |
61 | | -/// Apply a `heartbeat.update` payload to the configuration already in effect. |
62 | | -/// |
63 | | -/// [`HeartbeatConfig`](moltis_config::schema::HeartbeatConfig) is |
64 | | -/// `#[serde(default)]`, so deserializing a payload on its own turns every key |
65 | | -/// the caller left out into that key's default. The settings form only sends |
66 | | -/// the fields it renders — it has no input for `wake_cooldown` or `agent_id` — |
67 | | -/// so treating the payload as a whole config quietly rewrites the rest. An |
68 | | -/// explicit `null` still clears an optional field. |
69 | | -fn apply_heartbeat_patch( |
70 | | - current: &moltis_config::schema::HeartbeatConfig, |
71 | | - patch: &serde_json::Value, |
72 | | -) -> Result<moltis_config::schema::HeartbeatConfig, serde_json::Error> { |
73 | | - let mut merged = serde_json::to_value(current)?; |
74 | | - overlay_json(&mut merged, patch); |
75 | | - serde_json::from_value(merged) |
76 | | -} |
77 | | - |
78 | 45 | pub(super) fn register(reg: &mut MethodRegistry) { |
79 | 46 | // Config |
80 | 47 | reg.register( |
@@ -1446,67 +1413,4 @@ mod tests { |
1446 | 1413 | assert_eq!(params["_tool_policy"]["deny"][0], "*"); |
1447 | 1414 | assert_eq!(params["_private_context"], false); |
1448 | 1415 | } |
1449 | | - |
1450 | | - #[test] |
1451 | | - fn a_heartbeat_update_leaves_fields_the_payload_omits_alone() -> anyhow::Result<()> { |
1452 | | - let current = moltis_config::schema::HeartbeatConfig { |
1453 | | - wake_cooldown: "1h".into(), |
1454 | | - agent_id: Some("night-shift".into()), |
1455 | | - ..Default::default() |
1456 | | - }; |
1457 | | - |
1458 | | - // What the settings form sends. It has no input for `wake_cooldown` or |
1459 | | - // `agent_id`, so neither key is in the payload. |
1460 | | - let saved = apply_heartbeat_patch( |
1461 | | - ¤t, |
1462 | | - &serde_json::json!({ |
1463 | | - "enabled": true, |
1464 | | - "every": "15m", |
1465 | | - "ack_max_chars": 300, |
1466 | | - "deliver": false, |
1467 | | - "sandbox_enabled": true, |
1468 | | - "active_hours": {"start": "07:00", "end": "23:00", "timezone": "local"}, |
1469 | | - }), |
1470 | | - )?; |
1471 | | - |
1472 | | - assert_eq!(saved.every, "15m"); |
1473 | | - assert_eq!(saved.wake_cooldown, "1h"); |
1474 | | - assert_eq!(saved.agent_id.as_deref(), Some("night-shift")); |
1475 | | - Ok(()) |
1476 | | - } |
1477 | | - |
1478 | | - #[test] |
1479 | | - fn a_partial_active_hours_moves_only_the_keys_it_carries() -> anyhow::Result<()> { |
1480 | | - let current = moltis_config::schema::HeartbeatConfig { |
1481 | | - active_hours: moltis_config::schema::ActiveHoursConfig { |
1482 | | - start: "09:00".into(), |
1483 | | - end: "21:00".into(), |
1484 | | - timezone: "UTC".into(), |
1485 | | - }, |
1486 | | - ..Default::default() |
1487 | | - }; |
1488 | | - |
1489 | | - let saved = apply_heartbeat_patch( |
1490 | | - ¤t, |
1491 | | - &serde_json::json!({"active_hours": {"start": "07:00"}}), |
1492 | | - )?; |
1493 | | - |
1494 | | - assert_eq!(saved.active_hours.start, "07:00"); |
1495 | | - assert_eq!(saved.active_hours.end, "21:00"); |
1496 | | - assert_eq!(saved.active_hours.timezone, "UTC"); |
1497 | | - Ok(()) |
1498 | | - } |
1499 | | - |
1500 | | - #[test] |
1501 | | - fn an_explicit_null_still_clears_an_optional_field() -> anyhow::Result<()> { |
1502 | | - let current = moltis_config::schema::HeartbeatConfig { |
1503 | | - model: Some("anthropic/claude-sonnet-4".into()), |
1504 | | - ..Default::default() |
1505 | | - }; |
1506 | | - |
1507 | | - let saved = apply_heartbeat_patch(¤t, &serde_json::json!({"model": null}))?; |
1508 | | - |
1509 | | - assert!(saved.model.is_none()); |
1510 | | - Ok(()) |
1511 | | - } |
1512 | 1416 | } |
0 commit comments