Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@

# stray binary from `go build ./internal/cmd/gen` at repo root
/gen

# Local-only scratch: editor/agent state and working notes
docs/superpowers/
.claude/
109 changes: 109 additions & 0 deletions flashduty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package flashduty

import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
Expand Down Expand Up @@ -133,6 +134,114 @@ func TestResetPostMortemContentSendsZeroExpectedRevision(t *testing.T) {
}
}

func TestIncidentNotificationOverridePreservesExplicitFalse(t *testing.T) {
c, _ := NewClient("KEY", WithBaseURL("https://api.flashcat.cloud"), WithLogger(noopLogger{}))

tests := []struct {
name string
path string
body any
notifyPath []string
}{
{
name: "create incident",
path: "/incident/create",
body: &CreateIncidentRequest{
IncidentSeverity: "Critical",
AssignedTo: CreateIncidentRequestAssignedTo{
PersonIDs: []int64{1},
Notify: CreateIncidentRequestAssignedToNotify{
FollowPreference: Bool(false),
PersonalChannels: []string{"sms"},
},
},
},
notifyPath: []string{"assigned_to", "notify"},
},
{
name: "add responder",
path: "/incident/responder/add",
body: &AddIncidentResponderRequest{
IncidentID: "0123456789abcdef01234567",
PersonIDs: []int64{1},
Notify: AddIncidentResponderRequestNotify{
FollowPreference: Bool(false),
PersonalChannels: []string{"sms"},
},
},
notifyPath: []string{"notify"},
},
{
name: "assign incident",
path: "/incident/assign",
body: &AssignIncidentRequest{
IncidentID: "0123456789abcdef01234567",
AssignedTo: AssignedTo{
PersonIDs: []int64{1},
Notify: AssignedToNotify{
FollowPreference: Bool(false),
PersonalChannels: []string{"sms"},
},
},
},
notifyPath: []string{"assigned_to", "notify"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, err := c.newRequest(context.Background(), http.MethodPost, tt.path, tt.body)
if err != nil {
t.Fatal(err)
}
body, err := io.ReadAll(req.Body)
if err != nil {
t.Fatal(err)
}

var payload map[string]any
if err := json.Unmarshal(body, &payload); err != nil {
t.Fatal(err)
}
notify := payload
for _, key := range tt.notifyPath {
value, ok := notify[key].(map[string]any)
if !ok {
t.Fatalf("%s is missing from request body: %s", key, body)
}
notify = value
}
follow, ok := notify["follow_preference"]
if !ok || follow != false {
t.Fatalf("follow_preference = %#v, present = %t, body = %s", follow, ok, body)
}
})
}
}

func TestIncidentNotificationOverrideOmitsUnsetPreference(t *testing.T) {
c, _ := NewClient("KEY", WithBaseURL("https://api.flashcat.cloud"), WithLogger(noopLogger{}))
req, err := c.newRequest(context.Background(), http.MethodPost, "/incident/create", &CreateIncidentRequest{
IncidentSeverity: "Critical",
AssignedTo: CreateIncidentRequestAssignedTo{
PersonIDs: []int64{1},
Notify: CreateIncidentRequestAssignedToNotify{
PersonalChannels: []string{"sms"},
},
},
})
if err != nil {
t.Fatal(err)
}
body, err := io.ReadAll(req.Body)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(body), `"follow_preference"`) {
t.Fatalf("nil FollowPreference must be omitted from the wire, got body = %s", body)
}
}

