1use crate::protocol::{StatePatch, StatePatchFormat};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use thiserror::Error;
6
7#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
9#[serde(default)]
10pub struct WellKnownState {
11 pub cwd: Option<String>,
13 pub open_files: Option<Vec<String>>,
15 pub session_meta: Option<SessionMeta>,
17 pub budget: Option<BudgetState>,
19 pub active_skills: Option<Vec<String>>,
21 pub mcp_servers: Option<Vec<McpServerInfo>>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
27pub struct SessionMeta {
28 pub session_name: Option<String>,
29 pub user_id: Option<String>,
30 pub created_at: Option<String>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
35pub struct BudgetState {
36 pub total_tokens_used: u64,
37 pub max_tokens_budget: Option<u64>,
38 pub tool_calls_count: u32,
39 pub max_tool_calls: Option<u32>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
44pub struct McpServerInfo {
45 pub name: String,
46 pub transport: String,
48 pub tool_count: usize,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
52pub struct AppState {
53 pub revision: u64,
54 #[serde(default)]
55 pub data: Value,
56}
57
58impl Default for AppState {
59 fn default() -> Self {
60 Self {
61 revision: 0,
62 data: Value::Object(Default::default()),
63 }
64 }
65}
66
67impl AppState {
68 pub fn new(data: Value) -> Self {
69 Self { revision: 0, data }
70 }
71
72 pub fn well_known(&self) -> Result<WellKnownState, serde_json::Error> {
74 serde_json::from_value(self.data.clone())
75 }
76
77 pub fn cwd(&self) -> Option<String> {
79 self.data
80 .get("cwd")
81 .and_then(|v| v.as_str())
82 .map(String::from)
83 }
84
85 pub fn open_files(&self) -> Vec<String> {
87 self.data
88 .get("open_files")
89 .and_then(|v| serde_json::from_value(v.clone()).ok())
90 .unwrap_or_default()
91 }
92
93 pub fn apply_patch(&mut self, patch: &StatePatch) -> Result<(), StateError> {
94 match patch.format {
95 StatePatchFormat::JsonPatch => {
96 let parsed_patch: json_patch::Patch = serde_json::from_value(patch.patch.clone())
97 .map_err(StateError::InvalidJsonPatch)?;
98 json_patch::patch(&mut self.data, &parsed_patch)
99 .map_err(|e| StateError::PatchApply(e.to_string()))?;
100 }
101 StatePatchFormat::MergePatch => {
102 json_patch::merge(&mut self.data, &patch.patch);
103 }
104 }
105
106 self.revision = self.revision.saturating_add(1);
107 Ok(())
108 }
109}
110
111#[derive(Debug, Error)]
112pub enum StateError {
113 #[error("invalid JSON patch payload: {0}")]
114 InvalidJsonPatch(serde_json::Error),
115 #[error("failed to apply patch: {0}")]
116 PatchApply(String),
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::protocol::StatePatchSource;
123 use serde_json::json;
124
125 #[test]
126 fn default_state_is_empty_object() {
127 let state = AppState::default();
128 assert_eq!(state.revision, 0);
129 assert_eq!(state.data, json!({}));
130 }
131
132 #[test]
133 fn merge_patch_adds_fields() {
134 let mut state = AppState::default();
135 let patch = StatePatch {
136 format: StatePatchFormat::MergePatch,
137 patch: json!({"name": "arcan", "version": 1}),
138 source: StatePatchSource::System,
139 };
140 state.apply_patch(&patch).unwrap();
141
142 assert_eq!(state.revision, 1);
143 assert_eq!(state.data["name"], "arcan");
144 assert_eq!(state.data["version"], 1);
145 }
146
147 #[test]
148 fn merge_patch_overwrites_fields() {
149 let mut state = AppState::new(json!({"count": 0}));
150 let patch = StatePatch {
151 format: StatePatchFormat::MergePatch,
152 patch: json!({"count": 42}),
153 source: StatePatchSource::Tool,
154 };
155 state.apply_patch(&patch).unwrap();
156 assert_eq!(state.data["count"], 42);
157 }
158
159 #[test]
160 fn merge_patch_removes_null_fields() {
161 let mut state = AppState::new(json!({"a": 1, "b": 2}));
162 let patch = StatePatch {
163 format: StatePatchFormat::MergePatch,
164 patch: json!({"b": null}),
165 source: StatePatchSource::Model,
166 };
167 state.apply_patch(&patch).unwrap();
168 assert_eq!(state.data, json!({"a": 1}));
169 }
170
171 #[test]
172 fn json_patch_add_operation() {
173 let mut state = AppState::default();
174 let patch = StatePatch {
175 format: StatePatchFormat::JsonPatch,
176 patch: json!([{"op": "add", "path": "/foo", "value": "bar"}]),
177 source: StatePatchSource::System,
178 };
179 state.apply_patch(&patch).unwrap();
180 assert_eq!(state.data["foo"], "bar");
181 assert_eq!(state.revision, 1);
182 }
183
184 #[test]
185 fn json_patch_replace_operation() {
186 let mut state = AppState::new(json!({"x": 10}));
187 let patch = StatePatch {
188 format: StatePatchFormat::JsonPatch,
189 patch: json!([{"op": "replace", "path": "/x", "value": 20}]),
190 source: StatePatchSource::System,
191 };
192 state.apply_patch(&patch).unwrap();
193 assert_eq!(state.data["x"], 20);
194 }
195
196 #[test]
197 fn json_patch_invalid_payload_errors() {
198 let mut state = AppState::default();
199 let patch = StatePatch {
200 format: StatePatchFormat::JsonPatch,
201 patch: json!("not an array"),
202 source: StatePatchSource::System,
203 };
204 assert!(state.apply_patch(&patch).is_err());
205 assert_eq!(state.revision, 0);
206 }
207
208 #[test]
209 fn well_known_parses_populated_state() {
210 let state = AppState::new(json!({
211 "cwd": "/home/user",
212 "open_files": ["main.rs", "lib.rs"],
213 "budget": {
214 "total_tokens_used": 1000,
215 "max_tokens_budget": 10000,
216 "tool_calls_count": 5,
217 "max_tool_calls": 100
218 }
219 }));
220 let wk = state.well_known().unwrap();
221 assert_eq!(wk.cwd.as_deref(), Some("/home/user"));
222 assert_eq!(
223 wk.open_files,
224 Some(vec!["main.rs".to_string(), "lib.rs".to_string()])
225 );
226 assert!(wk.budget.is_some());
227 assert_eq!(wk.budget.unwrap().total_tokens_used, 1000);
228 }
229
230 #[test]
231 fn well_known_defaults_for_empty_state() {
232 let state = AppState::default();
233 let wk = state.well_known().unwrap();
234 assert_eq!(wk.cwd, None);
235 assert_eq!(wk.open_files, None);
236 assert_eq!(wk.session_meta, None);
237 assert_eq!(wk.budget, None);
238 assert_eq!(wk.active_skills, None);
239 assert_eq!(wk.mcp_servers, None);
240 }
241
242 #[test]
243 fn cwd_accessor() {
244 let state = AppState::new(json!({"cwd": "/tmp"}));
245 assert_eq!(state.cwd(), Some("/tmp".to_string()));
246
247 let empty = AppState::default();
248 assert_eq!(empty.cwd(), None);
249 }
250
251 #[test]
252 fn open_files_accessor() {
253 let state = AppState::new(json!({"open_files": ["a.rs", "b.rs"]}));
254 assert_eq!(
255 state.open_files(),
256 vec!["a.rs".to_string(), "b.rs".to_string()]
257 );
258
259 let empty = AppState::default();
260 assert!(empty.open_files().is_empty());
261 }
262
263 #[test]
264 fn json_patch_works_with_well_known_keys() {
265 let mut state = AppState::new(json!({"cwd": "/old"}));
266 let patch = StatePatch {
267 format: StatePatchFormat::JsonPatch,
268 patch: json!([{"op": "replace", "path": "/cwd", "value": "/new"}]),
269 source: StatePatchSource::System,
270 };
271 state.apply_patch(&patch).unwrap();
272 assert_eq!(state.cwd(), Some("/new".to_string()));
273 }
274
275 #[test]
276 fn revision_increments_with_each_patch() {
277 let mut state = AppState::default();
278 for i in 1..=5 {
279 let patch = StatePatch {
280 format: StatePatchFormat::MergePatch,
281 patch: json!({"step": i}),
282 source: StatePatchSource::System,
283 };
284 state.apply_patch(&patch).unwrap();
285 assert_eq!(state.revision, i as u64);
286 }
287 }
288}