From b716d4d0fe65b820440aed23c14fff60bdc64065 Mon Sep 17 00:00:00 2001 From: Oliver Jakoubek Date: Thu, 15 Jan 2026 10:48:38 +0100 Subject: [PATCH 1/8] Fix Close/Invalidate/Reopen to handle API array response The Checkvist API returns an array of tasks (containing the modified task and potentially its subtasks) for close, reopen, and invalidate operations. The code was incorrectly trying to decode into a single Task struct. Changes: - Decode response into []Task instead of Task - Return first element (the modified task) - Add defensive error handling for empty arrays - Update tests to mock array responses Fixes: checkvist-api-2zr --- .beads/issues.jsonl | 1 + tasks.go | 39 +++++++++++++++++++++++++++------------ tasks_test.go | 15 +++++++++------ 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index e43deb2..962d183 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -3,6 +3,7 @@ {"id":"checkvist-api-1ki","title":"Write GoDoc examples","description":"Create example_test.go with runnable examples:\n- Example_basicUsage\n- ExampleNewClient\n- ExampleClient_Authenticate\n- ExampleChecklistService_List\n- ExampleTaskService_Create\n- ExampleNewTask (builder pattern)\n- ExampleNewFilter (filter builder)\n- ExampleFilter_Apply\nAll examples should be runnable and appear in GoDoc.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.443075101+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:35:43.090941735+01:00","closed_at":"2026-01-14T14:35:43.090941735+01:00","close_reason":"GoDoc examples für alle geforderten Funktionen implementiert","dependencies":[{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:14.730667505+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:15.039931257+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:15.388830036+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-1ze","title":"Implement client-side Filter builder","description":"Create filter.go with Filter builder (since Checkvist API has no server-side filtering):\n- NewFilter(tasks []Task) *Filter\n- WithTag(tag string) *Filter\n- WithTags(tags ...string) *Filter (AND logic)\n- WithStatus(status TaskStatus) *Filter\n- WithDueBefore(t time.Time) *Filter\n- WithDueAfter(t time.Time) *Filter\n- WithDueOn(t time.Time) *Filter\n- WithOverdue() *Filter\n- WithSearch(query string) *Filter (searches in Content)\n- Apply() []Task\nPerformance target: \u003c10ms for 1000+ tasks\nFilters are combined with AND logic.","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.24379077+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:33:38.687033358+01:00","closed_at":"2026-01-14T14:33:38.687033358+01:00","close_reason":"Filter builder mit allen geforderten Methoden implementiert","dependencies":[{"issue_id":"checkvist-api-1ze","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.543121262+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-1zf","title":"Write unit tests for Filter","description":"Create filter_test.go with tests:\n- TestFilter_WithTag\n- TestFilter_WithMultipleTags\n- TestFilter_WithStatus\n- TestFilter_WithDueBefore\n- TestFilter_WithDueAfter\n- TestFilter_WithDueOn\n- TestFilter_WithOverdue\n- TestFilter_WithSearch\n- TestFilter_Combined\n- TestFilter_Performance_1000Tasks\nUse table-driven tests.","status":"closed","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.136650557+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:34:50.361097261+01:00","closed_at":"2026-01-14T14:34:50.361097261+01:00","close_reason":"Alle geforderten Tests implementiert und bestanden","dependencies":[{"issue_id":"checkvist-api-1zf","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:14.4378645+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-2zr","title":"Fix Close/Invalidate/Reopen to handle array response","description":"## Description\n\nThe `Close()`, `Invalidate()`, and `Reopen()` methods on `TaskService` fail with a JSON decoding error because the Checkvist API returns an array of tasks, not a single task object.\n\n## Error\n\n```go\ntask, err := client.Tasks(945445).Close(ctx, taskID)\nif err != nil {\n panic(err)\n}\n```\n\nResults in:\n```\npanic: decoding response: json: cannot unmarshal array into Go value of type checkvist.Task\n```\n\n## Root Cause\n\nIn `tasks.go` (lines 204-240), all three methods decode the API response into a single `Task`:\n\n```go\nvar task Task\nif err := s.client.doPost(ctx, path, nil, \u0026task); err != nil {\n return nil, err\n}\n```\n\nBut the Checkvist API returns an array containing the modified task and potentially its subtasks.\n\n## Affected Methods\n\n- `TaskService.Close()` (tasks.go:204)\n- `TaskService.Reopen()` (tasks.go:217)\n- `TaskService.Invalidate()` (tasks.go:230)\n\n## Acceptance Criteria\n\n- [ ] All three methods correctly decode the array response\n- [ ] Return the first element (the modified task) as `*Task`\n- [ ] Update tests to use array response fixtures\n- [ ] Add integration test notes in code comments","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T10:26:22.308158727+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T10:48:33.012621346+01:00","closed_at":"2026-01-15T10:48:33.012621346+01:00","close_reason":"Closed"} {"id":"checkvist-api-347","title":"Write unit tests for Checklists","description":"Create checklists_test.go with tests:\n- TestChecklists_List\n- TestChecklists_ListArchived\n- TestChecklists_Get\n- TestChecklists_Get_NotFound\n- TestChecklists_Create\n- TestChecklists_Update\n- TestChecklists_Delete\nUse table-driven tests. Create testdata/checklists/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.243525209+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:41:58.076714398+01:00","closed_at":"2026-01-14T13:41:58.076714398+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-347","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:13.512887479+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-47y","title":"Quality \u0026 Documentation","description":"Phase 4: Complete unit tests, GoDoc examples, README, and CHANGELOG. Target \u003e80% test coverage. All public functions documented with examples.","status":"closed","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.655509387+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:37:52.12671299+01:00","closed_at":"2026-01-14T14:37:52.12671299+01:00","close_reason":"Alle zugehörigen Features und Tasks abgeschlossen"} {"id":"checkvist-api-4pz","title":"Fix DueDate not applied when creating tasks with WithDueDate()","description":"## Problem\n\nWhen creating tasks with `WithDueDate()`, the due date is not set on the created task, even though other parameters (position, priority, tags) work correctly.\n\n## Example Code\n\n```go\ntask, err := client.Tasks(945445).Create(ctx,\n checkvist.NewTask(\"Buy milk\").\n WithPosition(1).\n WithDueDate(checkvist.DueTomorrow).\n WithPriority(2).\n WithTags(\"Shopping\"),\n)\n// Result: task has position=1, priority=2, tags=\"Shopping\", but DueDate is nil\n```\n\n## Analysis\n\nThe JSON being sent to the API looks correct:\n```json\n{\n \"task\": {\n \"content\": \"Buy milk\",\n \"position\": 1,\n \"due_date\": \"^Tomorrow\",\n \"priority\": 2,\n \"tags\": \"Shopping\"\n }\n}\n```\n\nThe task wrapper format is correct (other fields work). The issue is likely with the due_date format:\n- Current format: `\"^Tomorrow\"` (with caret prefix)\n- API might expect: `\"tomorrow\"` (without caret) or different syntax\n\n## Investigation Needed\n\n1. Check Checkvist API documentation for exact `due_date` format\n2. Test with different formats:\n - `\"tomorrow\"` (without ^)\n - `\"Tomorrow\"` (capitalized, without ^)\n - `\"2026-01-16\"` (ISO date)\n3. Check if the `^` prefix is only used in task content, not in API parameters\n\n## Affected Code\n\n- `models.go:178-188` - DueDate constants with `^` prefix\n- `models.go:190-205` - DueAt(), DueString(), DueInDays() functions\n\n## Acceptance Criteria\n\n- [ ] `WithDueDate(DueTomorrow)` creates task with correct due date\n- [ ] `WithDueDate(DueAt(time))` creates task with correct due date\n- [ ] All DueDate constants work correctly","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T10:11:00.062239196+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T10:17:38.380280674+01:00","closed_at":"2026-01-15T10:17:38.380280674+01:00","close_reason":"Closed"} diff --git a/tasks.go b/tasks.go index a8d358e..f45ad34 100644 --- a/tasks.go +++ b/tasks.go @@ -201,42 +201,57 @@ func (s *TaskService) Delete(ctx context.Context, taskID int) error { } // Close marks a task as completed. +// The API returns an array containing the modified task and potentially its subtasks. func (s *TaskService) Close(ctx context.Context, taskID int) (*Task, error) { path := fmt.Sprintf("/checklists/%d/tasks/%d/close.json", s.checklistID, taskID) - var task Task - if err := s.client.doPost(ctx, path, nil, &task); err != nil { + var tasks []Task + if err := s.client.doPost(ctx, path, nil, &tasks); err != nil { return nil, err } - parseDueDate(&task) - return &task, nil + if len(tasks) == 0 { + return nil, fmt.Errorf("close task: unexpected empty response") + } + + parseDueDate(&tasks[0]) + return &tasks[0], nil } // Reopen reopens a closed or invalidated task. +// The API returns an array containing the modified task and potentially its subtasks. func (s *TaskService) Reopen(ctx context.Context, taskID int) (*Task, error) { path := fmt.Sprintf("/checklists/%d/tasks/%d/reopen.json", s.checklistID, taskID) - var task Task - if err := s.client.doPost(ctx, path, nil, &task); err != nil { + var tasks []Task + if err := s.client.doPost(ctx, path, nil, &tasks); err != nil { return nil, err } - parseDueDate(&task) - return &task, nil + if len(tasks) == 0 { + return nil, fmt.Errorf("reopen task: unexpected empty response") + } + + parseDueDate(&tasks[0]) + return &tasks[0], nil } // Invalidate marks a task as invalidated (strikethrough). +// The API returns an array containing the modified task and potentially its subtasks. func (s *TaskService) Invalidate(ctx context.Context, taskID int) (*Task, error) { path := fmt.Sprintf("/checklists/%d/tasks/%d/invalidate.json", s.checklistID, taskID) - var task Task - if err := s.client.doPost(ctx, path, nil, &task); err != nil { + var tasks []Task + if err := s.client.doPost(ctx, path, nil, &tasks); err != nil { return nil, err } - parseDueDate(&task) - return &task, nil + if len(tasks) == 0 { + return nil, fmt.Errorf("invalidate task: unexpected empty response") + } + + parseDueDate(&tasks[0]) + return &tasks[0], nil } // parseDueDate attempts to parse the DueDateRaw string into a time.Time. diff --git a/tasks_test.go b/tasks_test.go index 6850be0..00c4960 100644 --- a/tasks_test.go +++ b/tasks_test.go @@ -277,10 +277,11 @@ func TestTasks_Close(t *testing.T) { if r.Method != http.MethodPost { t.Errorf("expected POST, got %s", r.Method) } - response := Task{ + // API returns an array containing the modified task and potentially subtasks + response := []Task{{ ID: 101, Status: StatusClosed, - } + }} json.NewEncoder(w).Encode(response) default: t.Errorf("unexpected path: %s", r.URL.Path) @@ -310,10 +311,11 @@ func TestTasks_Reopen(t *testing.T) { if r.Method != http.MethodPost { t.Errorf("expected POST, got %s", r.Method) } - response := Task{ + // API returns an array containing the modified task and potentially subtasks + response := []Task{{ ID: 101, Status: StatusOpen, - } + }} json.NewEncoder(w).Encode(response) default: t.Errorf("unexpected path: %s", r.URL.Path) @@ -343,10 +345,11 @@ func TestTasks_Invalidate(t *testing.T) { if r.Method != http.MethodPost { t.Errorf("expected POST, got %s", r.Method) } - response := Task{ + // API returns an array containing the modified task and potentially subtasks + response := []Task{{ ID: 101, Status: StatusInvalidated, - } + }} json.NewEncoder(w).Encode(response) default: t.Errorf("unexpected path: %s", r.URL.Path) From f1b47414dbc0fae71efadefcbe7637e71907ea77 Mon Sep 17 00:00:00 2001 From: Oliver Jakoubek Date: Thu, 15 Jan 2026 10:48:52 +0100 Subject: [PATCH 2/8] bd sync: 2026-01-15 --- .beads/sync_base.jsonl | 1 + 1 file changed, 1 insertion(+) diff --git a/.beads/sync_base.jsonl b/.beads/sync_base.jsonl index bd70a25..534ceb9 100644 --- a/.beads/sync_base.jsonl +++ b/.beads/sync_base.jsonl @@ -3,6 +3,7 @@ {"id":"checkvist-api-1ki","title":"Write GoDoc examples","description":"Create example_test.go with runnable examples:\n- Example_basicUsage\n- ExampleNewClient\n- ExampleClient_Authenticate\n- ExampleChecklistService_List\n- ExampleTaskService_Create\n- ExampleNewTask (builder pattern)\n- ExampleNewFilter (filter builder)\n- ExampleFilter_Apply\nAll examples should be runnable and appear in GoDoc.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.443075101+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:35:43.090941735+01:00","closed_at":"2026-01-14T14:35:43.090941735+01:00","close_reason":"GoDoc examples für alle geforderten Funktionen implementiert","dependencies":[{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:14.730667505+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:15.039931257+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-1ki","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:15.388830036+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-1ze","title":"Implement client-side Filter builder","description":"Create filter.go with Filter builder (since Checkvist API has no server-side filtering):\n- NewFilter(tasks []Task) *Filter\n- WithTag(tag string) *Filter\n- WithTags(tags ...string) *Filter (AND logic)\n- WithStatus(status TaskStatus) *Filter\n- WithDueBefore(t time.Time) *Filter\n- WithDueAfter(t time.Time) *Filter\n- WithDueOn(t time.Time) *Filter\n- WithOverdue() *Filter\n- WithSearch(query string) *Filter (searches in Content)\n- Apply() []Task\nPerformance target: <10ms for 1000+ tasks\nFilters are combined with AND logic.","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:56.24379077+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:33:38.687033358+01:00","closed_at":"2026-01-14T14:33:38.687033358+01:00","close_reason":"Filter builder mit allen geforderten Methoden implementiert","dependencies":[{"issue_id":"checkvist-api-1ze","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.543121262+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-1zf","title":"Write unit tests for Filter","description":"Create filter_test.go with tests:\n- TestFilter_WithTag\n- TestFilter_WithMultipleTags\n- TestFilter_WithStatus\n- TestFilter_WithDueBefore\n- TestFilter_WithDueAfter\n- TestFilter_WithDueOn\n- TestFilter_WithOverdue\n- TestFilter_WithSearch\n- TestFilter_Combined\n- TestFilter_Performance_1000Tasks\nUse table-driven tests.","status":"closed","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.136650557+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:34:50.361097261+01:00","closed_at":"2026-01-14T14:34:50.361097261+01:00","close_reason":"Alle geforderten Tests implementiert und bestanden","dependencies":[{"issue_id":"checkvist-api-1zf","depends_on_id":"checkvist-api-1ze","type":"blocks","created_at":"2026-01-14T12:33:14.4378645+01:00","created_by":"Oliver Jakoubek"}]} +{"id":"checkvist-api-2zr","title":"Fix Close/Invalidate/Reopen to handle array response","description":"## Description\n\nThe `Close()`, `Invalidate()`, and `Reopen()` methods on `TaskService` fail with a JSON decoding error because the Checkvist API returns an array of tasks, not a single task object.\n\n## Error\n\n```go\ntask, err := client.Tasks(945445).Close(ctx, taskID)\nif err != nil {\n panic(err)\n}\n```\n\nResults in:\n```\npanic: decoding response: json: cannot unmarshal array into Go value of type checkvist.Task\n```\n\n## Root Cause\n\nIn `tasks.go` (lines 204-240), all three methods decode the API response into a single `Task`:\n\n```go\nvar task Task\nif err := s.client.doPost(ctx, path, nil, &task); err != nil {\n return nil, err\n}\n```\n\nBut the Checkvist API returns an array containing the modified task and potentially its subtasks.\n\n## Affected Methods\n\n- `TaskService.Close()` (tasks.go:204)\n- `TaskService.Reopen()` (tasks.go:217)\n- `TaskService.Invalidate()` (tasks.go:230)\n\n## Acceptance Criteria\n\n- [ ] All three methods correctly decode the array response\n- [ ] Return the first element (the modified task) as `*Task`\n- [ ] Update tests to use array response fixtures\n- [ ] Add integration test notes in code comments","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T10:26:22.308158727+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T10:48:33.012621346+01:00","closed_at":"2026-01-15T10:48:33.012621346+01:00","close_reason":"Closed"} {"id":"checkvist-api-347","title":"Write unit tests for Checklists","description":"Create checklists_test.go with tests:\n- TestChecklists_List\n- TestChecklists_ListArchived\n- TestChecklists_Get\n- TestChecklists_Get_NotFound\n- TestChecklists_Create\n- TestChecklists_Update\n- TestChecklists_Delete\nUse table-driven tests. Create testdata/checklists/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.243525209+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:41:58.076714398+01:00","closed_at":"2026-01-14T13:41:58.076714398+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-347","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:13.512887479+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-47y","title":"Quality & Documentation","description":"Phase 4: Complete unit tests, GoDoc examples, README, and CHANGELOG. Target >80% test coverage. All public functions documented with examples.","status":"closed","priority":0,"issue_type":"epic","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:36.655509387+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:37:52.12671299+01:00","closed_at":"2026-01-14T14:37:52.12671299+01:00","close_reason":"Alle zugehörigen Features und Tasks abgeschlossen"} {"id":"checkvist-api-4pz","title":"Fix DueDate not applied when creating tasks with WithDueDate()","description":"## Problem\n\nWhen creating tasks with `WithDueDate()`, the due date is not set on the created task, even though other parameters (position, priority, tags) work correctly.\n\n## Example Code\n\n```go\ntask, err := client.Tasks(945445).Create(ctx,\n checkvist.NewTask(\"Buy milk\").\n WithPosition(1).\n WithDueDate(checkvist.DueTomorrow).\n WithPriority(2).\n WithTags(\"Shopping\"),\n)\n// Result: task has position=1, priority=2, tags=\"Shopping\", but DueDate is nil\n```\n\n## Analysis\n\nThe JSON being sent to the API looks correct:\n```json\n{\n \"task\": {\n \"content\": \"Buy milk\",\n \"position\": 1,\n \"due_date\": \"^Tomorrow\",\n \"priority\": 2,\n \"tags\": \"Shopping\"\n }\n}\n```\n\nThe task wrapper format is correct (other fields work). The issue is likely with the due_date format:\n- Current format: `\"^Tomorrow\"` (with caret prefix)\n- API might expect: `\"tomorrow\"` (without caret) or different syntax\n\n## Investigation Needed\n\n1. Check Checkvist API documentation for exact `due_date` format\n2. Test with different formats:\n - `\"tomorrow\"` (without ^)\n - `\"Tomorrow\"` (capitalized, without ^)\n - `\"2026-01-16\"` (ISO date)\n3. Check if the `^` prefix is only used in task content, not in API parameters\n\n## Affected Code\n\n- `models.go:178-188` - DueDate constants with `^` prefix\n- `models.go:190-205` - DueAt(), DueString(), DueInDays() functions\n\n## Acceptance Criteria\n\n- [ ] `WithDueDate(DueTomorrow)` creates task with correct due date\n- [ ] `WithDueDate(DueAt(time))` creates task with correct due date\n- [ ] All DueDate constants work correctly","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T10:11:00.062239196+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T10:17:38.380280674+01:00","closed_at":"2026-01-15T10:17:38.380280674+01:00","close_reason":"Closed"} From e4dfa7dd725bc05f3d5cb93d6c93fdf8a4c5dc93 Mon Sep 17 00:00:00 2001 From: Oliver Jakoubek Date: Thu, 15 Jan 2026 12:21:14 +0100 Subject: [PATCH 3/8] docs(changelog): Add release entries for v1.0.0, v1.0.1, v1.0.2 --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8215b8d..d1c8d80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **Tasks**: Fix `Close()`, `Reopen()`, and `Invalidate()` to handle API array response format + +## [1.0.2] - 2026-01-15 + +### Fixed + +- **DueDate**: Remove caret (`^`) prefix from due date values - API expects values like `"Tomorrow"` not `"^Tomorrow"` + +### Removed + +- **DueDate**: Remove unused `DueASAP` and `DueMonday` constants + +## [1.0.1] - 2026-01-15 + +### Fixed + +- **DueDate**: Fix parsing of due dates returned by API - now supports both slash format (`2026/01/15`) and ISO format (`2026-01-15`) + +## [1.0.0] - 2026-01-14 + ### Added - **Client**: Core API client with functional options pattern From 536ccf6ccfadb10e73aeb01f1f4c879464c94934 Mon Sep 17 00:00:00 2001 From: Oliver Jakoubek Date: Sun, 18 Jan 2026 14:41:31 +0100 Subject: [PATCH 4/8] bd sync: 2026-01-18 14:41:31 --- .beads/issues.jsonl | 1 + 1 file changed, 1 insertion(+) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 962d183..f6cccbb 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -27,6 +27,7 @@ {"id":"checkvist-api-mnh","title":"Implement error types and sentinel errors","description":"Create errors.go with:\n- APIError struct (StatusCode, Message, RequestID, Err) with Error() and Unwrap() methods\n- Sentinel errors: ErrUnauthorized, ErrNotFound, ErrRateLimited, ErrBadRequest, ErrServerError\n- Helper function to create APIError from HTTP response","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:07.619359293+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:23:43.296883265+01:00","closed_at":"2026-01-14T13:23:43.296883265+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-mnh","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:46.754134799+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-nrk","title":"Create README with quickstart","description":"Create README.md (in English) with:\n- Project description\n- Installation: go get code.beautifulmachines.dev/jakoubek/checkvist-api\n- Quick start example (init client, list checklists, create task)\n- API overview (Checklists, Tasks, Notes, Filters)\n- Builder pattern examples\n- Error handling examples\n- Configuration options\n- Link to GoDoc\n- License (MIT)","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.724338606+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:48:30.163762004+01:00","closed_at":"2026-01-14T13:48:30.163762004+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:15.785698203+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:16.125115134+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-otm","title":"Fix DueDate parsing: API returns slashes, parser expects dashes","description":"## Problem\n\nWhen fetching tasks via `TaskService.List()`, `DueDate` is always `nil` even for tasks that have a due date set.\n\n## Root Cause\n\nThe Checkvist API returns due dates in slash format (`2026/01/15`), but `parseDueDate()` in `tasks.go:250` only parses the ISO format with dashes:\n\n```go\n// Current implementation - only supports dashes\nif t, err := time.Parse(\"2006-01-02\", task.DueDateRaw); err == nil {\n task.DueDate = \u0026t\n}\n```\n\nAPI response format: `\"due\": \"2026/01/15\"` (slashes)\nExpected by parser: `\"2026-01-15\"` (dashes)\n\n## Solution\n\nExtend `parseDueDate()` to also parse the slash format:\n\n```go\nfunc parseDueDate(task *Task) {\n if task.DueDateRaw == \"\" {\n return\n }\n \n // Try multiple formats\n formats := []string{\n \"2006-01-02\", // ISO format (dashes)\n \"2006/01/02\", // Checkvist API format (slashes)\n }\n \n for _, format := range formats {\n if t, err := time.Parse(format, task.DueDateRaw); err == nil {\n task.DueDate = \u0026t\n return\n }\n }\n}\n```\n\n## Affected Code\n\n- `tasks.go:244-253` - `parseDueDate()` function\n\n## Acceptance Criteria\n\n- [ ] Tasks with due dates have `DueDate` correctly parsed\n- [ ] Both slash format (`2026/01/15`) and dash format (`2026-01-15`) work\n- [ ] Unit test added for slash format parsing","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T09:05:02.138578452+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T09:29:14.390484526+01:00","closed_at":"2026-01-15T09:29:14.390484526+01:00","close_reason":"Closed"} +{"id":"checkvist-api-rcj","title":"Fix Task.tasks field: expect []int not []*Task","description":"## Problem\n\nWhen decoding API responses containing the `tasks` field, JSON unmarshaling fails with:\n```\njson: cannot unmarshal number into Go struct field Task.tasks of type checkvist.Task\n```\n\n## Root Cause\n\nIn `models.go:144`, the `Children` field is incorrectly typed:\n```go\nChildren []*Task `json:\"tasks,omitempty\"`\n```\n\nAccording to the Checkvist API documentation, the `tasks` field is:\n\u003e \"tasks [JSON] Javascript array of children task IDs\"\n\nThe API returns an **array of integers** (child task IDs), not an array of full Task objects.\n\n## Solution\n\nChange the field type from `[]*Task` to `[]int` and rename appropriately:\n```go\nChildIDs []int `json:\"tasks,omitempty\"` // IDs of child tasks\n```\n\nThis is a breaking change for users who relied on `Children []*Task`, but the current implementation was incorrect and would fail on real API responses anyway.\n\n## Acceptance Criteria\n\n- [ ] `Task.ChildIDs` correctly unmarshals as `[]int`\n- [ ] Update test fixtures to include `tasks` field with sample child IDs\n- [ ] Tests pass with real API response structure\n- [ ] Update CHANGELOG.md noting the breaking change","status":"in_progress","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-18T14:32:44.748293162+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-18T14:34:44.774842767+01:00"} {"id":"checkvist-api-rl9","title":"Implement Task operations","description":"Create tasks.go with TaskService:\n- client.Tasks(checklistID) returns TaskService\n- List(ctx) ([]Task, error) - GET /checklists/{id}/tasks.json\n- Get(ctx, taskID) (*Task, error) - GET /checklists/{id}/tasks/{task_id}.json (includes parent hierarchy)\n- Create(ctx, builder *TaskBuilder) (*Task, error) - POST /checklists/{id}/tasks.json\n- Update(ctx, taskID, opts) (*Task, error) - PUT /checklists/{id}/tasks/{task_id}.json\n- Delete(ctx, taskID) error - DELETE /checklists/{id}/tasks/{task_id}.json\n- Close(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/close.json\n- Reopen(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/reopen.json\n- Invalidate(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/invalidate.json\nParse DueDate from DueDateRaw when retrieving tasks.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.90629838+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:39:29.443727155+01:00","closed_at":"2026-01-14T13:39:29.443727155+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:32:55.247292478+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:32:55.536931433+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-tjk","title":"Implement TaskBuilder fluent interface","description":"Enhance task creation with builder pattern:\n- NewTask(content string) *TaskBuilder\n- WithTags(tags ...string) *TaskBuilder\n- WithDueDate(due DueDate) *TaskBuilder\n- WithPriority(p int) *TaskBuilder\n- WithParent(parentID int64) *TaskBuilder\n- WithPosition(pos int) *TaskBuilder\n- Build() returns parameters for API call\nTaskBuilder should be chainable and return itself for fluent usage:\n checkvist.NewTask(\"Content\").WithTags(\"tag1\").WithDueDate(checkvist.DueTomorrow).WithPriority(1)","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:55.929907579+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:30:42.405125498+01:00","closed_at":"2026-01-14T14:30:42.405125498+01:00","close_reason":"TaskBuilder war bereits vollständig implementiert und getestet","dependencies":[{"issue_id":"checkvist-api-tjk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.20339206+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-v2f","title":"Write unit tests for Tasks","description":"Create tasks_test.go with tests:\n- TestTasks_List\n- TestTasks_Get\n- TestTasks_Get_WithParentHierarchy\n- TestTasks_Create\n- TestTasks_Create_WithBuilder\n- TestTasks_Update\n- TestTasks_Delete\n- TestTasks_Close\n- TestTasks_Reopen\n- TestTasks_Invalidate\n- TestDueDate_Parsing\nUse table-driven tests. Create testdata/tasks/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.538754679+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:46:01.50434559+01:00","closed_at":"2026-01-14T13:46:01.50434559+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-v2f","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:13.81085058+01:00","created_by":"Oliver Jakoubek"}]} From d92a1b90c25e10c2782667df008cf4a978bfa431 Mon Sep 17 00:00:00 2001 From: Oliver Jakoubek Date: Sun, 18 Jan 2026 14:41:42 +0100 Subject: [PATCH 5/8] Fix Task.tasks field type: rename Children []*Task to ChildIDs []int The Checkvist API returns the 'tasks' field as an array of child task IDs (integers), not as full Task objects. This was causing JSON unmarshal errors. BREAKING CHANGE: Task.Children []*Task renamed to Task.ChildIDs []int --- CHANGELOG.md | 4 +++ models.go | 4 +-- tasks_test.go | 54 ++++++++++++++++++++++++++++++++++++++ testdata/tasks/list.json | 6 +++-- testdata/tasks/single.json | 3 ++- 5 files changed, 66 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1c8d80..dff998a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING**: `Task.Children []*Task` renamed to `Task.ChildIDs []int` to match API response format (API returns array of child task IDs, not full task objects) + ### Fixed - **Tasks**: Fix `Close()`, `Reopen()`, and `Invalidate()` to handle API array response format diff --git a/models.go b/models.go index 05e7cae..f4aaf84 100644 --- a/models.go +++ b/models.go @@ -140,8 +140,8 @@ type Task struct { UpdatedAt APITime `json:"updated_at"` // CreatedAt is the timestamp when the task was created. CreatedAt APITime `json:"created_at"` - // Children contains nested child tasks (when fetched with tree structure). - Children []*Task `json:"tasks,omitempty"` + // ChildIDs contains IDs of child tasks (returned by API as array of integers). + ChildIDs []int `json:"tasks,omitempty"` // Notes contains the comments/notes attached to this task. Notes []Note `json:"notes,omitempty"` } diff --git a/tasks_test.go b/tasks_test.go index 00c4960..adbc2d0 100644 --- a/tasks_test.go +++ b/tasks_test.go @@ -73,6 +73,60 @@ func TestTasks_Get(t *testing.T) { } } +func TestTask_ChildIDs_Unmarshal(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.URL.Path { + case "/auth/login.json": + json.NewEncoder(w).Encode(map[string]string{"token": "test-token"}) + case "/checklists/1/tasks/101.json": + w.Write(loadFixture(t, "testdata/tasks/single.json")) + case "/checklists/1/tasks.json": + w.Write(loadFixture(t, "testdata/tasks/list.json")) + default: + t.Errorf("unexpected path: %s", r.URL.Path) + } + })) + defer server.Close() + + client := NewClient("user@example.com", "api-key", WithBaseURL(server.URL)) + + t.Run("single task with child IDs", func(t *testing.T) { + task, err := client.Tasks(1).Get(context.Background(), 101) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expectedChildIDs := []int{201, 202, 203} + if len(task.ChildIDs) != len(expectedChildIDs) { + t.Fatalf("expected %d child IDs, got %d", len(expectedChildIDs), len(task.ChildIDs)) + } + for i, id := range expectedChildIDs { + if task.ChildIDs[i] != id { + t.Errorf("expected ChildIDs[%d] = %d, got %d", i, id, task.ChildIDs[i]) + } + } + }) + + t.Run("list with mixed child IDs", func(t *testing.T) { + tasks, err := client.Tasks(1).List(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // First task has child IDs [201, 202] + if len(tasks[0].ChildIDs) != 2 { + t.Errorf("expected 2 child IDs for first task, got %d", len(tasks[0].ChildIDs)) + } + if tasks[0].ChildIDs[0] != 201 || tasks[0].ChildIDs[1] != 202 { + t.Errorf("expected ChildIDs [201, 202], got %v", tasks[0].ChildIDs) + } + // Second task has empty child IDs + if len(tasks[1].ChildIDs) != 0 { + t.Errorf("expected 0 child IDs for second task, got %d", len(tasks[1].ChildIDs)) + } + }) +} + func TestTasks_Create(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") diff --git a/testdata/tasks/list.json b/testdata/tasks/list.json index b3f5737..50a97fe 100644 --- a/testdata/tasks/list.json +++ b/testdata/tasks/list.json @@ -13,7 +13,8 @@ "comments_count": 0, "update_line": "", "updated_at": "2026/01/14 10:00:00 +0000", - "created_at": "2026/01/10 09:00:00 +0000" + "created_at": "2026/01/10 09:00:00 +0000", + "tasks": [201, 202] }, { "id": 102, @@ -29,6 +30,7 @@ "comments_count": 3, "update_line": "", "updated_at": "2026/01/14 11:00:00 +0000", - "created_at": "2026/01/11 10:00:00 +0000" + "created_at": "2026/01/11 10:00:00 +0000", + "tasks": [] } ] diff --git a/testdata/tasks/single.json b/testdata/tasks/single.json index ebd53a9..1107e24 100644 --- a/testdata/tasks/single.json +++ b/testdata/tasks/single.json @@ -12,5 +12,6 @@ "comments_count": 0, "update_line": "", "updated_at": "2026/01/14 10:00:00 +0000", - "created_at": "2026/01/10 09:00:00 +0000" + "created_at": "2026/01/10 09:00:00 +0000", + "tasks": [201, 202, 203] } From b97877f7fc7f25c792c0f399a8ba96aadc2074a6 Mon Sep 17 00:00:00 2001 From: Oliver Jakoubek Date: Sun, 18 Jan 2026 14:42:11 +0100 Subject: [PATCH 6/8] bd sync: 2026-01-18 14:42:11 --- .beads/sync_base.jsonl | 1 + 1 file changed, 1 insertion(+) diff --git a/.beads/sync_base.jsonl b/.beads/sync_base.jsonl index 534ceb9..15b99f0 100644 --- a/.beads/sync_base.jsonl +++ b/.beads/sync_base.jsonl @@ -27,6 +27,7 @@ {"id":"checkvist-api-mnh","title":"Implement error types and sentinel errors","description":"Create errors.go with:\n- APIError struct (StatusCode, Message, RequestID, Err) with Error() and Unwrap() methods\n- Sentinel errors: ErrUnauthorized, ErrNotFound, ErrRateLimited, ErrBadRequest, ErrServerError\n- Helper function to create APIError from HTTP response","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:07.619359293+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:23:43.296883265+01:00","closed_at":"2026-01-14T13:23:43.296883265+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-mnh","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:46.754134799+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-nrk","title":"Create README with quickstart","description":"Create README.md (in English) with:\n- Project description\n- Installation: go get code.beautifulmachines.dev/jakoubek/checkvist-api\n- Quick start example (init client, list checklists, create task)\n- API overview (Checklists, Tasks, Notes, Filters)\n- Builder pattern examples\n- Error handling examples\n- Configuration options\n- Link to GoDoc\n- License (MIT)","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.724338606+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:48:30.163762004+01:00","closed_at":"2026-01-14T13:48:30.163762004+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:15.785698203+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:16.125115134+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-otm","title":"Fix DueDate parsing: API returns slashes, parser expects dashes","description":"## Problem\n\nWhen fetching tasks via `TaskService.List()`, `DueDate` is always `nil` even for tasks that have a due date set.\n\n## Root Cause\n\nThe Checkvist API returns due dates in slash format (`2026/01/15`), but `parseDueDate()` in `tasks.go:250` only parses the ISO format with dashes:\n\n```go\n// Current implementation - only supports dashes\nif t, err := time.Parse(\"2006-01-02\", task.DueDateRaw); err == nil {\n task.DueDate = &t\n}\n```\n\nAPI response format: `\"due\": \"2026/01/15\"` (slashes)\nExpected by parser: `\"2026-01-15\"` (dashes)\n\n## Solution\n\nExtend `parseDueDate()` to also parse the slash format:\n\n```go\nfunc parseDueDate(task *Task) {\n if task.DueDateRaw == \"\" {\n return\n }\n \n // Try multiple formats\n formats := []string{\n \"2006-01-02\", // ISO format (dashes)\n \"2006/01/02\", // Checkvist API format (slashes)\n }\n \n for _, format := range formats {\n if t, err := time.Parse(format, task.DueDateRaw); err == nil {\n task.DueDate = &t\n return\n }\n }\n}\n```\n\n## Affected Code\n\n- `tasks.go:244-253` - `parseDueDate()` function\n\n## Acceptance Criteria\n\n- [ ] Tasks with due dates have `DueDate` correctly parsed\n- [ ] Both slash format (`2026/01/15`) and dash format (`2026-01-15`) work\n- [ ] Unit test added for slash format parsing","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T09:05:02.138578452+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T09:29:14.390484526+01:00","closed_at":"2026-01-15T09:29:14.390484526+01:00","close_reason":"Closed"} +{"id":"checkvist-api-rcj","title":"Fix Task.tasks field: expect []int not []*Task","description":"## Problem\n\nWhen decoding API responses containing the `tasks` field, JSON unmarshaling fails with:\n```\njson: cannot unmarshal number into Go struct field Task.tasks of type checkvist.Task\n```\n\n## Root Cause\n\nIn `models.go:144`, the `Children` field is incorrectly typed:\n```go\nChildren []*Task `json:\"tasks,omitempty\"`\n```\n\nAccording to the Checkvist API documentation, the `tasks` field is:\n> \"tasks [JSON] Javascript array of children task IDs\"\n\nThe API returns an **array of integers** (child task IDs), not an array of full Task objects.\n\n## Solution\n\nChange the field type from `[]*Task` to `[]int` and rename appropriately:\n```go\nChildIDs []int `json:\"tasks,omitempty\"` // IDs of child tasks\n```\n\nThis is a breaking change for users who relied on `Children []*Task`, but the current implementation was incorrect and would fail on real API responses anyway.\n\n## Acceptance Criteria\n\n- [ ] `Task.ChildIDs` correctly unmarshals as `[]int`\n- [ ] Update test fixtures to include `tasks` field with sample child IDs\n- [ ] Tests pass with real API response structure\n- [ ] Update CHANGELOG.md noting the breaking change","status":"in_progress","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-18T14:32:44.748293162+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-18T14:34:44.774842767+01:00"} {"id":"checkvist-api-rl9","title":"Implement Task operations","description":"Create tasks.go with TaskService:\n- client.Tasks(checklistID) returns TaskService\n- List(ctx) ([]Task, error) - GET /checklists/{id}/tasks.json\n- Get(ctx, taskID) (*Task, error) - GET /checklists/{id}/tasks/{task_id}.json (includes parent hierarchy)\n- Create(ctx, builder *TaskBuilder) (*Task, error) - POST /checklists/{id}/tasks.json\n- Update(ctx, taskID, opts) (*Task, error) - PUT /checklists/{id}/tasks/{task_id}.json\n- Delete(ctx, taskID) error - DELETE /checklists/{id}/tasks/{task_id}.json\n- Close(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/close.json\n- Reopen(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/reopen.json\n- Invalidate(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/invalidate.json\nParse DueDate from DueDateRaw when retrieving tasks.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.90629838+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:39:29.443727155+01:00","closed_at":"2026-01-14T13:39:29.443727155+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:32:55.247292478+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:32:55.536931433+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-tjk","title":"Implement TaskBuilder fluent interface","description":"Enhance task creation with builder pattern:\n- NewTask(content string) *TaskBuilder\n- WithTags(tags ...string) *TaskBuilder\n- WithDueDate(due DueDate) *TaskBuilder\n- WithPriority(p int) *TaskBuilder\n- WithParent(parentID int64) *TaskBuilder\n- WithPosition(pos int) *TaskBuilder\n- Build() returns parameters for API call\nTaskBuilder should be chainable and return itself for fluent usage:\n checkvist.NewTask(\"Content\").WithTags(\"tag1\").WithDueDate(checkvist.DueTomorrow).WithPriority(1)","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:55.929907579+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:30:42.405125498+01:00","closed_at":"2026-01-14T14:30:42.405125498+01:00","close_reason":"TaskBuilder war bereits vollständig implementiert und getestet","dependencies":[{"issue_id":"checkvist-api-tjk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.20339206+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-v2f","title":"Write unit tests for Tasks","description":"Create tasks_test.go with tests:\n- TestTasks_List\n- TestTasks_Get\n- TestTasks_Get_WithParentHierarchy\n- TestTasks_Create\n- TestTasks_Create_WithBuilder\n- TestTasks_Update\n- TestTasks_Delete\n- TestTasks_Close\n- TestTasks_Reopen\n- TestTasks_Invalidate\n- TestDueDate_Parsing\nUse table-driven tests. Create testdata/tasks/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.538754679+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:46:01.50434559+01:00","closed_at":"2026-01-14T13:46:01.50434559+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-v2f","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:13.81085058+01:00","created_by":"Oliver Jakoubek"}]} From b5e71cc320ba60bac12e0003cbf72e9b9ca870c4 Mon Sep 17 00:00:00 2001 From: Oliver Jakoubek Date: Sun, 18 Jan 2026 14:42:28 +0100 Subject: [PATCH 7/8] bd sync: 2026-01-18 14:42:28 --- .beads/issues.jsonl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index f6cccbb..c004d65 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -27,7 +27,7 @@ {"id":"checkvist-api-mnh","title":"Implement error types and sentinel errors","description":"Create errors.go with:\n- APIError struct (StatusCode, Message, RequestID, Err) with Error() and Unwrap() methods\n- Sentinel errors: ErrUnauthorized, ErrNotFound, ErrRateLimited, ErrBadRequest, ErrServerError\n- Helper function to create APIError from HTTP response","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:07.619359293+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:23:43.296883265+01:00","closed_at":"2026-01-14T13:23:43.296883265+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-mnh","depends_on_id":"checkvist-api-5wr","type":"blocks","created_at":"2026-01-14T12:32:46.754134799+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-nrk","title":"Create README with quickstart","description":"Create README.md (in English) with:\n- Project description\n- Installation: go get code.beautifulmachines.dev/jakoubek/checkvist-api\n- Quick start example (init client, list checklists, create task)\n- API overview (Checklists, Tasks, Notes, Filters)\n- Builder pattern examples\n- Error handling examples\n- Configuration options\n- Link to GoDoc\n- License (MIT)","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:38.724338606+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:48:30.163762004+01:00","closed_at":"2026-01-14T13:48:30.163762004+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-c2k","type":"blocks","created_at":"2026-01-14T12:33:15.785698203+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-nrk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:16.125115134+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-otm","title":"Fix DueDate parsing: API returns slashes, parser expects dashes","description":"## Problem\n\nWhen fetching tasks via `TaskService.List()`, `DueDate` is always `nil` even for tasks that have a due date set.\n\n## Root Cause\n\nThe Checkvist API returns due dates in slash format (`2026/01/15`), but `parseDueDate()` in `tasks.go:250` only parses the ISO format with dashes:\n\n```go\n// Current implementation - only supports dashes\nif t, err := time.Parse(\"2006-01-02\", task.DueDateRaw); err == nil {\n task.DueDate = \u0026t\n}\n```\n\nAPI response format: `\"due\": \"2026/01/15\"` (slashes)\nExpected by parser: `\"2026-01-15\"` (dashes)\n\n## Solution\n\nExtend `parseDueDate()` to also parse the slash format:\n\n```go\nfunc parseDueDate(task *Task) {\n if task.DueDateRaw == \"\" {\n return\n }\n \n // Try multiple formats\n formats := []string{\n \"2006-01-02\", // ISO format (dashes)\n \"2006/01/02\", // Checkvist API format (slashes)\n }\n \n for _, format := range formats {\n if t, err := time.Parse(format, task.DueDateRaw); err == nil {\n task.DueDate = \u0026t\n return\n }\n }\n}\n```\n\n## Affected Code\n\n- `tasks.go:244-253` - `parseDueDate()` function\n\n## Acceptance Criteria\n\n- [ ] Tasks with due dates have `DueDate` correctly parsed\n- [ ] Both slash format (`2026/01/15`) and dash format (`2026-01-15`) work\n- [ ] Unit test added for slash format parsing","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T09:05:02.138578452+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T09:29:14.390484526+01:00","closed_at":"2026-01-15T09:29:14.390484526+01:00","close_reason":"Closed"} -{"id":"checkvist-api-rcj","title":"Fix Task.tasks field: expect []int not []*Task","description":"## Problem\n\nWhen decoding API responses containing the `tasks` field, JSON unmarshaling fails with:\n```\njson: cannot unmarshal number into Go struct field Task.tasks of type checkvist.Task\n```\n\n## Root Cause\n\nIn `models.go:144`, the `Children` field is incorrectly typed:\n```go\nChildren []*Task `json:\"tasks,omitempty\"`\n```\n\nAccording to the Checkvist API documentation, the `tasks` field is:\n\u003e \"tasks [JSON] Javascript array of children task IDs\"\n\nThe API returns an **array of integers** (child task IDs), not an array of full Task objects.\n\n## Solution\n\nChange the field type from `[]*Task` to `[]int` and rename appropriately:\n```go\nChildIDs []int `json:\"tasks,omitempty\"` // IDs of child tasks\n```\n\nThis is a breaking change for users who relied on `Children []*Task`, but the current implementation was incorrect and would fail on real API responses anyway.\n\n## Acceptance Criteria\n\n- [ ] `Task.ChildIDs` correctly unmarshals as `[]int`\n- [ ] Update test fixtures to include `tasks` field with sample child IDs\n- [ ] Tests pass with real API response structure\n- [ ] Update CHANGELOG.md noting the breaking change","status":"in_progress","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-18T14:32:44.748293162+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-18T14:34:44.774842767+01:00"} +{"id":"checkvist-api-rcj","title":"Fix Task.tasks field: expect []int not []*Task","description":"## Problem\n\nWhen decoding API responses containing the `tasks` field, JSON unmarshaling fails with:\n```\njson: cannot unmarshal number into Go struct field Task.tasks of type checkvist.Task\n```\n\n## Root Cause\n\nIn `models.go:144`, the `Children` field is incorrectly typed:\n```go\nChildren []*Task `json:\"tasks,omitempty\"`\n```\n\nAccording to the Checkvist API documentation, the `tasks` field is:\n\u003e \"tasks [JSON] Javascript array of children task IDs\"\n\nThe API returns an **array of integers** (child task IDs), not an array of full Task objects.\n\n## Solution\n\nChange the field type from `[]*Task` to `[]int` and rename appropriately:\n```go\nChildIDs []int `json:\"tasks,omitempty\"` // IDs of child tasks\n```\n\nThis is a breaking change for users who relied on `Children []*Task`, but the current implementation was incorrect and would fail on real API responses anyway.\n\n## Acceptance Criteria\n\n- [ ] `Task.ChildIDs` correctly unmarshals as `[]int`\n- [ ] Update test fixtures to include `tasks` field with sample child IDs\n- [ ] Tests pass with real API response structure\n- [ ] Update CHANGELOG.md noting the breaking change","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-18T14:32:44.748293162+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-18T14:42:22.954414951+01:00","closed_at":"2026-01-18T14:42:22.954414951+01:00","close_reason":"Fixed Task.tasks field type: renamed Children []*Task to ChildIDs []int"} {"id":"checkvist-api-rl9","title":"Implement Task operations","description":"Create tasks.go with TaskService:\n- client.Tasks(checklistID) returns TaskService\n- List(ctx) ([]Task, error) - GET /checklists/{id}/tasks.json\n- Get(ctx, taskID) (*Task, error) - GET /checklists/{id}/tasks/{task_id}.json (includes parent hierarchy)\n- Create(ctx, builder *TaskBuilder) (*Task, error) - POST /checklists/{id}/tasks.json\n- Update(ctx, taskID, opts) (*Task, error) - PUT /checklists/{id}/tasks/{task_id}.json\n- Delete(ctx, taskID) error - DELETE /checklists/{id}/tasks/{task_id}.json\n- Close(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/close.json\n- Reopen(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/reopen.json\n- Invalidate(ctx, taskID) (*Task, error) - POST /checklists/{id}/tasks/{task_id}/invalidate.json\nParse DueDate from DueDateRaw when retrieving tasks.","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:53.90629838+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:39:29.443727155+01:00","closed_at":"2026-01-14T13:39:29.443727155+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-8u6","type":"blocks","created_at":"2026-01-14T12:32:55.247292478+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"checkvist-api-rl9","depends_on_id":"checkvist-api-lpn","type":"blocks","created_at":"2026-01-14T12:32:55.536931433+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-tjk","title":"Implement TaskBuilder fluent interface","description":"Enhance task creation with builder pattern:\n- NewTask(content string) *TaskBuilder\n- WithTags(tags ...string) *TaskBuilder\n- WithDueDate(due DueDate) *TaskBuilder\n- WithPriority(p int) *TaskBuilder\n- WithParent(parentID int64) *TaskBuilder\n- WithPosition(pos int) *TaskBuilder\n- Build() returns parameters for API call\nTaskBuilder should be chainable and return itself for fluent usage:\n checkvist.NewTask(\"Content\").WithTags(\"tag1\").WithDueDate(checkvist.DueTomorrow).WithPriority(1)","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:30:55.929907579+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T14:30:42.405125498+01:00","closed_at":"2026-01-14T14:30:42.405125498+01:00","close_reason":"TaskBuilder war bereits vollständig implementiert und getestet","dependencies":[{"issue_id":"checkvist-api-tjk","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:02.20339206+01:00","created_by":"Oliver Jakoubek"}]} {"id":"checkvist-api-v2f","title":"Write unit tests for Tasks","description":"Create tasks_test.go with tests:\n- TestTasks_List\n- TestTasks_Get\n- TestTasks_Get_WithParentHierarchy\n- TestTasks_Create\n- TestTasks_Create_WithBuilder\n- TestTasks_Update\n- TestTasks_Delete\n- TestTasks_Close\n- TestTasks_Reopen\n- TestTasks_Invalidate\n- TestDueDate_Parsing\nUse table-driven tests. Create testdata/tasks/ fixtures.","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-14T12:31:37.538754679+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-14T13:46:01.50434559+01:00","closed_at":"2026-01-14T13:46:01.50434559+01:00","close_reason":"Closed","dependencies":[{"issue_id":"checkvist-api-v2f","depends_on_id":"checkvist-api-rl9","type":"blocks","created_at":"2026-01-14T12:33:13.81085058+01:00","created_by":"Oliver Jakoubek"}]} From 273ebbfa88393c6b3393b74c035c9175ba117f8f Mon Sep 17 00:00:00 2001 From: Oliver Jakoubek Date: Sun, 18 Jan 2026 14:48:56 +0100 Subject: [PATCH 8/8] docs(changelog): Release v1.0.3 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dff998a..5a1c7ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.3] - 2026-01-18 + ### Changed - **BREAKING**: `Task.Children []*Task` renamed to `Task.ChildIDs []int` to match API response format (API returns array of child task IDs, not full task objects)