Skip to content

Commit dfe216e

Browse files
committed
fix(gateway): keep core service methods within size limit
The heartbeat patch fix pushed core.rs above the enforced 1,500-line limit, causing the Format CI job and release gates to fail before rustfmt ran. Move the self-contained patch helper and its tests into a sibling module without changing behavior.
1 parent b505b11 commit dfe216e

3 files changed

Lines changed: 101 additions & 97 deletions

File tree

crates/gateway/src/methods/services.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ mod channels;
512512
mod connectors;
513513
mod core;
514514
mod feedback;
515+
mod heartbeat_patch;
515516
mod instrumentation;
516517
mod modes;
517518
mod sessions;

crates/gateway/src/methods/services/core.rs

Lines changed: 1 addition & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::*;
1+
use super::{heartbeat_patch::apply_heartbeat_patch, *};
22

33
/// Strip gateway-owned routing and trust markers from RPC-supplied params.
44
///
@@ -42,39 +42,6 @@ async fn prepare_chat_send_params(ctx: &MethodContext) -> serde_json::Value {
4242
params
4343
}
4444

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-
7845
pub(super) fn register(reg: &mut MethodRegistry) {
7946
// Config
8047
reg.register(
@@ -1446,67 +1413,4 @@ mod tests {
14461413
assert_eq!(params["_tool_policy"]["deny"][0], "*");
14471414
assert_eq!(params["_private_context"], false);
14481415
}
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-
&current,
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-
&current,
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(&current, &serde_json::json!({"model": null}))?;
1508-
1509-
assert!(saved.model.is_none());
1510-
Ok(())
1511-
}
15121416
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use moltis_config::schema::HeartbeatConfig;
2+
3+
/// Apply a `heartbeat.update` payload to the configuration already in effect.
4+
///
5+
/// `HeartbeatConfig` is `#[serde(default)]`, so deserializing a payload on its
6+
/// own resets every omitted key to its default. An explicit `null` still clears
7+
/// an optional field.
8+
pub(super) fn apply_heartbeat_patch(
9+
current: &HeartbeatConfig,
10+
patch: &serde_json::Value,
11+
) -> Result<HeartbeatConfig, serde_json::Error> {
12+
let mut merged = serde_json::to_value(current)?;
13+
overlay_json(&mut merged, patch);
14+
serde_json::from_value(merged)
15+
}
16+
17+
/// Overlay `patch` onto `base`, recursing into objects so a partial nested
18+
/// object updates the keys it carries instead of replacing the whole thing.
19+
fn overlay_json(base: &mut serde_json::Value, patch: &serde_json::Value) {
20+
match (base, patch) {
21+
(serde_json::Value::Object(base), serde_json::Value::Object(patch)) => {
22+
for (key, value) in patch {
23+
overlay_json(
24+
base.entry(key.clone()).or_insert(serde_json::Value::Null),
25+
value,
26+
);
27+
}
28+
},
29+
(base, patch) => *base = patch.clone(),
30+
}
31+
}
32+
33+
#[cfg(test)]
34+
mod tests {
35+
use super::*;
36+
37+
#[test]
38+
fn update_leaves_fields_the_payload_omits_alone() -> anyhow::Result<()> {
39+
let current = HeartbeatConfig {
40+
wake_cooldown: "1h".into(),
41+
agent_id: Some("night-shift".into()),
42+
..Default::default()
43+
};
44+
45+
// The settings form has no input for these fields, so neither key is in
46+
// its payload.
47+
let saved = apply_heartbeat_patch(
48+
&current,
49+
&serde_json::json!({
50+
"enabled": true,
51+
"every": "15m",
52+
"ack_max_chars": 300,
53+
"deliver": false,
54+
"sandbox_enabled": true,
55+
"active_hours": {"start": "07:00", "end": "23:00", "timezone": "local"},
56+
}),
57+
)?;
58+
59+
assert_eq!(saved.every, "15m");
60+
assert_eq!(saved.wake_cooldown, "1h");
61+
assert_eq!(saved.agent_id.as_deref(), Some("night-shift"));
62+
Ok(())
63+
}
64+
65+
#[test]
66+
fn partial_active_hours_moves_only_the_keys_it_carries() -> anyhow::Result<()> {
67+
let current = HeartbeatConfig {
68+
active_hours: moltis_config::schema::ActiveHoursConfig {
69+
start: "09:00".into(),
70+
end: "21:00".into(),
71+
timezone: "UTC".into(),
72+
},
73+
..Default::default()
74+
};
75+
76+
let saved = apply_heartbeat_patch(
77+
&current,
78+
&serde_json::json!({"active_hours": {"start": "07:00"}}),
79+
)?;
80+
81+
assert_eq!(saved.active_hours.start, "07:00");
82+
assert_eq!(saved.active_hours.end, "21:00");
83+
assert_eq!(saved.active_hours.timezone, "UTC");
84+
Ok(())
85+
}
86+
87+
#[test]
88+
fn explicit_null_still_clears_an_optional_field() -> anyhow::Result<()> {
89+
let current = HeartbeatConfig {
90+
model: Some("anthropic/claude-sonnet-4".into()),
91+
..Default::default()
92+
};
93+
94+
let saved = apply_heartbeat_patch(&current, &serde_json::json!({"model": null}))?;
95+
96+
assert!(saved.model.is_none());
97+
Ok(())
98+
}
99+
}

0 commit comments

Comments
 (0)