handoff-mcp-server 0.7.0 → 0.7.2
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/package.json +2 -3
- package/scripts/postinstall.js +7 -1
- package/src/mcp/handlers/load_context.rs +32 -3
- package/src/mcp/handlers/save_context.rs +12 -2
- package/bin/handoff-mcp-bin +0 -0
- package/tests/mcp_protocol.rs +0 -208
- package/tests/storage_config.rs +0 -107
- package/tests/storage_sessions.rs +0 -535
- package/tests/storage_tasks.rs +0 -339
- package/tests/tool_dashboard.rs +0 -165
- package/tests/tool_import_context.rs +0 -443
- package/tests/tool_init.rs +0 -176
- package/tests/tool_referrals.rs +0 -604
- package/tests/tool_sessions.rs +0 -1194
- package/tests/tool_tasks.rs +0 -765
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
use serde_json::{json, Value};
|
|
2
|
-
use tempfile::TempDir;
|
|
3
|
-
|
|
4
|
-
fn send(input: &str) -> Option<Value> {
|
|
5
|
-
let result = handoff_mcp::mcp::protocol::process_line(input)?;
|
|
6
|
-
Some(serde_json::from_str(&result).expect("response should be valid JSON"))
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
fn setup_project() -> TempDir {
|
|
10
|
-
let dir = tempfile::tempdir().expect("failed to create temp dir");
|
|
11
|
-
|
|
12
|
-
std::process::Command::new("git")
|
|
13
|
-
.args(["init"])
|
|
14
|
-
.current_dir(dir.path())
|
|
15
|
-
.output()
|
|
16
|
-
.unwrap();
|
|
17
|
-
std::process::Command::new("git")
|
|
18
|
-
.args(["commit", "--allow-empty", "-m", "init"])
|
|
19
|
-
.current_dir(dir.path())
|
|
20
|
-
.output()
|
|
21
|
-
.unwrap();
|
|
22
|
-
|
|
23
|
-
let req = json!({
|
|
24
|
-
"jsonrpc": "2.0", "id": 0,
|
|
25
|
-
"method": "tools/call",
|
|
26
|
-
"params": {
|
|
27
|
-
"name": "handoff_init",
|
|
28
|
-
"arguments": {
|
|
29
|
-
"project_dir": dir.path().to_string_lossy(),
|
|
30
|
-
"project_name": "test"
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
send(&req.to_string()).unwrap();
|
|
35
|
-
dir
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
fn call_tool(name: &str, arguments: Value) -> Value {
|
|
39
|
-
let req = json!({
|
|
40
|
-
"jsonrpc": "2.0", "id": 1,
|
|
41
|
-
"method": "tools/call",
|
|
42
|
-
"params": { "name": name, "arguments": arguments }
|
|
43
|
-
});
|
|
44
|
-
send(&req.to_string()).unwrap()
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
fn get_text(resp: &Value) -> String {
|
|
48
|
-
resp["result"]["content"][0]["text"]
|
|
49
|
-
.as_str()
|
|
50
|
-
.unwrap_or("")
|
|
51
|
-
.to_string()
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
#[test]
|
|
55
|
-
fn import_source_only() {
|
|
56
|
-
let dir = setup_project();
|
|
57
|
-
let resp = call_tool(
|
|
58
|
-
"handoff_import_context",
|
|
59
|
-
json!({
|
|
60
|
-
"project_dir": dir.path().to_string_lossy(),
|
|
61
|
-
"source": {
|
|
62
|
-
"description": "test import",
|
|
63
|
-
"format": "markdown"
|
|
64
|
-
}
|
|
65
|
-
}),
|
|
66
|
-
);
|
|
67
|
-
let text = get_text(&resp);
|
|
68
|
-
assert!(text.contains("Import complete"));
|
|
69
|
-
assert!(text.contains("Tasks created: 0"));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
#[test]
|
|
73
|
-
fn import_flat_tasks() {
|
|
74
|
-
let dir = setup_project();
|
|
75
|
-
let resp = call_tool(
|
|
76
|
-
"handoff_import_context",
|
|
77
|
-
json!({
|
|
78
|
-
"project_dir": dir.path().to_string_lossy(),
|
|
79
|
-
"source": { "description": "flat task import" },
|
|
80
|
-
"tasks": [
|
|
81
|
-
{ "title": "Task A", "status": "todo" },
|
|
82
|
-
{ "title": "Task B", "status": "in_progress", "priority": "high" }
|
|
83
|
-
]
|
|
84
|
-
}),
|
|
85
|
-
);
|
|
86
|
-
let text = get_text(&resp);
|
|
87
|
-
assert!(text.contains("Tasks created: 2"));
|
|
88
|
-
assert!(text.contains("2 top-level"));
|
|
89
|
-
assert!(text.contains("0 nested"));
|
|
90
|
-
|
|
91
|
-
let list_resp = call_tool(
|
|
92
|
-
"handoff_list_tasks",
|
|
93
|
-
json!({ "project_dir": dir.path().to_string_lossy() }),
|
|
94
|
-
);
|
|
95
|
-
let list_text = get_text(&list_resp);
|
|
96
|
-
assert!(list_text.contains("Task A"));
|
|
97
|
-
assert!(list_text.contains("Task B"));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
#[test]
|
|
101
|
-
fn import_nested_tasks() {
|
|
102
|
-
let dir = setup_project();
|
|
103
|
-
let resp = call_tool(
|
|
104
|
-
"handoff_import_context",
|
|
105
|
-
json!({
|
|
106
|
-
"project_dir": dir.path().to_string_lossy(),
|
|
107
|
-
"source": { "description": "nested import" },
|
|
108
|
-
"tasks": [
|
|
109
|
-
{
|
|
110
|
-
"title": "Parent",
|
|
111
|
-
"status": "in_progress",
|
|
112
|
-
"children": [
|
|
113
|
-
{ "title": "Child 1", "status": "done" },
|
|
114
|
-
{
|
|
115
|
-
"title": "Child 2",
|
|
116
|
-
"status": "todo",
|
|
117
|
-
"children": [
|
|
118
|
-
{ "title": "Grandchild", "status": "todo" }
|
|
119
|
-
]
|
|
120
|
-
}
|
|
121
|
-
]
|
|
122
|
-
}
|
|
123
|
-
]
|
|
124
|
-
}),
|
|
125
|
-
);
|
|
126
|
-
let text = get_text(&resp);
|
|
127
|
-
assert!(text.contains("Tasks created: 4"));
|
|
128
|
-
assert!(text.contains("1 top-level"));
|
|
129
|
-
assert!(text.contains("3 nested"));
|
|
130
|
-
|
|
131
|
-
let list_resp = call_tool(
|
|
132
|
-
"handoff_list_tasks",
|
|
133
|
-
json!({ "project_dir": dir.path().to_string_lossy() }),
|
|
134
|
-
);
|
|
135
|
-
let list_text = get_text(&list_resp);
|
|
136
|
-
assert!(list_text.contains("Parent"));
|
|
137
|
-
assert!(list_text.contains("Child 1"));
|
|
138
|
-
assert!(list_text.contains("Child 2"));
|
|
139
|
-
assert!(list_text.contains("Grandchild"));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
#[test]
|
|
143
|
-
fn import_with_session() {
|
|
144
|
-
let dir = setup_project();
|
|
145
|
-
let resp = call_tool(
|
|
146
|
-
"handoff_import_context",
|
|
147
|
-
json!({
|
|
148
|
-
"project_dir": dir.path().to_string_lossy(),
|
|
149
|
-
"source": { "description": "session import" },
|
|
150
|
-
"session": {
|
|
151
|
-
"summary": "[import] test session",
|
|
152
|
-
"decisions": [
|
|
153
|
-
{ "decision": "Use OAuth2", "reason": "mobile support", "confidence": "confirmed" }
|
|
154
|
-
],
|
|
155
|
-
"blockers": ["waiting on API key"],
|
|
156
|
-
"handoff_notes": [
|
|
157
|
-
{ "note": "check auth flow", "category": "caution" }
|
|
158
|
-
]
|
|
159
|
-
}
|
|
160
|
-
}),
|
|
161
|
-
);
|
|
162
|
-
let text = get_text(&resp);
|
|
163
|
-
assert!(text.contains("Session saved: yes"));
|
|
164
|
-
|
|
165
|
-
let load_resp = call_tool(
|
|
166
|
-
"handoff_load_context",
|
|
167
|
-
json!({ "project_dir": dir.path().to_string_lossy() }),
|
|
168
|
-
);
|
|
169
|
-
let load_text = get_text(&load_resp);
|
|
170
|
-
assert!(load_text.contains("[import] test session"));
|
|
171
|
-
assert!(load_text.contains("Use OAuth2"));
|
|
172
|
-
assert!(load_text.contains("waiting on API key"));
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
#[test]
|
|
176
|
-
fn import_with_raw_notes() {
|
|
177
|
-
let dir = setup_project();
|
|
178
|
-
let resp = call_tool(
|
|
179
|
-
"handoff_import_context",
|
|
180
|
-
json!({
|
|
181
|
-
"project_dir": dir.path().to_string_lossy(),
|
|
182
|
-
"source": { "description": "raw notes import" },
|
|
183
|
-
"session": {
|
|
184
|
-
"summary": "[import] with raw notes"
|
|
185
|
-
},
|
|
186
|
-
"raw_notes": "Deploy on Fridays is forbidden\nSSL cert expires 7/1"
|
|
187
|
-
}),
|
|
188
|
-
);
|
|
189
|
-
let text = get_text(&resp);
|
|
190
|
-
assert!(text.contains("Raw notes: saved as handoff_note"));
|
|
191
|
-
|
|
192
|
-
let load_resp = call_tool(
|
|
193
|
-
"handoff_load_context",
|
|
194
|
-
json!({ "project_dir": dir.path().to_string_lossy() }),
|
|
195
|
-
);
|
|
196
|
-
let load_text = get_text(&load_resp);
|
|
197
|
-
assert!(load_text.contains("Deploy on Fridays is forbidden"));
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
#[test]
|
|
201
|
-
fn import_raw_notes_without_session_creates_auto_session() {
|
|
202
|
-
let dir = setup_project();
|
|
203
|
-
let resp = call_tool(
|
|
204
|
-
"handoff_import_context",
|
|
205
|
-
json!({
|
|
206
|
-
"project_dir": dir.path().to_string_lossy(),
|
|
207
|
-
"source": { "description": "auto session test" },
|
|
208
|
-
"raw_notes": "Some unstructured notes"
|
|
209
|
-
}),
|
|
210
|
-
);
|
|
211
|
-
let text = get_text(&resp);
|
|
212
|
-
assert!(text.contains("Session saved: yes"));
|
|
213
|
-
assert!(text.contains("Raw notes: saved as handoff_note"));
|
|
214
|
-
|
|
215
|
-
let load_resp = call_tool(
|
|
216
|
-
"handoff_load_context",
|
|
217
|
-
json!({ "project_dir": dir.path().to_string_lossy() }),
|
|
218
|
-
);
|
|
219
|
-
let load_text = get_text(&load_resp);
|
|
220
|
-
assert!(load_text.contains("[import] auto session test"));
|
|
221
|
-
assert!(load_text.contains("Some unstructured notes"));
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
#[test]
|
|
225
|
-
fn import_full_scenario() {
|
|
226
|
-
let dir = setup_project();
|
|
227
|
-
let resp = call_tool(
|
|
228
|
-
"handoff_import_context",
|
|
229
|
-
json!({
|
|
230
|
-
"project_dir": dir.path().to_string_lossy(),
|
|
231
|
-
"source": {
|
|
232
|
-
"description": "tmp/260601-sprint-handoff.md",
|
|
233
|
-
"format": "markdown"
|
|
234
|
-
},
|
|
235
|
-
"tasks": [
|
|
236
|
-
{
|
|
237
|
-
"title": "Auth renewal",
|
|
238
|
-
"status": "in_progress",
|
|
239
|
-
"priority": "high",
|
|
240
|
-
"labels": ["auth"],
|
|
241
|
-
"children": [
|
|
242
|
-
{ "title": "Session migration", "status": "done" },
|
|
243
|
-
{ "title": "PKCE implementation", "status": "in_progress" }
|
|
244
|
-
]
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
"title": "CI speedup",
|
|
248
|
-
"status": "todo",
|
|
249
|
-
"priority": "medium",
|
|
250
|
-
"done_criteria": [
|
|
251
|
-
{ "item": "Build time under 5min", "checked": false }
|
|
252
|
-
]
|
|
253
|
-
}
|
|
254
|
-
],
|
|
255
|
-
"session": {
|
|
256
|
-
"summary": "[import] Sprint handoff migration",
|
|
257
|
-
"decisions": [
|
|
258
|
-
{ "decision": "OAuth2 + PKCE", "confidence": "confirmed" }
|
|
259
|
-
],
|
|
260
|
-
"blockers": ["DB migration window TBD"],
|
|
261
|
-
"references": [
|
|
262
|
-
{ "label": "Original doc", "uri": "tmp/260601-sprint-handoff.md", "type": "doc" }
|
|
263
|
-
],
|
|
264
|
-
"context_pointers": [
|
|
265
|
-
{ "path": "src/auth/oauth.rs", "reason": "PKCE core" }
|
|
266
|
-
]
|
|
267
|
-
},
|
|
268
|
-
"raw_notes": "Deploy on Fridays is forbidden"
|
|
269
|
-
}),
|
|
270
|
-
);
|
|
271
|
-
let text = get_text(&resp);
|
|
272
|
-
assert!(text.contains("Tasks created: 4"));
|
|
273
|
-
assert!(text.contains("2 top-level"));
|
|
274
|
-
assert!(text.contains("2 nested"));
|
|
275
|
-
assert!(text.contains("Session saved: yes"));
|
|
276
|
-
assert!(text.contains("Raw notes: saved as handoff_note"));
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
#[test]
|
|
280
|
-
fn import_without_source_fails() {
|
|
281
|
-
let dir = setup_project();
|
|
282
|
-
let resp = call_tool(
|
|
283
|
-
"handoff_import_context",
|
|
284
|
-
json!({
|
|
285
|
-
"project_dir": dir.path().to_string_lossy(),
|
|
286
|
-
"tasks": [{ "title": "Something" }]
|
|
287
|
-
}),
|
|
288
|
-
);
|
|
289
|
-
let text = get_text(&resp);
|
|
290
|
-
assert!(text.contains("Error"));
|
|
291
|
-
assert!(text.contains("source"));
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
#[test]
|
|
295
|
-
fn import_task_without_title_fails() {
|
|
296
|
-
let dir = setup_project();
|
|
297
|
-
let resp = call_tool(
|
|
298
|
-
"handoff_import_context",
|
|
299
|
-
json!({
|
|
300
|
-
"project_dir": dir.path().to_string_lossy(),
|
|
301
|
-
"source": { "description": "bad task" },
|
|
302
|
-
"tasks": [{ "status": "todo" }]
|
|
303
|
-
}),
|
|
304
|
-
);
|
|
305
|
-
let text = get_text(&resp);
|
|
306
|
-
assert!(text.contains("Error"));
|
|
307
|
-
assert!(text.contains("title"));
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
#[test]
|
|
311
|
-
fn import_preserves_existing_tasks() {
|
|
312
|
-
let dir = setup_project();
|
|
313
|
-
|
|
314
|
-
call_tool(
|
|
315
|
-
"handoff_update_task",
|
|
316
|
-
json!({
|
|
317
|
-
"project_dir": dir.path().to_string_lossy(),
|
|
318
|
-
"task": { "title": "Pre-existing task", "status": "in_progress" }
|
|
319
|
-
}),
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
call_tool(
|
|
323
|
-
"handoff_import_context",
|
|
324
|
-
json!({
|
|
325
|
-
"project_dir": dir.path().to_string_lossy(),
|
|
326
|
-
"source": { "description": "import after existing" },
|
|
327
|
-
"tasks": [
|
|
328
|
-
{ "title": "Imported task", "status": "todo" }
|
|
329
|
-
]
|
|
330
|
-
}),
|
|
331
|
-
);
|
|
332
|
-
|
|
333
|
-
let list_resp = call_tool(
|
|
334
|
-
"handoff_list_tasks",
|
|
335
|
-
json!({ "project_dir": dir.path().to_string_lossy() }),
|
|
336
|
-
);
|
|
337
|
-
let list_text = get_text(&list_resp);
|
|
338
|
-
assert!(list_text.contains("Pre-existing task"));
|
|
339
|
-
assert!(list_text.contains("Imported task"));
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
#[test]
|
|
343
|
-
fn import_source_recorded_in_environment() {
|
|
344
|
-
let dir = setup_project();
|
|
345
|
-
call_tool(
|
|
346
|
-
"handoff_import_context",
|
|
347
|
-
json!({
|
|
348
|
-
"project_dir": dir.path().to_string_lossy(),
|
|
349
|
-
"source": {
|
|
350
|
-
"description": "my-handoff.md",
|
|
351
|
-
"format": "markdown"
|
|
352
|
-
},
|
|
353
|
-
"session": {
|
|
354
|
-
"summary": "[import] env test"
|
|
355
|
-
}
|
|
356
|
-
}),
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
let sessions_dir = dir.path().join(".handoff/sessions");
|
|
360
|
-
let entries: Vec<_> = std::fs::read_dir(&sessions_dir)
|
|
361
|
-
.unwrap()
|
|
362
|
-
.filter_map(|e| e.ok())
|
|
363
|
-
.filter(|e| e.file_name().to_string_lossy().ends_with(".open.json"))
|
|
364
|
-
.collect();
|
|
365
|
-
assert_eq!(entries.len(), 1);
|
|
366
|
-
|
|
367
|
-
let content = std::fs::read_to_string(entries[0].path()).unwrap();
|
|
368
|
-
let session: serde_json::Value = serde_json::from_str(&content).unwrap();
|
|
369
|
-
assert_eq!(
|
|
370
|
-
session["environment"]["import_source"]["description"],
|
|
371
|
-
"my-handoff.md"
|
|
372
|
-
);
|
|
373
|
-
assert_eq!(
|
|
374
|
-
session["environment"]["import_source"]["format"],
|
|
375
|
-
"markdown"
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
fn is_error(resp: &Value) -> bool {
|
|
380
|
-
resp["result"]["isError"].as_bool().unwrap_or(false)
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
#[test]
|
|
384
|
-
fn import_invalid_priority_rejected() {
|
|
385
|
-
let dir = setup_project();
|
|
386
|
-
let resp = call_tool(
|
|
387
|
-
"handoff_import_context",
|
|
388
|
-
json!({
|
|
389
|
-
"project_dir": dir.path().to_string_lossy(),
|
|
390
|
-
"source": { "description": "bad priority import" },
|
|
391
|
-
"tasks": [
|
|
392
|
-
{ "title": "Task", "priority": "urgent" }
|
|
393
|
-
]
|
|
394
|
-
}),
|
|
395
|
-
);
|
|
396
|
-
assert!(is_error(&resp));
|
|
397
|
-
let text = get_text(&resp);
|
|
398
|
-
assert!(text.contains("Invalid priority"));
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
#[test]
|
|
402
|
-
fn import_valid_priorities_accepted() {
|
|
403
|
-
let dir = setup_project();
|
|
404
|
-
let resp = call_tool(
|
|
405
|
-
"handoff_import_context",
|
|
406
|
-
json!({
|
|
407
|
-
"project_dir": dir.path().to_string_lossy(),
|
|
408
|
-
"source": { "description": "valid priorities" },
|
|
409
|
-
"tasks": [
|
|
410
|
-
{ "title": "Low", "priority": "low" },
|
|
411
|
-
{ "title": "Medium", "priority": "medium" },
|
|
412
|
-
{ "title": "High", "priority": "high" }
|
|
413
|
-
]
|
|
414
|
-
}),
|
|
415
|
-
);
|
|
416
|
-
assert!(!is_error(&resp), "error: {}", get_text(&resp));
|
|
417
|
-
let text = get_text(&resp);
|
|
418
|
-
assert!(text.contains("Tasks created: 3"));
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
#[test]
|
|
422
|
-
fn import_invalid_priority_in_child_rejected() {
|
|
423
|
-
let dir = setup_project();
|
|
424
|
-
let resp = call_tool(
|
|
425
|
-
"handoff_import_context",
|
|
426
|
-
json!({
|
|
427
|
-
"project_dir": dir.path().to_string_lossy(),
|
|
428
|
-
"source": { "description": "bad child priority" },
|
|
429
|
-
"tasks": [
|
|
430
|
-
{
|
|
431
|
-
"title": "Parent",
|
|
432
|
-
"priority": "high",
|
|
433
|
-
"children": [
|
|
434
|
-
{ "title": "Child", "priority": "ASAP" }
|
|
435
|
-
]
|
|
436
|
-
}
|
|
437
|
-
]
|
|
438
|
-
}),
|
|
439
|
-
);
|
|
440
|
-
assert!(is_error(&resp));
|
|
441
|
-
let text = get_text(&resp);
|
|
442
|
-
assert!(text.contains("Invalid priority"));
|
|
443
|
-
}
|
package/tests/tool_init.rs
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
use serde_json::{json, Value};
|
|
2
|
-
use tempfile::TempDir;
|
|
3
|
-
|
|
4
|
-
fn send(input: &str) -> Option<Value> {
|
|
5
|
-
let result = handoff_mcp::mcp::protocol::process_line(input)?;
|
|
6
|
-
Some(serde_json::from_str(&result).expect("response should be valid JSON"))
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
fn setup() -> TempDir {
|
|
10
|
-
tempfile::tempdir().expect("failed to create temp dir")
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
#[test]
|
|
14
|
-
fn init_creates_handoff_directory() {
|
|
15
|
-
let dir = setup();
|
|
16
|
-
let project_dir = dir.path().to_string_lossy();
|
|
17
|
-
|
|
18
|
-
let req = json!({
|
|
19
|
-
"jsonrpc": "2.0",
|
|
20
|
-
"id": 1,
|
|
21
|
-
"method": "tools/call",
|
|
22
|
-
"params": {
|
|
23
|
-
"name": "handoff_init",
|
|
24
|
-
"arguments": {
|
|
25
|
-
"project_dir": project_dir,
|
|
26
|
-
"project_name": "test-project",
|
|
27
|
-
"description": "A test project"
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
let resp = send(&req.to_string()).expect("should return response");
|
|
33
|
-
assert_eq!(resp["id"], 1);
|
|
34
|
-
assert!(resp["error"].is_null(), "error: {:?}", resp["error"]);
|
|
35
|
-
|
|
36
|
-
let content = &resp["result"]["content"][0]["text"];
|
|
37
|
-
assert!(
|
|
38
|
-
content.as_str().unwrap().contains("test-project"),
|
|
39
|
-
"response: {content}"
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
let handoff = dir.path().join(".handoff");
|
|
43
|
-
assert!(handoff.exists());
|
|
44
|
-
assert!(handoff.join("config.toml").exists());
|
|
45
|
-
assert!(handoff.join("sessions").is_dir());
|
|
46
|
-
assert!(handoff.join("tasks").is_dir());
|
|
47
|
-
|
|
48
|
-
let config_content = std::fs::read_to_string(handoff.join("config.toml")).unwrap();
|
|
49
|
-
assert!(config_content.contains("test-project"));
|
|
50
|
-
assert!(config_content.contains("A test project"));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
#[test]
|
|
54
|
-
fn init_double_init_returns_error() {
|
|
55
|
-
let dir = setup();
|
|
56
|
-
let project_dir = dir.path().to_string_lossy();
|
|
57
|
-
|
|
58
|
-
let req = json!({
|
|
59
|
-
"jsonrpc": "2.0",
|
|
60
|
-
"id": 1,
|
|
61
|
-
"method": "tools/call",
|
|
62
|
-
"params": {
|
|
63
|
-
"name": "handoff_init",
|
|
64
|
-
"arguments": {
|
|
65
|
-
"project_dir": project_dir,
|
|
66
|
-
"project_name": "test-project"
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
send(&req.to_string()).unwrap();
|
|
72
|
-
|
|
73
|
-
let req2 = json!({
|
|
74
|
-
"jsonrpc": "2.0",
|
|
75
|
-
"id": 2,
|
|
76
|
-
"method": "tools/call",
|
|
77
|
-
"params": {
|
|
78
|
-
"name": "handoff_init",
|
|
79
|
-
"arguments": {
|
|
80
|
-
"project_dir": project_dir,
|
|
81
|
-
"project_name": "test-project"
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
let resp = send(&req2.to_string()).unwrap();
|
|
87
|
-
let content = &resp["result"]["content"][0]["text"];
|
|
88
|
-
assert!(content.as_str().unwrap().contains("already"));
|
|
89
|
-
assert_eq!(resp["result"]["isError"], true);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
#[test]
|
|
93
|
-
fn init_without_project_name_returns_error() {
|
|
94
|
-
let dir = setup();
|
|
95
|
-
let project_dir = dir.path().to_string_lossy();
|
|
96
|
-
|
|
97
|
-
let req = json!({
|
|
98
|
-
"jsonrpc": "2.0",
|
|
99
|
-
"id": 1,
|
|
100
|
-
"method": "tools/call",
|
|
101
|
-
"params": {
|
|
102
|
-
"name": "handoff_init",
|
|
103
|
-
"arguments": {
|
|
104
|
-
"project_dir": project_dir
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
let resp = send(&req.to_string()).unwrap();
|
|
110
|
-
assert_eq!(resp["result"]["isError"], true);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
#[test]
|
|
114
|
-
fn init_config_has_defaults() {
|
|
115
|
-
let dir = setup();
|
|
116
|
-
let project_dir = dir.path().to_string_lossy();
|
|
117
|
-
|
|
118
|
-
let req = json!({
|
|
119
|
-
"jsonrpc": "2.0",
|
|
120
|
-
"id": 1,
|
|
121
|
-
"method": "tools/call",
|
|
122
|
-
"params": {
|
|
123
|
-
"name": "handoff_init",
|
|
124
|
-
"arguments": {
|
|
125
|
-
"project_dir": project_dir,
|
|
126
|
-
"project_name": "defaults-test"
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
send(&req.to_string()).unwrap();
|
|
132
|
-
|
|
133
|
-
let config =
|
|
134
|
-
handoff_mcp::storage::config::read_config(&dir.path().join(".handoff/config.toml"))
|
|
135
|
-
.unwrap();
|
|
136
|
-
|
|
137
|
-
assert_eq!(config.project.name, "defaults-test");
|
|
138
|
-
assert_eq!(config.settings.history_limit, 20);
|
|
139
|
-
assert_eq!(config.settings.done_task_limit, 10);
|
|
140
|
-
assert!(config.settings.auto_git_summary);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
#[test]
|
|
144
|
-
fn tools_call_unknown_tool_returns_error() {
|
|
145
|
-
let req = json!({
|
|
146
|
-
"jsonrpc": "2.0",
|
|
147
|
-
"id": 1,
|
|
148
|
-
"method": "tools/call",
|
|
149
|
-
"params": {
|
|
150
|
-
"name": "nonexistent_tool",
|
|
151
|
-
"arguments": {}
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
let resp = send(&req.to_string()).unwrap();
|
|
156
|
-
assert_eq!(resp["result"]["isError"], true);
|
|
157
|
-
assert!(resp["result"]["content"][0]["text"]
|
|
158
|
-
.as_str()
|
|
159
|
-
.unwrap()
|
|
160
|
-
.contains("not implemented"));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
#[test]
|
|
164
|
-
fn tools_call_missing_name_returns_error() {
|
|
165
|
-
let req = json!({
|
|
166
|
-
"jsonrpc": "2.0",
|
|
167
|
-
"id": 1,
|
|
168
|
-
"method": "tools/call",
|
|
169
|
-
"params": {
|
|
170
|
-
"arguments": {}
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
let resp = send(&req.to_string()).unwrap();
|
|
175
|
-
assert!(resp["error"].is_object());
|
|
176
|
-
}
|