Skip to content

Commit dff1254

Browse files
authored
Default check-ins answer date to today (#376)
* Default checkins answer date to today * Address PR review feedback * Fix check-ins answer date follow-ups
1 parent da54d0c commit dff1254

5 files changed

Lines changed: 201 additions & 4 deletions

File tree

internal/commands/checkins.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"strconv"
66
"strings"
7+
"time"
78

89
"github.com/basecamp/basecamp-sdk/go/pkg/basecamp"
910
"github.com/spf13/cobra"
@@ -13,6 +14,8 @@ import (
1314
"github.com/basecamp/basecamp-cli/internal/richtext"
1415
)
1516

17+
var checkinsNow = time.Now
18+
1619
// NewCheckinsCmd creates the checkins command group.
1720
func NewCheckinsCmd() *cobra.Command {
1821
var project string
@@ -774,6 +777,10 @@ func newCheckinsAnswerCreateCmd(project *string) *cobra.Command {
774777
if err != nil {
775778
return output.ErrUsage("Invalid question ID")
776779
}
780+
effectiveGroupOn := groupOn
781+
if effectiveGroupOn == "" {
782+
effectiveGroupOn = checkinsNow().Format("2006-01-02")
783+
}
777784

778785
html := richtext.MarkdownToHTML(content)
779786

@@ -794,7 +801,7 @@ func newCheckinsAnswerCreateCmd(project *string) *cobra.Command {
794801

795802
req := &basecamp.CreateAnswerRequest{
796803
Content: html,
797-
GroupOn: groupOn,
804+
GroupOn: effectiveGroupOn,
798805
}
799806

800807
answer, err := app.Account().Checkins().CreateAnswer(cmd.Context(), qID, req)
@@ -825,7 +832,7 @@ func newCheckinsAnswerCreateCmd(project *string) *cobra.Command {
825832
},
826833
}
827834

828-
cmd.Flags().StringVar(&groupOn, "date", "", "Date to group answer (ISO 8601, e.g., 2024-01-22)")
835+
cmd.Flags().StringVar(&groupOn, "date", "", "Date to group answer (ISO 8601, e.g., 2024-01-22; defaults to today)")
829836
cmd.Flags().StringArrayVar(&attachFiles, "attach", nil, "Attach file (repeatable)")
830837

831838
return cmd

internal/commands/checkins_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package commands
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"net/http"
7+
"strings"
8+
"testing"
9+
"time"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
type mockCheckinsAnswerCreateTransport struct {
16+
recordedPath string
17+
recordedBody map[string]any
18+
}
19+
20+
func (m *mockCheckinsAnswerCreateTransport) RoundTrip(req *http.Request) (*http.Response, error) {
21+
header := make(http.Header)
22+
header.Set("Content-Type", "application/json")
23+
24+
switch {
25+
case req.Method == "GET" && strings.Contains(req.URL.Path, "/projects.json"):
26+
return &http.Response{
27+
StatusCode: 200,
28+
Body: io.NopCloser(strings.NewReader(`[{"id":123,"name":"Test Project"}]`)),
29+
Header: header,
30+
}, nil
31+
case req.Method == "POST" && strings.Contains(req.URL.Path, "/questions/456/answers.json"):
32+
m.recordedPath = req.URL.Path
33+
if req.Body != nil {
34+
defer req.Body.Close()
35+
}
36+
body, err := io.ReadAll(req.Body)
37+
if err != nil {
38+
return nil, err
39+
}
40+
if err := json.Unmarshal(body, &m.recordedBody); err != nil {
41+
return nil, err
42+
}
43+
return &http.Response{
44+
StatusCode: 201,
45+
Body: io.NopCloser(strings.NewReader(`{
46+
"id": 789,
47+
"content": "<p>hello world</p>",
48+
"group_on": "2026-03-25",
49+
"creator": {"name": "Rob Zolkos"},
50+
"parent": {"id": 456, "title": "What did you work on today?", "type": "Question", "url": "https://example.test/questions/456", "app_url": "https://example.test/questions/456"},
51+
"bucket": {"id": 123, "name": "Test Project", "type": "Project"},
52+
"status": "active",
53+
"type": "Question::Answer",
54+
"title": "Answer"
55+
}`)),
56+
Header: header,
57+
}, nil
58+
default:
59+
return &http.Response{
60+
StatusCode: 404,
61+
Body: io.NopCloser(strings.NewReader(`{"error":"Not Found"}`)),
62+
Header: header,
63+
}, nil
64+
}
65+
}
66+
67+
func TestCheckinsAnswerCreateDefaultsDateToToday(t *testing.T) {
68+
originalNow := checkinsNow
69+
checkinsNow = func() time.Time {
70+
return time.Date(2026, 3, 25, 9, 30, 0, 0, time.Local)
71+
}
72+
t.Cleanup(func() {
73+
checkinsNow = originalNow
74+
})
75+
76+
transport := &mockCheckinsAnswerCreateTransport{}
77+
app, _ := newTestAppWithTransport(t, transport)
78+
app.Config.ProjectID = "123"
79+
80+
project := ""
81+
cmd := newCheckinsAnswerCreateCmd(&project)
82+
83+
err := executeCommand(cmd, app, "456", "hello world")
84+
require.NoError(t, err)
85+
require.NotNil(t, transport.recordedBody)
86+
assert.Equal(t, "/99999/questions/456/answers.json", transport.recordedPath)
87+
assert.Equal(t, "<p>hello world</p>", transport.recordedBody["content"])
88+
assert.Equal(t, "2026-03-25", transport.recordedBody["group_on"])
89+
}
90+
91+
func TestCheckinsAnswerCreatePreservesExplicitDate(t *testing.T) {
92+
transport := &mockCheckinsAnswerCreateTransport{}
93+
app, _ := newTestAppWithTransport(t, transport)
94+
app.Config.ProjectID = "123"
95+
96+
project := ""
97+
cmd := newCheckinsAnswerCreateCmd(&project)
98+
99+
err := executeCommand(cmd, app, "456", "hello world", "--date", "2026-03-25")
100+
require.NoError(t, err)
101+
require.NotNil(t, transport.recordedBody)
102+
assert.Equal(t, "2026-03-25", transport.recordedBody["group_on"])
103+
}

internal/tui/workspace/data/hub.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"github.com/basecamp/basecamp-sdk/go/pkg/basecamp"
1111
)
1212

13+
var hubNow = time.Now
14+
1315
// Hub is the central data coordinator providing typed, realm-scoped pool access.
1416
//
1517
// Hub manages three realm tiers:
@@ -1160,7 +1162,10 @@ func (h *Hub) CreateCheckinAnswer(ctx context.Context, accountID string, project
11601162
if client == nil {
11611163
return fmt.Errorf("no client for account %s", accountID)
11621164
}
1163-
_, err := client.Checkins().CreateAnswer(ctx, questionID, &basecamp.CreateAnswerRequest{Content: content})
1165+
_, err := client.Checkins().CreateAnswer(ctx, questionID, &basecamp.CreateAnswerRequest{
1166+
Content: content,
1167+
GroupOn: hubNow().Format("2006-01-02"),
1168+
})
11641169
return err
11651170
}
11661171

internal/tui/workspace/data/hub_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package data
22

33
import (
4+
"context"
5+
"encoding/json"
6+
"io"
7+
"net/http"
8+
"strings"
49
"testing"
510
"time"
611

@@ -9,6 +14,82 @@ import (
914
"github.com/stretchr/testify/require"
1015
)
1116

17+
type hubCheckinsTestTokenProvider struct{}
18+
19+
func (hubCheckinsTestTokenProvider) AccessToken(_ context.Context) (string, error) {
20+
return "test-token", nil
21+
}
22+
23+
type mockHubCheckinsTransport struct {
24+
recordedPath string
25+
recordedBody map[string]any
26+
}
27+
28+
func (m *mockHubCheckinsTransport) RoundTrip(req *http.Request) (*http.Response, error) {
29+
header := make(http.Header)
30+
header.Set("Content-Type", "application/json")
31+
32+
switch {
33+
case req.Method == http.MethodPost && strings.Contains(req.URL.Path, "/questions/456/answers.json"):
34+
m.recordedPath = req.URL.Path
35+
if req.Body != nil {
36+
defer req.Body.Close()
37+
}
38+
body, err := io.ReadAll(req.Body)
39+
if err != nil {
40+
return nil, err
41+
}
42+
if err := json.Unmarshal(body, &m.recordedBody); err != nil {
43+
return nil, err
44+
}
45+
return &http.Response{
46+
StatusCode: http.StatusCreated,
47+
Body: io.NopCloser(strings.NewReader(`{
48+
"id": 789,
49+
"content": "<p>hello world</p>",
50+
"group_on": "2026-03-25",
51+
"creator": {"name": "Rob Zolkos"},
52+
"parent": {"id": 456, "title": "What did you work on today?", "type": "Question", "url": "https://example.test/questions/456", "app_url": "https://example.test/questions/456"},
53+
"bucket": {"id": 123, "name": "Test Project", "type": "Project"},
54+
"status": "active",
55+
"type": "Question::Answer",
56+
"title": "Answer"
57+
}`)),
58+
Header: header,
59+
}, nil
60+
default:
61+
return &http.Response{
62+
StatusCode: http.StatusNotFound,
63+
Body: io.NopCloser(strings.NewReader(`{"error":"Not Found"}`)),
64+
Header: header,
65+
}, nil
66+
}
67+
}
68+
69+
func TestHubCreateCheckinAnswerDefaultsDateToToday(t *testing.T) {
70+
originalNow := hubNow
71+
hubNow = func() time.Time {
72+
return time.Date(2026, 3, 25, 9, 30, 0, 0, time.Local)
73+
}
74+
t.Cleanup(func() {
75+
hubNow = originalNow
76+
})
77+
78+
transport := &mockHubCheckinsTransport{}
79+
sdk := basecamp.NewClient(&basecamp.Config{}, hubCheckinsTestTokenProvider{},
80+
basecamp.WithTransport(transport),
81+
basecamp.WithMaxRetries(0),
82+
)
83+
h := NewHub(NewMultiStore(sdk), "")
84+
85+
err := h.CreateCheckinAnswer(context.Background(), "99999", 123, 456, "<p>hello world</p>")
86+
require.NoError(t, err)
87+
require.NotNil(t, transport.recordedBody)
88+
assert.Equal(t, "/99999/questions/456/answers.json", transport.recordedPath)
89+
assert.Equal(t, "<p>hello world</p>", transport.recordedBody["content"])
90+
assert.Equal(t, "2026-03-25", transport.recordedBody["group_on"])
91+
}
92+
1293
func TestHubNewHasGlobalRealm(t *testing.T) {
1394
h := NewHub(nil, "")
1495
require.NotNil(t, h.Global())

skills/basecamp/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,7 @@ basecamp checkins answers <question_id> --in <project> # List answers
565565
basecamp checkins answer <id> --in <project> # Answer details
566566
basecamp checkins question create "What did you work on?" --in <project>
567567
basecamp checkins question update <id> "New question" --frequency every_week
568-
basecamp checkins answer create <question-id> "My answer" --in <project>
568+
basecamp checkins answer create <question-id> "My answer" --in <project> # Defaults to today
569569
basecamp checkins answer update <id> "Updated" --in <project>
570570
```
571571

@@ -845,6 +845,7 @@ cat ~/.config/basecamp/accounts.json # Check available accounts
845845
- `basecamp comment <id> "Text"` (not a flag)
846846
- `basecamp webhooks create "https://..." --in <project>` (not `--url`)
847847
- `basecamp checkins answer create <question-id> "content"` (not `--question`)
848+
- `--date YYYY-MM-DD` is optional for `checkins answer create`; if omitted, it defaults to today
848849

849850
**Missing argument errors (code: "usage"):**
850851
When a required positional argument is missing, the CLI returns a structured error naming

0 commit comments

Comments
 (0)