handoff-mcp-server 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/bin/handoff-mcp-bin +0 -0
- package/bin/handoff-mcp.js +0 -0
- package/package.json +1 -1
- package/src/mcp/handlers/dashboard.rs +5 -1
- package/src/mcp/handlers/load_context.rs +59 -5
- package/src/mcp/handlers/save_context.rs +40 -2
- package/src/mcp/resources.rs +2 -1
- package/src/mcp/tools.rs +11 -3
- package/src/storage/sessions.rs +28 -2
- package/tests/storage_sessions.rs +176 -0
- package/tests/tool_sessions.rs +359 -0
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/bin/handoff-mcp-bin
CHANGED
|
Binary file
|
package/bin/handoff-mcp.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "handoff-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "MCP server that gives AI coding agents persistent memory across sessions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AlphaElements <66808803+alphaelements@users.noreply.github.com>",
|
|
@@ -6,7 +6,7 @@ use serde_json::Value;
|
|
|
6
6
|
use crate::storage::config::read_config;
|
|
7
7
|
use crate::storage::expand_tilde;
|
|
8
8
|
use crate::storage::referrals::read_referral_summaries;
|
|
9
|
-
use crate::storage::sessions::{read_active_sessions, read_open_sessions};
|
|
9
|
+
use crate::storage::sessions::{read_active_sessions, read_open_sessions, read_paused_sessions};
|
|
10
10
|
use crate::storage::tasks::build_task_index;
|
|
11
11
|
|
|
12
12
|
pub fn handle(arguments: &Value) -> Result<String> {
|
|
@@ -73,6 +73,9 @@ fn collect_project_info(project_path: &Path) -> Result<Value> {
|
|
|
73
73
|
let sessions_dir = handoff_dir.join("sessions");
|
|
74
74
|
let mut sessions = read_open_sessions(&sessions_dir)?;
|
|
75
75
|
sessions.extend(read_active_sessions(&sessions_dir)?);
|
|
76
|
+
let paused = read_paused_sessions(&sessions_dir)?;
|
|
77
|
+
let paused_count = paused.len() as u32;
|
|
78
|
+
sessions.extend(paused);
|
|
76
79
|
|
|
77
80
|
let (_, summary) =
|
|
78
81
|
build_task_index(&handoff_dir.join("tasks"), config.settings.done_task_limit)?;
|
|
@@ -105,5 +108,6 @@ fn collect_project_info(project_path: &Path) -> Result<Value> {
|
|
|
105
108
|
"blocked_tasks": blocked_tasks,
|
|
106
109
|
"blockers": blockers,
|
|
107
110
|
"unread_referrals": unread_referrals,
|
|
111
|
+
"paused_sessions": paused_count,
|
|
108
112
|
}))
|
|
109
113
|
}
|
|
@@ -5,7 +5,8 @@ use super::resolve_project_dir;
|
|
|
5
5
|
use crate::storage::config::read_config;
|
|
6
6
|
use crate::storage::referrals::read_referral_summaries;
|
|
7
7
|
use crate::storage::sessions::{
|
|
8
|
-
activate_open_sessions, activate_session_by_id, read_open_sessions,
|
|
8
|
+
activate_open_sessions, activate_session_by_id, read_active_sessions, read_open_sessions,
|
|
9
|
+
read_paused_sessions, resume_paused_session_by_id,
|
|
9
10
|
};
|
|
10
11
|
use crate::storage::tasks::build_task_index;
|
|
11
12
|
use crate::storage::{ensure_handoff_exists, handoff_dir};
|
|
@@ -38,13 +39,50 @@ pub fn handle(arguments: &Value) -> Result<String> {
|
|
|
38
39
|
|
|
39
40
|
let target_session_id = arguments.get("session_id").and_then(|v| v.as_str());
|
|
40
41
|
|
|
42
|
+
let active_sessions = read_active_sessions(&sessions_dir)?;
|
|
41
43
|
let sessions = read_open_sessions(&sessions_dir)?;
|
|
44
|
+
let paused_sessions = read_paused_sessions(&sessions_dir)?;
|
|
42
45
|
|
|
43
46
|
let selected_session = if let Some(sid) = target_session_id {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.
|
|
47
|
-
|
|
47
|
+
let already_active = active_sessions
|
|
48
|
+
.iter()
|
|
49
|
+
.any(|s| s.id.as_deref().is_some_and(|id| id == sid));
|
|
50
|
+
if already_active {
|
|
51
|
+
active_sessions
|
|
52
|
+
.into_iter()
|
|
53
|
+
.find(|s| s.id.as_deref().is_some_and(|id| id == sid))
|
|
54
|
+
} else if !active_sessions.is_empty() {
|
|
55
|
+
let active_ids: Vec<String> = active_sessions
|
|
56
|
+
.iter()
|
|
57
|
+
.filter_map(|s| s.id.clone())
|
|
58
|
+
.collect();
|
|
59
|
+
anyhow::bail!(
|
|
60
|
+
"Cannot activate session '{sid}': another session is already active ({}).\n\
|
|
61
|
+
Use save_context with close_session_id or pause_session_id to \
|
|
62
|
+
close/pause the active session first.",
|
|
63
|
+
active_ids.join(", ")
|
|
64
|
+
);
|
|
65
|
+
} else if activate_session_by_id(&sessions_dir, sid)?.is_some() {
|
|
66
|
+
sessions
|
|
67
|
+
.into_iter()
|
|
68
|
+
.find(|s| s.id.as_deref().is_some_and(|id| id == sid))
|
|
69
|
+
} else if resume_paused_session_by_id(&sessions_dir, sid)?.is_some() {
|
|
70
|
+
paused_sessions
|
|
71
|
+
.into_iter()
|
|
72
|
+
.find(|s| s.id.as_deref().is_some_and(|id| id == sid))
|
|
73
|
+
} else {
|
|
74
|
+
None
|
|
75
|
+
}
|
|
76
|
+
} else if !active_sessions.is_empty() {
|
|
77
|
+
active_sessions.into_iter().last()
|
|
78
|
+
} else if sessions.len() > 1 {
|
|
79
|
+
let open_ids: Vec<String> = sessions.iter().filter_map(|s| s.id.clone()).collect();
|
|
80
|
+
anyhow::bail!(
|
|
81
|
+
"Multiple open sessions found ({}).\n\
|
|
82
|
+
Specify session_id to choose one, or use save_context to \
|
|
83
|
+
close/pause the others first.",
|
|
84
|
+
open_ids.join(", ")
|
|
85
|
+
);
|
|
48
86
|
} else {
|
|
49
87
|
activate_open_sessions(&sessions_dir)?;
|
|
50
88
|
sessions.into_iter().last()
|
|
@@ -126,5 +164,21 @@ pub fn handle(arguments: &Value) -> Result<String> {
|
|
|
126
164
|
result["referrals"] = serde_json::to_value(&open_referrals)?;
|
|
127
165
|
}
|
|
128
166
|
|
|
167
|
+
let current_paused = read_paused_sessions(&sessions_dir)?;
|
|
168
|
+
if !current_paused.is_empty() {
|
|
169
|
+
let summaries: Vec<Value> = current_paused
|
|
170
|
+
.iter()
|
|
171
|
+
.map(|s| {
|
|
172
|
+
serde_json::json!({
|
|
173
|
+
"id": s.id,
|
|
174
|
+
"summary": s.summary,
|
|
175
|
+
"ended_at": s.ended_at,
|
|
176
|
+
"branch": s.branch,
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
.collect();
|
|
180
|
+
result["paused_sessions"] = serde_json::json!(summaries);
|
|
181
|
+
}
|
|
182
|
+
|
|
129
183
|
serde_json::to_string_pretty(&result).context("Failed to serialize context")
|
|
130
184
|
}
|
|
@@ -10,7 +10,8 @@ use crate::storage::ensure_handoff_exists;
|
|
|
10
10
|
use crate::storage::git::capture_git_state;
|
|
11
11
|
use crate::storage::sessions::{
|
|
12
12
|
close_active_sessions, close_open_sessions, close_session_by_id, enforce_history_limit,
|
|
13
|
-
generate_session_id,
|
|
13
|
+
generate_session_id, pause_active_sessions, pause_session_by_id, read_active_sessions,
|
|
14
|
+
write_open_session, SessionData,
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
pub fn handle(arguments: &Value) -> Result<String> {
|
|
@@ -26,6 +27,20 @@ pub fn handle(arguments: &Value) -> Result<String> {
|
|
|
26
27
|
.ok_or_else(|| anyhow::anyhow!("'summary' is required"))?;
|
|
27
28
|
|
|
28
29
|
let close_id = arguments.get("close_session_id").and_then(|v| v.as_str());
|
|
30
|
+
let pause_id = arguments.get("pause_session_id").and_then(|v| v.as_str());
|
|
31
|
+
let pause_all = arguments
|
|
32
|
+
.get("pause_active")
|
|
33
|
+
.and_then(|v| v.as_bool())
|
|
34
|
+
.unwrap_or(false);
|
|
35
|
+
|
|
36
|
+
let mut total_paused = 0usize;
|
|
37
|
+
if let Some(id) = pause_id {
|
|
38
|
+
if pause_session_by_id(&sessions_dir, id)?.is_some() {
|
|
39
|
+
total_paused = 1;
|
|
40
|
+
}
|
|
41
|
+
} else if pause_all {
|
|
42
|
+
total_paused = pause_active_sessions(&sessions_dir)?.len();
|
|
43
|
+
}
|
|
29
44
|
|
|
30
45
|
let total_closed = if let Some(id) = close_id {
|
|
31
46
|
let closed = close_session_by_id(&sessions_dir, id)?;
|
|
@@ -34,7 +49,20 @@ pub fn handle(arguments: &Value) -> Result<String> {
|
|
|
34
49
|
} else {
|
|
35
50
|
0
|
|
36
51
|
}
|
|
52
|
+
} else if pause_id.is_some() || pause_all {
|
|
53
|
+
let closed_open = close_open_sessions(&sessions_dir)?;
|
|
54
|
+
closed_open.len()
|
|
37
55
|
} else {
|
|
56
|
+
let active = read_active_sessions(&sessions_dir)?;
|
|
57
|
+
if active.len() > 1 {
|
|
58
|
+
let active_ids: Vec<String> = active.iter().filter_map(|s| s.id.clone()).collect();
|
|
59
|
+
anyhow::bail!(
|
|
60
|
+
"Multiple active sessions found ({}).\n\
|
|
61
|
+
Use close_session_id or pause_session_id to specify which to \
|
|
62
|
+
close/pause before saving a new session.",
|
|
63
|
+
active_ids.join(", ")
|
|
64
|
+
);
|
|
65
|
+
}
|
|
38
66
|
let closed_active = close_active_sessions(&sessions_dir)?;
|
|
39
67
|
let closed_open = close_open_sessions(&sessions_dir)?;
|
|
40
68
|
closed_active.len() + closed_open.len()
|
|
@@ -81,13 +109,23 @@ pub fn handle(arguments: &Value) -> Result<String> {
|
|
|
81
109
|
.unwrap_or_default()
|
|
82
110
|
);
|
|
83
111
|
|
|
112
|
+
if total_paused > 0 {
|
|
113
|
+
msg.push_str(&format!("\nPaused {} session(s)", total_paused));
|
|
114
|
+
}
|
|
115
|
+
if let Some(id) = pause_id {
|
|
116
|
+
if total_paused == 0 {
|
|
117
|
+
msg.push_str(&format!(
|
|
118
|
+
"\nWarning: pause_session_id '{id}' not found among active sessions"
|
|
119
|
+
));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
84
122
|
if total_closed > 0 {
|
|
85
123
|
msg.push_str(&format!("\nClosed {} previous session(s)", total_closed));
|
|
86
124
|
}
|
|
87
125
|
if let Some(id) = close_id {
|
|
88
126
|
if total_closed == 0 {
|
|
89
127
|
msg.push_str(&format!(
|
|
90
|
-
"\nWarning: close_session_id '{id}' not found among active/open sessions"
|
|
128
|
+
"\nWarning: close_session_id '{id}' not found among active/open/paused sessions"
|
|
91
129
|
));
|
|
92
130
|
}
|
|
93
131
|
}
|
package/src/mcp/resources.rs
CHANGED
|
@@ -2,7 +2,7 @@ use anyhow::{Context, Result};
|
|
|
2
2
|
use serde_json::{json, Value};
|
|
3
3
|
|
|
4
4
|
use crate::storage::config::read_config;
|
|
5
|
-
use crate::storage::sessions::{read_active_sessions, read_open_sessions};
|
|
5
|
+
use crate::storage::sessions::{read_active_sessions, read_open_sessions, read_paused_sessions};
|
|
6
6
|
use crate::storage::{ensure_handoff_exists, handoff_dir};
|
|
7
7
|
|
|
8
8
|
pub fn handle_resource_read(uri: &str) -> Result<Value> {
|
|
@@ -26,6 +26,7 @@ fn read_sessions_resource(handoff: &std::path::Path) -> Result<Value> {
|
|
|
26
26
|
let sessions_dir = handoff.join("sessions");
|
|
27
27
|
let mut sessions = read_open_sessions(&sessions_dir)?;
|
|
28
28
|
sessions.extend(read_active_sessions(&sessions_dir)?);
|
|
29
|
+
sessions.extend(read_paused_sessions(&sessions_dir)?);
|
|
29
30
|
|
|
30
31
|
let contents: Vec<Value> = sessions
|
|
31
32
|
.iter()
|
package/src/mcp/tools.rs
CHANGED
|
@@ -28,7 +28,7 @@ pub fn all_tool_definitions() -> Vec<ToolDefinition> {
|
|
|
28
28
|
},
|
|
29
29
|
ToolDefinition {
|
|
30
30
|
name: "handoff_load_context".to_string(),
|
|
31
|
-
description: "Load handoff context for the current project. Call at session start to resume work.".to_string(),
|
|
31
|
+
description: "Load handoff context for the current project. Call at session start to resume work. Can also resume a paused session by ID.".to_string(),
|
|
32
32
|
input_schema: json!({
|
|
33
33
|
"type": "object",
|
|
34
34
|
"properties": {
|
|
@@ -38,7 +38,7 @@ pub fn all_tool_definitions() -> Vec<ToolDefinition> {
|
|
|
38
38
|
},
|
|
39
39
|
"session_id": {
|
|
40
40
|
"type": "string",
|
|
41
|
-
"description": "Session ID to activate and load. If omitted, activates all open sessions and returns the latest."
|
|
41
|
+
"description": "Session ID to activate and load. Searches open sessions first, then paused sessions. If omitted, activates all open sessions and returns the latest."
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
}),
|
|
@@ -59,7 +59,15 @@ pub fn all_tool_definitions() -> Vec<ToolDefinition> {
|
|
|
59
59
|
},
|
|
60
60
|
"close_session_id": {
|
|
61
61
|
"type": "string",
|
|
62
|
-
"description": "Session ID to close. If omitted, all active and open sessions are closed
|
|
62
|
+
"description": "Session ID to close. If omitted (and no pause options set), all active and open sessions are closed."
|
|
63
|
+
},
|
|
64
|
+
"pause_session_id": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "Session ID to pause instead of close. The paused session can be resumed later via load_context with the same session_id. Use this when switching to different work temporarily."
|
|
67
|
+
},
|
|
68
|
+
"pause_active": {
|
|
69
|
+
"type": "boolean",
|
|
70
|
+
"description": "If true, pause all active sessions instead of closing them. Cannot be combined with close_session_id."
|
|
63
71
|
},
|
|
64
72
|
"decisions": {
|
|
65
73
|
"type": "array",
|
package/src/storage/sessions.rs
CHANGED
|
@@ -258,11 +258,37 @@ pub fn close_open_sessions(sessions_dir: &Path) -> Result<Vec<PathBuf>> {
|
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
pub fn close_session_by_id(sessions_dir: &Path, session_id: &str) -> Result<Option<PathBuf>> {
|
|
261
|
-
// Try active first, then open
|
|
261
|
+
// Try active first, then open, then paused
|
|
262
262
|
if let Some(path) = transition_session_by_id(sessions_dir, session_id, "active", "closed")? {
|
|
263
263
|
return Ok(Some(path));
|
|
264
264
|
}
|
|
265
|
-
transition_session_by_id(sessions_dir, session_id, "open", "closed")
|
|
265
|
+
if let Some(path) = transition_session_by_id(sessions_dir, session_id, "open", "closed")? {
|
|
266
|
+
return Ok(Some(path));
|
|
267
|
+
}
|
|
268
|
+
transition_session_by_id(sessions_dir, session_id, "paused", "closed")
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
pub fn pause_active_sessions(sessions_dir: &Path) -> Result<Vec<PathBuf>> {
|
|
272
|
+
transition_sessions(sessions_dir, "active", "paused")
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
pub fn pause_session_by_id(sessions_dir: &Path, session_id: &str) -> Result<Option<PathBuf>> {
|
|
276
|
+
transition_session_by_id(sessions_dir, session_id, "active", "paused")
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
pub fn resume_paused_session_by_id(
|
|
280
|
+
sessions_dir: &Path,
|
|
281
|
+
session_id: &str,
|
|
282
|
+
) -> Result<Option<PathBuf>> {
|
|
283
|
+
transition_session_by_id(sessions_dir, session_id, "paused", "active")
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
pub fn read_paused_sessions(sessions_dir: &Path) -> Result<Vec<SessionData>> {
|
|
287
|
+
read_sessions_by_status(sessions_dir, "paused")
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
pub fn close_paused_sessions(sessions_dir: &Path) -> Result<Vec<PathBuf>> {
|
|
291
|
+
transition_sessions(sessions_dir, "paused", "closed")
|
|
266
292
|
}
|
|
267
293
|
|
|
268
294
|
pub fn enforce_history_limit(sessions_dir: &Path, limit: u32) -> Result<u32> {
|
|
@@ -357,3 +357,179 @@ fn close_session_by_id_nonexistent_returns_none() {
|
|
|
357
357
|
let result = close_session_by_id(&sessions_dir, "s-nonexistent").unwrap();
|
|
358
358
|
assert!(result.is_none());
|
|
359
359
|
}
|
|
360
|
+
|
|
361
|
+
#[test]
|
|
362
|
+
fn pause_active_sessions_renames_to_paused() {
|
|
363
|
+
let dir = setup();
|
|
364
|
+
let sessions_dir = dir.path().join("sessions");
|
|
365
|
+
fs::create_dir_all(&sessions_dir).unwrap();
|
|
366
|
+
|
|
367
|
+
let s = make_session("working on feature", "2026-06-15T10:00:00Z");
|
|
368
|
+
write_open_session(&sessions_dir, &s).unwrap();
|
|
369
|
+
activate_open_sessions(&sessions_dir).unwrap();
|
|
370
|
+
|
|
371
|
+
let paused = pause_active_sessions(&sessions_dir).unwrap();
|
|
372
|
+
assert_eq!(paused.len(), 1);
|
|
373
|
+
assert!(paused[0].to_string_lossy().contains(".paused.json"));
|
|
374
|
+
assert!(paused[0].exists());
|
|
375
|
+
|
|
376
|
+
assert!(read_active_sessions(&sessions_dir).unwrap().is_empty());
|
|
377
|
+
let paused_sessions = read_paused_sessions(&sessions_dir).unwrap();
|
|
378
|
+
assert_eq!(paused_sessions.len(), 1);
|
|
379
|
+
assert_eq!(paused_sessions[0].summary, "working on feature");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#[test]
|
|
383
|
+
fn pause_session_by_id_pauses_only_targeted() {
|
|
384
|
+
let dir = setup();
|
|
385
|
+
let sessions_dir = dir.path().join("sessions");
|
|
386
|
+
fs::create_dir_all(&sessions_dir).unwrap();
|
|
387
|
+
|
|
388
|
+
let s1 = make_session("session one", "2026-06-15T10:00:00Z");
|
|
389
|
+
let s2 = make_session("session two", "2026-06-15T11:00:00Z");
|
|
390
|
+
write_open_session(&sessions_dir, &s1).unwrap();
|
|
391
|
+
write_open_session(&sessions_dir, &s2).unwrap();
|
|
392
|
+
activate_open_sessions(&sessions_dir).unwrap();
|
|
393
|
+
|
|
394
|
+
let active = read_active_sessions(&sessions_dir).unwrap();
|
|
395
|
+
assert_eq!(active.len(), 2);
|
|
396
|
+
let target_id = active[0].id.as_ref().unwrap().clone();
|
|
397
|
+
|
|
398
|
+
let result = pause_session_by_id(&sessions_dir, &target_id).unwrap();
|
|
399
|
+
assert!(result.is_some());
|
|
400
|
+
|
|
401
|
+
let remaining_active = read_active_sessions(&sessions_dir).unwrap();
|
|
402
|
+
assert_eq!(remaining_active.len(), 1);
|
|
403
|
+
|
|
404
|
+
let paused = read_paused_sessions(&sessions_dir).unwrap();
|
|
405
|
+
assert_eq!(paused.len(), 1);
|
|
406
|
+
assert_eq!(paused[0].id.as_deref().unwrap(), target_id);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
#[test]
|
|
410
|
+
fn resume_paused_session_by_id_reactivates() {
|
|
411
|
+
let dir = setup();
|
|
412
|
+
let sessions_dir = dir.path().join("sessions");
|
|
413
|
+
fs::create_dir_all(&sessions_dir).unwrap();
|
|
414
|
+
|
|
415
|
+
let s = make_session("paused work", "2026-06-15T10:00:00Z");
|
|
416
|
+
write_open_session(&sessions_dir, &s).unwrap();
|
|
417
|
+
activate_open_sessions(&sessions_dir).unwrap();
|
|
418
|
+
pause_active_sessions(&sessions_dir).unwrap();
|
|
419
|
+
|
|
420
|
+
let paused = read_paused_sessions(&sessions_dir).unwrap();
|
|
421
|
+
assert_eq!(paused.len(), 1);
|
|
422
|
+
let sid = paused[0].id.as_ref().unwrap().clone();
|
|
423
|
+
|
|
424
|
+
let result = resume_paused_session_by_id(&sessions_dir, &sid).unwrap();
|
|
425
|
+
assert!(result.is_some());
|
|
426
|
+
assert!(result.unwrap().to_string_lossy().contains(".active.json"));
|
|
427
|
+
|
|
428
|
+
assert!(read_paused_sessions(&sessions_dir).unwrap().is_empty());
|
|
429
|
+
let active = read_active_sessions(&sessions_dir).unwrap();
|
|
430
|
+
assert_eq!(active.len(), 1);
|
|
431
|
+
assert_eq!(active[0].summary, "paused work");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
#[test]
|
|
435
|
+
fn close_session_by_id_closes_paused() {
|
|
436
|
+
let dir = setup();
|
|
437
|
+
let sessions_dir = dir.path().join("sessions");
|
|
438
|
+
fs::create_dir_all(&sessions_dir).unwrap();
|
|
439
|
+
|
|
440
|
+
let s = make_session("will close from paused", "2026-06-15T10:00:00Z");
|
|
441
|
+
write_open_session(&sessions_dir, &s).unwrap();
|
|
442
|
+
activate_open_sessions(&sessions_dir).unwrap();
|
|
443
|
+
pause_active_sessions(&sessions_dir).unwrap();
|
|
444
|
+
|
|
445
|
+
let paused = read_paused_sessions(&sessions_dir).unwrap();
|
|
446
|
+
let sid = paused[0].id.as_ref().unwrap().clone();
|
|
447
|
+
|
|
448
|
+
let result = close_session_by_id(&sessions_dir, &sid).unwrap();
|
|
449
|
+
assert!(result.is_some());
|
|
450
|
+
assert!(result.unwrap().to_string_lossy().contains(".closed.json"));
|
|
451
|
+
|
|
452
|
+
assert!(read_paused_sessions(&sessions_dir).unwrap().is_empty());
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
#[test]
|
|
456
|
+
fn close_paused_sessions_closes_all() {
|
|
457
|
+
let dir = setup();
|
|
458
|
+
let sessions_dir = dir.path().join("sessions");
|
|
459
|
+
fs::create_dir_all(&sessions_dir).unwrap();
|
|
460
|
+
|
|
461
|
+
let s1 = make_session("paused one", "2026-06-15T10:00:00Z");
|
|
462
|
+
let s2 = make_session("paused two", "2026-06-15T11:00:00Z");
|
|
463
|
+
write_open_session(&sessions_dir, &s1).unwrap();
|
|
464
|
+
write_open_session(&sessions_dir, &s2).unwrap();
|
|
465
|
+
activate_open_sessions(&sessions_dir).unwrap();
|
|
466
|
+
pause_active_sessions(&sessions_dir).unwrap();
|
|
467
|
+
|
|
468
|
+
assert_eq!(read_paused_sessions(&sessions_dir).unwrap().len(), 2);
|
|
469
|
+
|
|
470
|
+
let closed = close_paused_sessions(&sessions_dir).unwrap();
|
|
471
|
+
assert_eq!(closed.len(), 2);
|
|
472
|
+
assert!(read_paused_sessions(&sessions_dir).unwrap().is_empty());
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
#[test]
|
|
476
|
+
fn full_lifecycle_with_pause_and_resume() {
|
|
477
|
+
let dir = setup();
|
|
478
|
+
let sessions_dir = dir.path().join("sessions");
|
|
479
|
+
fs::create_dir_all(&sessions_dir).unwrap();
|
|
480
|
+
|
|
481
|
+
let s1 = make_session("feature work", "2026-06-15T10:00:00Z");
|
|
482
|
+
write_open_session(&sessions_dir, &s1).unwrap();
|
|
483
|
+
activate_open_sessions(&sessions_dir).unwrap();
|
|
484
|
+
|
|
485
|
+
let active = read_active_sessions(&sessions_dir).unwrap();
|
|
486
|
+
assert_eq!(active.len(), 1);
|
|
487
|
+
let s1_id = active[0].id.as_ref().unwrap().clone();
|
|
488
|
+
|
|
489
|
+
pause_session_by_id(&sessions_dir, &s1_id).unwrap();
|
|
490
|
+
assert!(read_active_sessions(&sessions_dir).unwrap().is_empty());
|
|
491
|
+
assert_eq!(read_paused_sessions(&sessions_dir).unwrap().len(), 1);
|
|
492
|
+
|
|
493
|
+
let s2 = make_session("urgent fix", "2026-06-15T12:00:00Z");
|
|
494
|
+
write_open_session(&sessions_dir, &s2).unwrap();
|
|
495
|
+
activate_open_sessions(&sessions_dir).unwrap();
|
|
496
|
+
|
|
497
|
+
let active2 = read_active_sessions(&sessions_dir).unwrap();
|
|
498
|
+
assert_eq!(active2.len(), 1);
|
|
499
|
+
assert_eq!(active2[0].summary, "urgent fix");
|
|
500
|
+
assert_eq!(read_paused_sessions(&sessions_dir).unwrap().len(), 1);
|
|
501
|
+
|
|
502
|
+
close_active_sessions(&sessions_dir).unwrap();
|
|
503
|
+
|
|
504
|
+
resume_paused_session_by_id(&sessions_dir, &s1_id).unwrap();
|
|
505
|
+
let active3 = read_active_sessions(&sessions_dir).unwrap();
|
|
506
|
+
assert_eq!(active3.len(), 1);
|
|
507
|
+
assert_eq!(active3[0].summary, "feature work");
|
|
508
|
+
assert!(read_paused_sessions(&sessions_dir).unwrap().is_empty());
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
#[test]
|
|
512
|
+
fn enforce_history_limit_ignores_paused() {
|
|
513
|
+
let dir = setup();
|
|
514
|
+
let sessions_dir = dir.path().join("sessions");
|
|
515
|
+
fs::create_dir_all(&sessions_dir).unwrap();
|
|
516
|
+
|
|
517
|
+
for i in 1..=3 {
|
|
518
|
+
let name = format!("20260610-{i:06}-s{i}.closed.json");
|
|
519
|
+
fs::write(
|
|
520
|
+
sessions_dir.join(&name),
|
|
521
|
+
format!(r#"{{"version":2,"summary":"closed {i}"}}"#),
|
|
522
|
+
)
|
|
523
|
+
.unwrap();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
let s = make_session("paused session", "2026-06-15T10:00:00Z");
|
|
527
|
+
write_open_session(&sessions_dir, &s).unwrap();
|
|
528
|
+
activate_open_sessions(&sessions_dir).unwrap();
|
|
529
|
+
pause_active_sessions(&sessions_dir).unwrap();
|
|
530
|
+
|
|
531
|
+
enforce_history_limit(&sessions_dir, 2).unwrap();
|
|
532
|
+
|
|
533
|
+
let paused = read_paused_sessions(&sessions_dir).unwrap();
|
|
534
|
+
assert_eq!(paused.len(), 1, "paused sessions should not be removed");
|
|
535
|
+
}
|
package/tests/tool_sessions.rs
CHANGED
|
@@ -949,3 +949,362 @@ fn save_context_warns_on_unknown_close_session_id() {
|
|
|
949
949
|
"should warn about unknown close_session_id: {text}"
|
|
950
950
|
);
|
|
951
951
|
}
|
|
952
|
+
|
|
953
|
+
// --- pause/resume session tests ---
|
|
954
|
+
|
|
955
|
+
#[test]
|
|
956
|
+
fn save_context_with_pause_session_id() {
|
|
957
|
+
let dir = setup_project();
|
|
958
|
+
let pd = dir.path().to_string_lossy().to_string();
|
|
959
|
+
|
|
960
|
+
call_tool(
|
|
961
|
+
"handoff_save_context",
|
|
962
|
+
json!({ "project_dir": &pd, "summary": "session one" }),
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
let resp = call_tool("handoff_load_context", json!({ "project_dir": &pd }));
|
|
966
|
+
let text = get_text(&resp);
|
|
967
|
+
let parsed: Value = serde_json::from_str(&text).unwrap();
|
|
968
|
+
let session_id = parsed["session_id"].as_str().unwrap().to_string();
|
|
969
|
+
|
|
970
|
+
let resp = call_tool(
|
|
971
|
+
"handoff_save_context",
|
|
972
|
+
json!({
|
|
973
|
+
"project_dir": &pd,
|
|
974
|
+
"summary": "session two (switching work)",
|
|
975
|
+
"pause_session_id": session_id
|
|
976
|
+
}),
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
let text = get_text(&resp);
|
|
980
|
+
assert!(!is_error(&resp), "error: {text}");
|
|
981
|
+
assert!(
|
|
982
|
+
text.contains("Paused 1 session(s)"),
|
|
983
|
+
"should report paused: {text}"
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
let sessions_dir = dir.path().join(".handoff/sessions");
|
|
987
|
+
let paused: Vec<_> = std::fs::read_dir(&sessions_dir)
|
|
988
|
+
.unwrap()
|
|
989
|
+
.filter_map(|e| e.ok())
|
|
990
|
+
.filter(|e| e.file_name().to_string_lossy().ends_with(".paused.json"))
|
|
991
|
+
.collect();
|
|
992
|
+
assert_eq!(paused.len(), 1, "should have 1 paused session");
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
#[test]
|
|
996
|
+
fn save_context_with_pause_active() {
|
|
997
|
+
let dir = setup_project();
|
|
998
|
+
let pd = dir.path().to_string_lossy().to_string();
|
|
999
|
+
|
|
1000
|
+
call_tool(
|
|
1001
|
+
"handoff_save_context",
|
|
1002
|
+
json!({ "project_dir": &pd, "summary": "session one" }),
|
|
1003
|
+
);
|
|
1004
|
+
call_tool("handoff_load_context", json!({ "project_dir": &pd }));
|
|
1005
|
+
|
|
1006
|
+
let resp = call_tool(
|
|
1007
|
+
"handoff_save_context",
|
|
1008
|
+
json!({
|
|
1009
|
+
"project_dir": &pd,
|
|
1010
|
+
"summary": "session two",
|
|
1011
|
+
"pause_active": true
|
|
1012
|
+
}),
|
|
1013
|
+
);
|
|
1014
|
+
|
|
1015
|
+
let text = get_text(&resp);
|
|
1016
|
+
assert!(!is_error(&resp), "error: {text}");
|
|
1017
|
+
assert!(
|
|
1018
|
+
text.contains("Paused 1 session(s)"),
|
|
1019
|
+
"should report paused: {text}"
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
#[test]
|
|
1024
|
+
fn load_context_resumes_paused_session() {
|
|
1025
|
+
let dir = setup_project();
|
|
1026
|
+
let pd = dir.path().to_string_lossy().to_string();
|
|
1027
|
+
|
|
1028
|
+
call_tool(
|
|
1029
|
+
"handoff_save_context",
|
|
1030
|
+
json!({
|
|
1031
|
+
"project_dir": &pd,
|
|
1032
|
+
"summary": "original work",
|
|
1033
|
+
"handoff_notes": [{ "note": "Continue feature X", "category": "suggestion" }]
|
|
1034
|
+
}),
|
|
1035
|
+
);
|
|
1036
|
+
|
|
1037
|
+
let resp = call_tool("handoff_load_context", json!({ "project_dir": &pd }));
|
|
1038
|
+
let text = get_text(&resp);
|
|
1039
|
+
let parsed: Value = serde_json::from_str(&text).unwrap();
|
|
1040
|
+
let original_sid = parsed["session_id"].as_str().unwrap().to_string();
|
|
1041
|
+
|
|
1042
|
+
call_tool(
|
|
1043
|
+
"handoff_save_context",
|
|
1044
|
+
json!({
|
|
1045
|
+
"project_dir": &pd,
|
|
1046
|
+
"summary": "switching to urgent work",
|
|
1047
|
+
"pause_session_id": &original_sid
|
|
1048
|
+
}),
|
|
1049
|
+
);
|
|
1050
|
+
|
|
1051
|
+
let resp = call_tool(
|
|
1052
|
+
"handoff_load_context",
|
|
1053
|
+
json!({ "project_dir": &pd, "session_id": &original_sid }),
|
|
1054
|
+
);
|
|
1055
|
+
let text = get_text(&resp);
|
|
1056
|
+
let parsed: Value = serde_json::from_str(&text).unwrap();
|
|
1057
|
+
|
|
1058
|
+
assert_eq!(
|
|
1059
|
+
parsed["session_id"].as_str().unwrap(),
|
|
1060
|
+
original_sid,
|
|
1061
|
+
"should resume the paused session"
|
|
1062
|
+
);
|
|
1063
|
+
assert!(
|
|
1064
|
+
parsed["last_session"]["summary"]
|
|
1065
|
+
.as_str()
|
|
1066
|
+
.unwrap()
|
|
1067
|
+
.contains("original work"),
|
|
1068
|
+
"should load the original session data"
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
#[test]
|
|
1073
|
+
fn load_context_shows_paused_sessions() {
|
|
1074
|
+
let dir = setup_project();
|
|
1075
|
+
let pd = dir.path().to_string_lossy().to_string();
|
|
1076
|
+
|
|
1077
|
+
call_tool(
|
|
1078
|
+
"handoff_save_context",
|
|
1079
|
+
json!({ "project_dir": &pd, "summary": "work A" }),
|
|
1080
|
+
);
|
|
1081
|
+
call_tool("handoff_load_context", json!({ "project_dir": &pd }));
|
|
1082
|
+
|
|
1083
|
+
call_tool(
|
|
1084
|
+
"handoff_save_context",
|
|
1085
|
+
json!({
|
|
1086
|
+
"project_dir": &pd,
|
|
1087
|
+
"summary": "work B",
|
|
1088
|
+
"pause_active": true
|
|
1089
|
+
}),
|
|
1090
|
+
);
|
|
1091
|
+
|
|
1092
|
+
let resp = call_tool("handoff_load_context", json!({ "project_dir": &pd }));
|
|
1093
|
+
let text = get_text(&resp);
|
|
1094
|
+
let parsed: Value = serde_json::from_str(&text).unwrap();
|
|
1095
|
+
|
|
1096
|
+
let paused = parsed["paused_sessions"]
|
|
1097
|
+
.as_array()
|
|
1098
|
+
.expect("should have paused_sessions");
|
|
1099
|
+
assert_eq!(paused.len(), 1);
|
|
1100
|
+
assert_eq!(paused[0]["summary"], "work A");
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
#[test]
|
|
1104
|
+
fn save_context_pause_unknown_id_warns() {
|
|
1105
|
+
let dir = setup_project();
|
|
1106
|
+
let pd = dir.path().to_string_lossy().to_string();
|
|
1107
|
+
|
|
1108
|
+
let resp = call_tool(
|
|
1109
|
+
"handoff_save_context",
|
|
1110
|
+
json!({
|
|
1111
|
+
"project_dir": &pd,
|
|
1112
|
+
"summary": "new session",
|
|
1113
|
+
"pause_session_id": "s-99999999-999999-999999"
|
|
1114
|
+
}),
|
|
1115
|
+
);
|
|
1116
|
+
|
|
1117
|
+
let text = get_text(&resp);
|
|
1118
|
+
assert!(
|
|
1119
|
+
text.contains("not found"),
|
|
1120
|
+
"should warn about unknown pause_session_id: {text}"
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
#[test]
|
|
1125
|
+
fn full_pause_resume_lifecycle() {
|
|
1126
|
+
let dir = setup_project();
|
|
1127
|
+
let pd = dir.path().to_string_lossy().to_string();
|
|
1128
|
+
|
|
1129
|
+
call_tool(
|
|
1130
|
+
"handoff_save_context",
|
|
1131
|
+
json!({
|
|
1132
|
+
"project_dir": &pd,
|
|
1133
|
+
"summary": "feature work"
|
|
1134
|
+
}),
|
|
1135
|
+
);
|
|
1136
|
+
let resp = call_tool("handoff_load_context", json!({ "project_dir": &pd }));
|
|
1137
|
+
let text = get_text(&resp);
|
|
1138
|
+
let parsed: Value = serde_json::from_str(&text).unwrap();
|
|
1139
|
+
let feature_sid = parsed["session_id"].as_str().unwrap().to_string();
|
|
1140
|
+
|
|
1141
|
+
call_tool(
|
|
1142
|
+
"handoff_save_context",
|
|
1143
|
+
json!({
|
|
1144
|
+
"project_dir": &pd,
|
|
1145
|
+
"summary": "urgent fix",
|
|
1146
|
+
"pause_session_id": &feature_sid
|
|
1147
|
+
}),
|
|
1148
|
+
);
|
|
1149
|
+
|
|
1150
|
+
let resp2 = call_tool("handoff_load_context", json!({ "project_dir": &pd }));
|
|
1151
|
+
let text2 = get_text(&resp2);
|
|
1152
|
+
let parsed2: Value = serde_json::from_str(&text2).unwrap();
|
|
1153
|
+
assert!(
|
|
1154
|
+
parsed2["last_session"]["summary"]
|
|
1155
|
+
.as_str()
|
|
1156
|
+
.unwrap()
|
|
1157
|
+
.contains("urgent fix"),
|
|
1158
|
+
"should load the new session"
|
|
1159
|
+
);
|
|
1160
|
+
assert!(
|
|
1161
|
+
parsed2["paused_sessions"].as_array().unwrap().len() == 1,
|
|
1162
|
+
"should show 1 paused session"
|
|
1163
|
+
);
|
|
1164
|
+
|
|
1165
|
+
let urgent_sid = parsed2["session_id"].as_str().unwrap().to_string();
|
|
1166
|
+
call_tool(
|
|
1167
|
+
"handoff_save_context",
|
|
1168
|
+
json!({
|
|
1169
|
+
"project_dir": &pd,
|
|
1170
|
+
"summary": "urgent fix done",
|
|
1171
|
+
"close_session_id": &urgent_sid
|
|
1172
|
+
}),
|
|
1173
|
+
);
|
|
1174
|
+
|
|
1175
|
+
let resp3 = call_tool(
|
|
1176
|
+
"handoff_load_context",
|
|
1177
|
+
json!({ "project_dir": &pd, "session_id": &feature_sid }),
|
|
1178
|
+
);
|
|
1179
|
+
let text3 = get_text(&resp3);
|
|
1180
|
+
let parsed3: Value = serde_json::from_str(&text3).unwrap();
|
|
1181
|
+
assert_eq!(
|
|
1182
|
+
parsed3["session_id"].as_str().unwrap(),
|
|
1183
|
+
feature_sid,
|
|
1184
|
+
"should resume the paused feature session"
|
|
1185
|
+
);
|
|
1186
|
+
assert!(
|
|
1187
|
+
parsed3
|
|
1188
|
+
.get("paused_sessions")
|
|
1189
|
+
.and_then(|v| v.as_array())
|
|
1190
|
+
.is_none()
|
|
1191
|
+
|| parsed3["paused_sessions"].as_array().unwrap().is_empty(),
|
|
1192
|
+
"no more paused sessions after resume"
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// --- active session uniqueness tests ---
|
|
1197
|
+
|
|
1198
|
+
#[test]
|
|
1199
|
+
fn load_context_rejects_activate_when_another_session_active() {
|
|
1200
|
+
let dir = setup_project();
|
|
1201
|
+
let pd = dir.path().to_string_lossy().to_string();
|
|
1202
|
+
|
|
1203
|
+
// Create session A and activate it
|
|
1204
|
+
call_tool(
|
|
1205
|
+
"handoff_save_context",
|
|
1206
|
+
json!({ "project_dir": &pd, "summary": "session A" }),
|
|
1207
|
+
);
|
|
1208
|
+
let resp = call_tool("handoff_load_context", json!({ "project_dir": &pd }));
|
|
1209
|
+
let text = get_text(&resp);
|
|
1210
|
+
let parsed: Value = serde_json::from_str(&text).unwrap();
|
|
1211
|
+
let sid_a = parsed["session_id"].as_str().unwrap().to_string();
|
|
1212
|
+
|
|
1213
|
+
// Create session B (without closing A — use close_session_id to close only A's open)
|
|
1214
|
+
// Actually: save_context default closes active + open, so A gets closed.
|
|
1215
|
+
// We need to simulate the scenario: write a second open session manually
|
|
1216
|
+
// while A is still active. We'll use pause to keep A alive.
|
|
1217
|
+
call_tool(
|
|
1218
|
+
"handoff_save_context",
|
|
1219
|
+
json!({
|
|
1220
|
+
"project_dir": &pd,
|
|
1221
|
+
"summary": "session B",
|
|
1222
|
+
"pause_session_id": &sid_a
|
|
1223
|
+
}),
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
// Now: A is paused, B is open. Load B (activates it).
|
|
1227
|
+
let resp = call_tool("handoff_load_context", json!({ "project_dir": &pd }));
|
|
1228
|
+
assert!(!is_error(&resp));
|
|
1229
|
+
let text = get_text(&resp);
|
|
1230
|
+
let parsed: Value = serde_json::from_str(&text).unwrap();
|
|
1231
|
+
let sid_b = parsed["session_id"].as_str().unwrap().to_string();
|
|
1232
|
+
|
|
1233
|
+
// Try to resume A while B is active — should fail
|
|
1234
|
+
let resp = call_tool(
|
|
1235
|
+
"handoff_load_context",
|
|
1236
|
+
json!({ "project_dir": &pd, "session_id": &sid_a }),
|
|
1237
|
+
);
|
|
1238
|
+
assert!(
|
|
1239
|
+
is_error(&resp),
|
|
1240
|
+
"should reject activating A while B is active: {}",
|
|
1241
|
+
get_text(&resp)
|
|
1242
|
+
);
|
|
1243
|
+
let text = get_text(&resp);
|
|
1244
|
+
assert!(
|
|
1245
|
+
text.contains("already active"),
|
|
1246
|
+
"error should mention active session: {text}"
|
|
1247
|
+
);
|
|
1248
|
+
assert!(
|
|
1249
|
+
text.contains(&sid_b),
|
|
1250
|
+
"error should mention the blocking session ID: {text}"
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
#[test]
|
|
1255
|
+
fn load_context_allows_reloading_already_active_session() {
|
|
1256
|
+
let dir = setup_project();
|
|
1257
|
+
let pd = dir.path().to_string_lossy().to_string();
|
|
1258
|
+
|
|
1259
|
+
call_tool(
|
|
1260
|
+
"handoff_save_context",
|
|
1261
|
+
json!({ "project_dir": &pd, "summary": "session A" }),
|
|
1262
|
+
);
|
|
1263
|
+
|
|
1264
|
+
let resp = call_tool("handoff_load_context", json!({ "project_dir": &pd }));
|
|
1265
|
+
let text = get_text(&resp);
|
|
1266
|
+
let parsed: Value = serde_json::from_str(&text).unwrap();
|
|
1267
|
+
let sid = parsed["session_id"].as_str().unwrap().to_string();
|
|
1268
|
+
|
|
1269
|
+
// Loading the same session again should succeed (idempotent)
|
|
1270
|
+
let resp2 = call_tool(
|
|
1271
|
+
"handoff_load_context",
|
|
1272
|
+
json!({ "project_dir": &pd, "session_id": &sid }),
|
|
1273
|
+
);
|
|
1274
|
+
assert!(
|
|
1275
|
+
!is_error(&resp2),
|
|
1276
|
+
"reloading the same active session should succeed: {}",
|
|
1277
|
+
get_text(&resp2)
|
|
1278
|
+
);
|
|
1279
|
+
let text2 = get_text(&resp2);
|
|
1280
|
+
let parsed2: Value = serde_json::from_str(&text2).unwrap();
|
|
1281
|
+
assert_eq!(parsed2["session_id"].as_str().unwrap(), sid);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
#[test]
|
|
1285
|
+
fn load_context_returns_active_session_without_open() {
|
|
1286
|
+
let dir = setup_project();
|
|
1287
|
+
let pd = dir.path().to_string_lossy().to_string();
|
|
1288
|
+
|
|
1289
|
+
call_tool(
|
|
1290
|
+
"handoff_save_context",
|
|
1291
|
+
json!({ "project_dir": &pd, "summary": "my session" }),
|
|
1292
|
+
);
|
|
1293
|
+
|
|
1294
|
+
// First load activates the open session
|
|
1295
|
+
let resp = call_tool("handoff_load_context", json!({ "project_dir": &pd }));
|
|
1296
|
+
let text = get_text(&resp);
|
|
1297
|
+
let parsed: Value = serde_json::from_str(&text).unwrap();
|
|
1298
|
+
let sid = parsed["session_id"].as_str().unwrap().to_string();
|
|
1299
|
+
|
|
1300
|
+
// Second load (no session_id) should return the active session
|
|
1301
|
+
let resp2 = call_tool("handoff_load_context", json!({ "project_dir": &pd }));
|
|
1302
|
+
assert!(!is_error(&resp2), "error: {}", get_text(&resp2));
|
|
1303
|
+
let text2 = get_text(&resp2);
|
|
1304
|
+
let parsed2: Value = serde_json::from_str(&text2).unwrap();
|
|
1305
|
+
assert_eq!(
|
|
1306
|
+
parsed2["session_id"].as_str().unwrap(),
|
|
1307
|
+
sid,
|
|
1308
|
+
"should return the same active session"
|
|
1309
|
+
);
|
|
1310
|
+
}
|