feat(mcp): agents can add, tick, rename and remove subtasks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-02 10:40:17 +01:00
1 parent 0f17cc3c33
commit e60e825b3c
4 files changed
+157 -3

No files matched your search

+57
View File
@@ -289,6 +289,63 @@ func TestMCPCreateDeleteAndOrigin(t *testing.T) {
}
}
// Agents manage a task's subtasks one at a time; the rest of the checklist stays as it is.
func TestMCPSubtasks(t *testing.T) {
srv, store, token := setupTestServer(t)
router := srv.Router()
claims, _ := auth.ValidateToken(testJWTSecret, token)
key, _ := store.CreateAPIKey(claims.UserID, "Claude Code", []string{"tasks:read", "tasks:write"})
call := func(id int, name, args string) (map[string]interface{}, bool) {
t.Helper()
_, res := rpc(t, router, key.RawKey, fmt.Sprintf(`{"jsonrpc":"2.0","id":%d,"method":"tools/call","params":{"name":%q,"arguments":%s}}`, id, name, args))
result, isErr := callResult(t, res)
if isErr {
return result, true
}
return result["structuredContent"].(map[string]interface{}), false
}
task, _ := srv.createTask(claims.UserID, "Umzug", "", "manual", "", "", "", nil)
call(1, "add_subtask", `{"taskId":"`+task.ID+`","title":"Kartons kaufen"}`)
res, isErr := call(2, "add_subtask", `{"taskId":"`+task.ID+`","title":" Strom ummelden "}`)
if isErr {
t.Fatalf("add_subtask: %v", res)
}
subs := res["subtasks"].([]interface{})
if len(subs) != 2 || subs[1].(map[string]interface{})["title"] != "Strom ummelden" {
t.Fatalf("subtasks after adding: %v", subs)
}
first := subs[0].(map[string]interface{})["id"].(string)
second := subs[1].(map[string]interface{})["id"].(string)
call(3, "update_subtask", `{"taskId":"`+task.ID+`","subtaskId":"`+first+`","completed":true}`)
call(4, "update_subtask", `{"taskId":"`+task.ID+`","subtaskId":"`+second+`","title":"Strom und Gas ummelden"}`)
th, _ := store.GetThoughtByID(claims.UserID, task.ID)
if len(th.Subtasks) != 2 || !th.Subtasks[0].IsCompleted || th.Subtasks[1].IsCompleted || th.Subtasks[1].Title != "Strom und Gas ummelden" {
t.Fatalf("subtasks after updating: %+v", th.Subtasks)
}
if th.Origin == nil || th.Origin.ChangedVia != "mcp" {
t.Fatalf("subtask change should count as agent change: %+v", th.Origin)
}
call(5, "update_subtask", `{"taskId":"`+task.ID+`","subtaskId":"`+first+`","delete":true}`)
if th, _ = store.GetThoughtByID(claims.UserID, task.ID); len(th.Subtasks) != 1 || th.Subtasks[0].ID != second {
t.Fatalf("subtasks after deleting: %+v", th.Subtasks)
}
if _, isErr = call(6, "update_subtask", `{"taskId":"`+task.ID+`","subtaskId":"st_unknown","completed":true}`); !isErr {
t.Fatalf("unknown subtask must fail")
}
if _, isErr = call(7, "add_subtask", `{"taskId":"`+task.ID+`","title":" "}`); !isErr {
t.Fatalf("empty title must fail")
}
other, _ := store.CreateUser("other@example.com", "hash", "Other", "member")
foreign, _ := store.CreateThoughtWithDetails(other.ID, "Fremd", "normal", "manual", "", "", "todo", "")
if _, isErr = call(8, "add_subtask", `{"taskId":"`+foreign.ID+`","title":"Spam"}`); !isErr {
t.Fatalf("key must not change foreign tasks")
}
}
// Operators can switch MCP off: in the admin settings, or fixed by MCP_ENABLED. OAuth goes with it.
func TestMCPSwitchOff(t *testing.T) {
srv, _, session := setupTestServer(t)