Skip to content

Commit f787a9d

Browse files
committed
refactor: move incident adapters to sdk
1 parent cb17a7f commit f787a9d

10 files changed

Lines changed: 218 additions & 731 deletions

AGENTS.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Agent Instructions
2+
3+
## Flashduty SDK Boundary
4+
5+
- Do not implement Flashduty public API endpoint clients directly in this CLI repository.
6+
- If a CLI command needs an endpoint that is missing from `github.com/flashcatcloud/flashduty-sdk`, add the typed adapter to `flashduty-sdk` first, with focused SDK tests.
7+
- The CLI should consume SDK methods and keep only command parsing, output formatting, and CLI-specific orchestration.
8+
- Existing raw HTTP adapters in the CLI are migration debt. Prefer removing them as SDK coverage catches up.

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@ module github.com/flashcatcloud/flashduty-cli
33
go 1.25.1
44

55
require (
6-
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260514094839-5405a3ab38b1
6+
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260520063928-ea6a81de4c95
77
github.com/mattn/go-runewidth v0.0.23
88
github.com/spf13/cobra v1.10.2
9+
github.com/spf13/pflag v1.0.9
910
golang.org/x/term v0.42.0
1011
gopkg.in/yaml.v3 v3.0.1
1112
)
1213

1314
require (
1415
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
1516
github.com/inconshreveable/mousetrap v1.1.0 // indirect
16-
github.com/spf13/pflag v1.0.9 // indirect
1717
github.com/toon-format/toon-go v0.0.0-20251202084852-7ca0e27c4e8c // indirect
1818
golang.org/x/sync v0.19.0 // indirect
1919
golang.org/x/sys v0.43.0 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
22
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
33
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
4-
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260514094839-5405a3ab38b1 h1:Q9FJkGSAQCXCjjnjS18QYMNpHT8O27oRj9kd13tUeiI=
5-
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260514094839-5405a3ab38b1/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY=
4+
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260520063928-ea6a81de4c95 h1:b7O4LtOfecgmuq2Ipv6vkssHNFzqfUbo5utBuR8X4kI=
5+
github.com/flashcatcloud/flashduty-sdk v0.8.1-0.20260520063928-ea6a81de4c95/go.mod h1:dG4eJfdZaj4jNBMwEexbfK/3PmcIMhNeJ88L/DcZzUY=
66
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
77
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
88
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=

internal/cli/command_test.go

Lines changed: 105 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"testing"
1010

1111
flashduty "github.com/flashcatcloud/flashduty-sdk"
12+
"github.com/spf13/cobra"
13+
"github.com/spf13/pflag"
1214
)
1315

1416
// mockClient provides default "not implemented" stubs for all flashdutyClient
@@ -71,38 +73,42 @@ func (m *mockClient) DisableIncidentMerge(context.Context, []string) error {
7173
return fmt.Errorf("mockClient: DisableIncidentMerge not implemented")
7274
}
7375