func TestNewRequestAppliesHookAndHeaders(t *testing.T) {
c, _ := NewClient("KEY",
WithRequestHeaders(map[string][]string{"X-Static": {"s"}}),
Expand Down
19 changes: 19 additions & 0 deletions internal/cmd/gen/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,25 @@ func TestEmitStructRequiredNullableRequestScalarOmitsNil(t *testing.T) {
}
}

func TestEmitStructOptionalNullableRequestBoolPreservesFalse(t *testing.T) {
g := newTestGen(map[string]any{})
g.reqGoNames["NotifyRequest"] = true

schema := map[string]any{
"type": "object",
"properties": map[string]any{
"follow_preference": map[string]any{
"type": []any{"boolean", "null"},
},
},
}

src := g.emitStruct("NotifyRequest", schema)
if !strings.Contains(src, `FollowPreference *bool `+"`"+`json:"follow_preference,omitempty" toon:"follow_preference,omitempty"`+"`") {
t.Fatalf("optional nullable request bool must preserve explicit false; got:\n%s", src)
}
}

func TestMergeAllOfKeepsRequiredRequestFields(t *testing.T) {
g := newTestGen(map[string]any{
"BaseRequest": map[string]any{
Expand Down
32 changes: 28 additions & 4 deletions models_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 35 additions & 4 deletions openapi/openapi.en.json
Original file line number Diff line number Diff line change
Expand Up @@ -31156,8 +31156,11 @@
"description": "Override the notification channels used for this assignment.",
"properties": {
"follow_preference": {
"type": "boolean",
"description": "When true, fall back to each responder's personal preference."
"type": [
"boolean",
"null"
],
"description": "When false, use `personal_channels`; when true or omitted, use each responder's personal preference."
},
"personal_channels": {
"type": "array",
Expand Down Expand Up @@ -31256,6 +31259,31 @@
"maxItems": 100,
"description": "Email recipients, used by integrations such as ServiceNow."
},
"notify": {
"type": "object",
"description": "Override the notification channels used for this assignment.",
"properties": {
"follow_preference": {
"type": [
"boolean",
"null"
],
"description": "When false, use `personal_channels`; when true or omitted, use each responder's personal preference."
},
"personal_channels": {
"type": "array",
"items": {
"type": "string"
},
"description": "Channels to use (e.g. `voice`, `sms`, `email`)."
},
"template_id": {
"type": "string",
"pattern": "^[0-9a-fA-F]{24}$",
"description": "Notification template ID (MongoDB ObjectID)."
}
}
},
"escalate_rule_name": {
"type": "string",
"description": "Escalation rule display name, filled by the server."
Expand Down Expand Up @@ -33632,8 +33660,11 @@
"description": "Optional notification override. Defaults to following each person's personal preference.",
"properties": {
"follow_preference": {
"type": "boolean",
"description": "When true, fall back to each responder's personal preference."
"type": [
"boolean",
"null"
],
"description": "When false, use `personal_channels`; when true or omitted, use each responder's personal preference."
},
"personal_channels": {
"type": "array",
Expand Down
39 changes: 35 additions & 4 deletions openapi/openapi.zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -31148,8 +31148,11 @@
"description": "为本次指派覆盖的通知渠道。",
"properties": {
"follow_preference": {
"type": "boolean",
"description": "为 true 时跟随处理人员的个人偏好。"
"type": [
"boolean",
"null"
],
"description": "为 false 时使用 `personal_channels`;为 true 或省略时跟随处理人员的个人偏好。"
},
"personal_channels": {
"type": "array",
Expand Down Expand Up @@ -31248,6 +31251,31 @@
"maxItems": 100,
"description": "邮箱接收人列表,用于 ServiceNow 等外部系统。"
},
"notify": {
"type": "object",
"description": "为本次指派覆盖通知渠道。",
"properties": {
"follow_preference": {
"type": [
"boolean",
"null"
],
"description": "为 false 时使用 `personal_channels`;为 true 或省略时跟随处理人员的个人偏好。"
},
"personal_channels": {
"type": "array",
"items": {
"type": "string"
},
"description": "使用的通知渠道(如 `voice`、`sms`、`email`)。"
},
"template_id": {
"type": "string",
"pattern": "^[0-9a-fA-F]{24}$",
"description": "通知模板 ID(MongoDB ObjectID)。"
}
}
},
"escalate_rule_name": {
"type": "string",
"description": "分派策略名称,由服务端填充。"
Expand Down Expand Up @@ -33623,8 +33651,11 @@
"description": "通知配置,默认跟随每人的个人通知偏好。",
"properties": {
"follow_preference": {
"type": "boolean",
"description": "为 true 时跟随处理人员的个人偏好。"
"type": [
"boolean",
"null"
],
"description": "为 false 时使用 `personal_channels`;为 true 或省略时跟随处理人员的个人偏好。"
},
"personal_channels": {
"type": "array",
Expand Down