74-
func (m *mockClient) CommentIncidents(context.Context, *IncidentCommentInput) error {
76+
func (m *mockClient) CommentIncidents(context.Context, *flashduty.IncidentCommentInput) error {
7577
return fmt.Errorf("mockClient: CommentIncidents not implemented")
7678
}
7779

78-
func (m *mockClient) AddIncidentResponders(context.Context, *IncidentAddResponderInput) error {
80+
func (m *mockClient) AddIncidentResponders(context.Context, *flashduty.IncidentAddResponderInput) error {
7981
return fmt.Errorf("mockClient: AddIncidentResponders not implemented")
8082
}
8183

82-
func (m *mockClient) CreateIncidentWarRoom(context.Context, *IncidentWarRoomCreateInput) (*IncidentWarRoom, error) {
84+
func (m *mockClient) CreateIncidentWarRoom(context.Context, *flashduty.IncidentWarRoomCreateInput) (*flashduty.IncidentWarRoom, error) {
8385
return nil, fmt.Errorf("mockClient: CreateIncidentWarRoom not implemented")
8486
}
8587

86-
func (m *mockClient) ListIncidentWarRooms(context.Context, *IncidentWarRoomListInput) (*IncidentWarRoomListOutput, error) {
88+
func (m *mockClient) ListIncidentWarRooms(context.Context, *flashduty.IncidentWarRoomListInput) (*flashduty.IncidentWarRoomListOutput, error) {
8789
return nil, fmt.Errorf("mockClient: ListIncidentWarRooms not implemented")
8890
}
8991

90-
func (m *mockClient) GetIncidentWarRoom(context.Context, *IncidentWarRoomDetailInput) (*IncidentWarRoom, error) {
92+
func (m *mockClient) GetIncidentWarRoom(context.Context, *flashduty.IncidentWarRoomDetailInput) (*flashduty.IncidentWarRoom, error) {
9193
return nil, fmt.Errorf("mockClient: GetIncidentWarRoom not implemented")
9294
}
9395

94-
func (m *mockClient) DeleteIncidentWarRoom(context.Context, *IncidentWarRoomDeleteInput) error {
96+
func (m *mockClient) DeleteIncidentWarRoom(context.Context, *flashduty.IncidentWarRoomDeleteInput) error {
9597
return fmt.Errorf("mockClient: DeleteIncidentWarRoom not implemented")
9698
}
9799

98-
func (m *mockClient) AddIncidentWarRoomMembers(context.Context, *IncidentWarRoomAddMemberInput) error {
100+
func (m *mockClient) AddIncidentWarRoomMembers(context.Context, *flashduty.IncidentWarRoomAddMemberInput) error {
99101
return fmt.Errorf("mockClient: AddIncidentWarRoomMembers not implemented")
100102
}
101103

102-
func (m *mockClient) GetIncidentWarRoomDefaultObservers(context.Context, string) ([]IncidentWarRoomObserver, error) {
104+
func (m *mockClient) GetIncidentWarRoomDefaultObservers(context.Context, string) ([]flashduty.IncidentWarRoomObserver, error) {
103105
return nil, fmt.Errorf("mockClient: GetIncidentWarRoomDefaultObservers not implemented")
104106
}
105107

108+
func (m *mockClient) ListWarRoomEnabledDataSources(context.Context) (*flashduty.ListWarRoomEnabledDataSourcesOutput, error) {
109+
return nil, fmt.Errorf("mockClient: ListWarRoomEnabledDataSources not implemented")
110+
}
111+
106112
func (m *mockClient) ListChannels(context.Context, *flashduty.ListChannelsInput) (*flashduty.ListChannelsOutput, error) {
107113
return nil, fmt.Errorf("mockClient: ListChannels not implemented")
108114
}
@@ -307,6 +313,8 @@ func saveAndResetGlobals(t *testing.T) {
307313
// and returns (stdout string, error). It also resets cobra flag state after
308314
// execution.
309315
func execCommand(args ...string) (string, error) {
316+
resetCommandFlags(rootCmd)
317+
310318
buf := new(bytes.Buffer)
311319
rootCmd.SetOut(buf)
312320
rootCmd.SetErr(buf)
@@ -319,10 +327,35 @@ func execCommand(args ...string) (string, error) {
319327
rootCmd.SetArgs(nil)
320328
rootCmd.SetOut(nil)
321329
rootCmd.SetErr(nil)
330+
resetCommandFlags(rootCmd)
322331

323332
return buf.String(), err
324333
}
325334

335+
func resetCommandFlags(cmd *cobra.Command) {
336+
if cmd == nil {
337+
return
338+
}
339+
resetFlagSet(cmd.Flags())
340+
resetFlagSet(cmd.PersistentFlags())
341+
for _, child := range cmd.Commands() {
342+
resetCommandFlags(child)
343+
}
344+
}
345+
346+
func resetFlagSet(flags *pflag.FlagSet) {
347+
if flags == nil {
348+
return
349+
}
350+
flags.VisitAll(func(flag *pflag.Flag) {
351+
switch flag.Value.Type() {
352+
case "bool", "int", "int64", "string":
353+
_ = flag.Value.Set(flag.DefValue)
354+
flag.Changed = false
355+
}
356+
})
357+
}
358+
326359
// ---------------------------------------------------------------------------
327360
// Test 191: incident get returns empty results
328361
// ---------------------------------------------------------------------------
@@ -665,8 +698,8 @@ type mockIncidentLifecycle struct {
665698
wakeIDs []string
666699
removeIDs []string
667700
disableMergeIDs []string
668-
commentInput *IncidentCommentInput
669-
responderInput *IncidentAddResponderInput
701+
commentInput *flashduty.IncidentCommentInput
702+
responderInput *flashduty.IncidentAddResponderInput
670703
}
671704

672705
func (m *mockIncidentLifecycle) UnackIncidents(_ context.Context, incidentIDs []string) error {
@@ -689,14 +722,14 @@ func (m *mockIncidentLifecycle) DisableIncidentMerge(_ context.Context, incident
689722
return nil
690723
}
691724

692-
func (m *mockIncidentLifecycle) CommentIncidents(_ context.Context, input *IncidentCommentInput) error {
725+
func (m *mockIncidentLifecycle) CommentIncidents(_ context.Context, input *flashduty.IncidentCommentInput) error {
693726
copied := *input
694727
copied.IncidentIDs = append([]string(nil), input.IncidentIDs...)
695728
m.commentInput = &copied
696729
return nil
697730
}
698731

699-
func (m *mockIncidentLifecycle) AddIncidentResponders(_ context.Context, input *IncidentAddResponderInput) error {
732+
func (m *mockIncidentLifecycle) AddIncidentResponders(_ context.Context, input *flashduty.IncidentAddResponderInput) error {
700733
copied := *input
701734
copied.PersonIDs = append([]int64(nil), input.PersonIDs...)
702735
if input.Notify != nil {
@@ -907,56 +940,61 @@ func TestCommandIncidentDisableMerge(t *testing.T) {
907940
type mockIncidentWarRoom struct {
908941
mockClient
909942

910-
createInput *IncidentWarRoomCreateInput
911-
listInput *IncidentWarRoomListInput
912-
getInput *IncidentWarRoomDetailInput
913-
deleteInput *IncidentWarRoomDeleteInput
914-
addMemberInput *IncidentWarRoomAddMemberInput
943+
createInput *flashduty.IncidentWarRoomCreateInput
944+
listInput *flashduty.IncidentWarRoomListInput
945+
getInput *flashduty.IncidentWarRoomDetailInput
946+
deleteInput *flashduty.IncidentWarRoomDeleteInput
947+
addMemberInput *flashduty.IncidentWarRoomAddMemberInput
915948
defaultObserverIncID string
916-
defaultObserverOutput []IncidentWarRoomObserver
949+
defaultObserverOutput []flashduty.IncidentWarRoomObserver
950+
enabledDataSources []flashduty.DataSourceIntegration
917951
}
918952

919-
func (m *mockIncidentWarRoom) CreateIncidentWarRoom(_ context.Context, input *IncidentWarRoomCreateInput) (*IncidentWarRoom, error) {
953+
func (m *mockIncidentWarRoom) CreateIncidentWarRoom(_ context.Context, input *flashduty.IncidentWarRoomCreateInput) (*flashduty.IncidentWarRoom, error) {
920954
copied := *input
921955
copied.MemberIDs = append([]int64(nil), input.MemberIDs...)
922956
m.createInput = &copied
923-
return &IncidentWarRoom{ChatID: "chat-1", ChatName: "INC outage", ShareLink: "https://chat.example/1"}, nil
957+
return &flashduty.IncidentWarRoom{ChatID: "chat-1", ChatName: "INC outage", ShareLink: "https://chat.example/1"}, nil
924958
}
925959

926-
func (m *mockIncidentWarRoom) ListIncidentWarRooms(_ context.Context, input *IncidentWarRoomListInput) (*IncidentWarRoomListOutput, error) {
960+
func (m *mockIncidentWarRoom) ListIncidentWarRooms(_ context.Context, input *flashduty.IncidentWarRoomListInput) (*flashduty.IncidentWarRoomListOutput, error) {
927961
copied := *input
928962
m.listInput = &copied
929-
return &IncidentWarRoomListOutput{
930-
Items: []IncidentWarRoomItem{
963+
return &flashduty.IncidentWarRoomListOutput{
964+
Items: []flashduty.IncidentWarRoomItem{
931965
{IntegrationID: 42, ChatID: "chat-1", IncidentID: "inc-1", Status: "enabled", PluginType: "feishu"},
932966
},
933967
}, nil
934968
}
935969

936-
func (m *mockIncidentWarRoom) GetIncidentWarRoom(_ context.Context, input *IncidentWarRoomDetailInput) (*IncidentWarRoom, error) {
970+
func (m *mockIncidentWarRoom) GetIncidentWarRoom(_ context.Context, input *flashduty.IncidentWarRoomDetailInput) (*flashduty.IncidentWarRoom, error) {
937971
copied := *input
938972
m.getInput = &copied
939-
return &IncidentWarRoom{ChatID: "chat-1", ChatName: "INC outage", ShareLink: "https://chat.example/1"}, nil
973+
return &flashduty.IncidentWarRoom{ChatID: "chat-1", ChatName: "INC outage", ShareLink: "https://chat.example/1"}, nil
940974
}
941975

942-
func (m *mockIncidentWarRoom) DeleteIncidentWarRoom(_ context.Context, input *IncidentWarRoomDeleteInput) error {
976+
func (m *mockIncidentWarRoom) DeleteIncidentWarRoom(_ context.Context, input *flashduty.IncidentWarRoomDeleteInput) error {
943977
copied := *input
944978
m.deleteInput = &copied
945979
return nil
946980
}
947981

948-
func (m *mockIncidentWarRoom) AddIncidentWarRoomMembers(_ context.Context, input *IncidentWarRoomAddMemberInput) error {
982+
func (m *mockIncidentWarRoom) AddIncidentWarRoomMembers(_ context.Context, input *flashduty.IncidentWarRoomAddMemberInput) error {
949983
copied := *input
950984
copied.MemberIDs = append([]int64(nil), input.MemberIDs...)
951985
m.addMemberInput = &copied
952986
return nil
953987
}
954988

955-
func (m *mockIncidentWarRoom) GetIncidentWarRoomDefaultObservers(_ context.Context, incidentID string) ([]IncidentWarRoomObserver, error) {
989+
func (m *mockIncidentWarRoom) GetIncidentWarRoomDefaultObservers(_ context.Context, incidentID string) ([]flashduty.IncidentWarRoomObserver, error) {
956990
m.defaultObserverIncID = incidentID
957991
return m.defaultObserverOutput, nil
958992
}
959993

994+
func (m *mockIncidentWarRoom) ListWarRoomEnabledDataSources(context.Context) (*flashduty.ListWarRoomEnabledDataSourcesOutput, error) {
995+
return &flashduty.ListWarRoomEnabledDataSourcesOutput{Items: m.enabledDataSources}, nil
996+
}
997+
960998
func TestCommandIncidentWarRoomCreateWithObservers(t *testing.T) {
961999
saveAndResetGlobals(t)
9621000
mock := &mockIncidentWarRoom{}
@@ -980,10 +1018,48 @@ func TestCommandIncidentWarRoomCreateWithObservers(t *testing.T) {
9801018
}
9811019
}
9821020

1021+
func TestCommandIncidentWarRoomCreateAutoDiscoversIntegration(t *testing.T) {
1022+
saveAndResetGlobals(t)
1023+
mock := &mockIncidentWarRoom{
1024+
enabledDataSources: []flashduty.DataSourceIntegration{
1025+
{DataSourceID: 42, Name: "Feishu", PluginType: "feishu_app"},
1026+
},
1027+
}
1028+
newClientFn = func() (flashdutyClient, error) { return mock, nil }
1029+
1030+
out, err := execCommand("incident", "war-room", "create", "inc-1", "--member", "101")
1031+
if err != nil {
1032+
t.Fatalf("[incident-war-room-create-autodiscover] unexpected error: %v", err)
1033+
}
1034+
if mock.createInput == nil {
1035+
t.Fatal("[incident-war-room-create-autodiscover] expected CreateIncidentWarRoom to be called")
1036+
}
1037+
if mock.createInput.IntegrationID != 42 {
1038+
t.Fatalf("[incident-war-room-create-autodiscover] expected integration 42, got %#v", mock.createInput)
1039+
}
1040+
if !strings.Contains(out, "War room created: chat-1") {
1041+
t.Fatalf("[incident-war-room-create-autodiscover] unexpected output:\n%s", out)
1042+
}
1043+
}
1044+
1045+
func TestCommandIncidentWarRoomCreateRequiresEnabledIntegration(t *testing.T) {
1046+
saveAndResetGlobals(t)
1047+
mock := &mockIncidentWarRoom{}
1048+
newClientFn = func() (flashdutyClient, error) { return mock, nil }
1049+
1050+
_, err := execCommand("incident", "war-room", "create", "inc-1")
1051+
if err == nil || !strings.Contains(err.Error(), "no IM integration has war-room enabled") {
1052+
t.Fatalf("[incident-war-room-create-no-enabled-integration] expected enabled integration error, got %v", err)
1053+
}
1054+
if mock.createInput != nil {
1055+
t.Fatalf("[incident-war-room-create-no-enabled-integration] did not expect create call: %#v", mock.createInput)
1056+
}
1057+
}
1058+
9831059
func TestCommandIncidentWarRoomDefaultObservers(t *testing.T) {
9841060
saveAndResetGlobals(t)
9851061
mock := &mockIncidentWarRoom{
986-
defaultObserverOutput: []IncidentWarRoomObserver{
1062+
defaultObserverOutput: []flashduty.IncidentWarRoomObserver{
9871063
{PersonID: 101, PersonName: "Alice", Email: "alice@example.com"},
9881064
},
9891065
}

0 commit comments

Comments
 (0)