From 1e32e12145632f81b5e18e91f94853df05639a9c Mon Sep 17 00:00:00 2001 From: zhangqing <1770666340@qq.com> Date: Tue, 2 Jun 2026 15:08:53 +0800 Subject: [PATCH 1/3] feat: add notification module + fix issue --label bug - Add notification +list, +view, +read, +delete commands - Add notification unit tests - Register notification module in register.go - Fix issue +create ignoring --label flag Co-Authored-By: Claude Opus 4.6 --- doc/changes/notification-shortcut.md | 12 +++ shortcuts/issue/issue.go | 3 + shortcuts/notification/notification.go | 112 ++++++++++++++++++++ shortcuts/notification/notification_test.go | 110 +++++++++++++++++++ shortcuts/register.go | 3 + 5 files changed, 240 insertions(+) create mode 100644 doc/changes/notification-shortcut.md create mode 100644 shortcuts/notification/notification.go create mode 100644 shortcuts/notification/notification_test.go diff --git a/doc/changes/notification-shortcut.md b/doc/changes/notification-shortcut.md new file mode 100644 index 0000000..7850c6b --- /dev/null +++ b/doc/changes/notification-shortcut.md @@ -0,0 +1,12 @@ +# notification shortcut + +新增 `notification` 命令组,支持用户通知管理: + +| 命令 | 功能 | +|------|------| +| `notification +list` | 列出通知列表 | +| `notification +view --id ` | 查看通知详情 | +| `notification +read --id ` | 标记通知已读 | +| `notification +delete --id ` | 删除通知 | + +修复:`issue +create` 命令的 `--label` 参数现在会正确传入请求 body。 diff --git a/shortcuts/issue/issue.go b/shortcuts/issue/issue.go index 7c0bc68..48a70e4 100644 --- a/shortcuts/issue/issue.go +++ b/shortcuts/issue/issue.go @@ -82,6 +82,9 @@ func Shortcuts() []*common.Shortcut { if m := ctx.Arg("milestone"); m != "" { body["fixed_version_id"] = m } + if l := ctx.Arg("label"); l != "" { + body["label_id"] = l + } env, err := ctx.CallAPI("POST", v1RepoPath(ctx)+"/issues", body) if err != nil { return err diff --git a/shortcuts/notification/notification.go b/shortcuts/notification/notification.go new file mode 100644 index 0000000..003a6a9 --- /dev/null +++ b/shortcuts/notification/notification.go @@ -0,0 +1,112 @@ +package notification + +import ( + "fmt" + "net/url" + + "github.com/gitlink-org/gitlink-cli/cmd/cmdutil" + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +// Shortcuts returns notification management shortcuts. +func Shortcuts() []*common.Shortcut { + return []*common.Shortcut{ + { + Name: "list", + Description: "List notifications", + Flags: []common.Flag{ + {Name: "page", Short: "p", Usage: "Page number", Default: "1"}, + {Name: "limit", Short: "l", Usage: "Items per page", Default: "20"}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := resolveLogin() + if err != nil { + return err + } + q := url.Values{} + q.Set("page", ctx.Arg("page")) + q.Set("limit", ctx.Arg("limit")) + env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("/users/%s/messages", login), q) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "view", + Description: "View notification details", + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: "Notification ID", Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := resolveLogin() + if err != nil { + return err + } + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s/messages/%s", login, id), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "read", + Description: "Mark a notification as read", + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: "Notification ID", Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := resolveLogin() + if err != nil { + return err + } + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + env, err := ctx.CallAPI("PATCH", fmt.Sprintf("/users/%s/messages/%s", login, id), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "delete", + Description: "Delete a notification", + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: "Notification ID", Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + login, err := resolveLogin() + if err != nil { + return err + } + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + env, err := ctx.CallAPI("DELETE", fmt.Sprintf("/users/%s/messages/%s", login, id), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + } +} + +// resolveLogin returns the current user login from global flags. +func resolveLogin() (string, error) { + login := cmdutil.Owner + if login == "" { + return "", fmt.Errorf("provide --owner (your login) or set it via config") + } + return login, nil +} diff --git a/shortcuts/notification/notification_test.go b/shortcuts/notification/notification_test.go new file mode 100644 index 0000000..51f5c9c --- /dev/null +++ b/shortcuts/notification/notification_test.go @@ -0,0 +1,110 @@ +package notification + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gitlink-org/gitlink-cli/internal/client" + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +func TestNotificationList(t *testing.T) { + server := newNotificationTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "GET", "/users/testuser/messages") + writeJSON(t, w, map[string]interface{}{"total_count": 1, "messages": []interface{}{}}) + }) + defer server.Close() + + if err := runNotificationShortcut(t, server, "list", nil); err != nil { + t.Fatalf("list shortcut failed: %v", err) + } +} + +func TestNotificationView(t *testing.T) { + server := newNotificationTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "GET", "/users/testuser/messages/42") + writeJSON(t, w, map[string]interface{}{"id": 42, "subject": "test notification"}) + }) + defer server.Close() + + if err := runNotificationShortcut(t, server, "view", map[string]string{"id": "42"}); err != nil { + t.Fatalf("view shortcut failed: %v", err) + } +} + +func TestNotificationRead(t *testing.T) { + server := newNotificationTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "PATCH", "/users/testuser/messages/42") + writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"}) + }) + defer server.Close() + + if err := runNotificationShortcut(t, server, "read", map[string]string{"id": "42"}); err != nil { + t.Fatalf("read shortcut failed: %v", err) + } +} + +func TestNotificationDelete(t *testing.T) { + server := newNotificationTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "DELETE", "/users/testuser/messages/42") + writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"}) + }) + defer server.Close() + + if err := runNotificationShortcut(t, server, "delete", map[string]string{"id": "42"}); err != nil { + t.Fatalf("delete shortcut failed: %v", err) + } +} + +// --- test helpers --- + +func runNotificationShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error { + t.Helper() + shortcut := findNotificationShortcut(t, name) + ctx := &common.RuntimeContext{ + Client: &client.Client{ + HTTP: server.Client(), + BaseURL: server.URL, + }, + Owner: "testuser", + Format: "json", + Args: args, + } + if ctx.Args == nil { + ctx.Args = map[string]string{} + } + return shortcut.Run(ctx) +} + +func findNotificationShortcut(t *testing.T, name string) *common.Shortcut { + t.Helper() + for _, shortcut := range Shortcuts() { + if shortcut.Name == name { + return shortcut + } + } + t.Fatalf("shortcut %q not found", name) + return nil +} + +func newNotificationTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server { + t.Helper() + return httptest.NewServer(handler) +} + +func assertRequest(t *testing.T, r *http.Request, method, path string) { + t.Helper() + if r.Method != method || r.URL.Path != path { + t.Fatalf("got request %s %s, want %s %s", r.Method, r.URL.Path, method, path) + } +} + +func writeJSON(t *testing.T, w http.ResponseWriter, payload interface{}) { + t.Helper() + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(payload); err != nil { + t.Fatalf("failed to write response: %v", err) + } +} diff --git a/shortcuts/register.go b/shortcuts/register.go index 3b3d8be..bc42996 100644 --- a/shortcuts/register.go +++ b/shortcuts/register.go @@ -10,6 +10,7 @@ import ( "github.com/gitlink-org/gitlink-cli/shortcuts/compare" "github.com/gitlink-org/gitlink-cli/shortcuts/issue" "github.com/gitlink-org/gitlink-cli/shortcuts/member" + "github.com/gitlink-org/gitlink-cli/shortcuts/notification" "github.com/gitlink-org/gitlink-cli/shortcuts/milestone" "github.com/gitlink-org/gitlink-cli/shortcuts/org" "github.com/gitlink-org/gitlink-cli/shortcuts/pr" @@ -41,6 +42,7 @@ func RegisterAll(root *cobra.Command) { "workflow": workflow.Shortcuts(), "wiki": wiki.Shortcuts(), "label": label.Shortcuts(), + "notification": notification.Shortcuts(), } descriptions := map[string]string{ @@ -60,6 +62,7 @@ func RegisterAll(root *cobra.Command) { "workflow": "AI agent workflow analysis", "wiki": "Wiki page operations", "label": "Label operations", + "notification": "Notification operations", } for name, shortcuts := range groups { From dee04432d2273f00cc65f9377d017e54d262bbe7 Mon Sep 17 00:00:00 2001 From: zhangqing <1770666340@qq.com> Date: Tue, 2 Jun 2026 15:21:12 +0800 Subject: [PATCH 2/3] fix: use ctx.Owner instead of global cmdutil.Owner in notification Tests set Owner on RuntimeContext, not on the global cmdutil variable. This fixes all 4 notification test failures. Co-Authored-By: Claude Opus 4.6 --- shortcuts/notification/notification.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/shortcuts/notification/notification.go b/shortcuts/notification/notification.go index 003a6a9..eb9a14d 100644 --- a/shortcuts/notification/notification.go +++ b/shortcuts/notification/notification.go @@ -4,7 +4,6 @@ import ( "fmt" "net/url" - "github.com/gitlink-org/gitlink-cli/cmd/cmdutil" "github.com/gitlink-org/gitlink-cli/shortcuts/common" ) @@ -19,7 +18,7 @@ func Shortcuts() []*common.Shortcut { {Name: "limit", Short: "l", Usage: "Items per page", Default: "20"}, }, Run: func(ctx *common.RuntimeContext) error { - login, err := resolveLogin() + login, err := resolveLogin(ctx) if err != nil { return err } @@ -40,7 +39,7 @@ func Shortcuts() []*common.Shortcut { {Name: "id", Short: "i", Usage: "Notification ID", Required: true}, }, Run: func(ctx *common.RuntimeContext) error { - login, err := resolveLogin() + login, err := resolveLogin(ctx) if err != nil { return err } @@ -62,7 +61,7 @@ func Shortcuts() []*common.Shortcut { {Name: "id", Short: "i", Usage: "Notification ID", Required: true}, }, Run: func(ctx *common.RuntimeContext) error { - login, err := resolveLogin() + login, err := resolveLogin(ctx) if err != nil { return err } @@ -84,7 +83,7 @@ func Shortcuts() []*common.Shortcut { {Name: "id", Short: "i", Usage: "Notification ID", Required: true}, }, Run: func(ctx *common.RuntimeContext) error { - login, err := resolveLogin() + login, err := resolveLogin(ctx) if err != nil { return err } @@ -102,11 +101,10 @@ func Shortcuts() []*common.Shortcut { } } -// resolveLogin returns the current user login from global flags. -func resolveLogin() (string, error) { - login := cmdutil.Owner - if login == "" { +// resolveLogin returns the user login from the runtime context. +func resolveLogin(ctx *common.RuntimeContext) (string, error) { + if ctx.Owner == "" { return "", fmt.Errorf("provide --owner (your login) or set it via config") } - return login, nil + return ctx.Owner, nil } From 263c20801a879a6e110436e01703335b229a2692 Mon Sep 17 00:00:00 2001 From: zhangqing <1770666340@qq.com> Date: Tue, 2 Jun 2026 15:24:15 +0800 Subject: [PATCH 3/3] fix: update notification test paths to include .json suffix The HTTP client automatically appends .json to API paths. Co-Authored-By: Claude Opus 4.6 --- shortcuts/notification/notification_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shortcuts/notification/notification_test.go b/shortcuts/notification/notification_test.go index 51f5c9c..4466c62 100644 --- a/shortcuts/notification/notification_test.go +++ b/shortcuts/notification/notification_test.go @@ -12,7 +12,7 @@ import ( func TestNotificationList(t *testing.T) { server := newNotificationTestServer(t, func(w http.ResponseWriter, r *http.Request) { - assertRequest(t, r, "GET", "/users/testuser/messages") + assertRequest(t, r, "GET", "/users/testuser/messages.json") writeJSON(t, w, map[string]interface{}{"total_count": 1, "messages": []interface{}{}}) }) defer server.Close() @@ -24,7 +24,7 @@ func TestNotificationList(t *testing.T) { func TestNotificationView(t *testing.T) { server := newNotificationTestServer(t, func(w http.ResponseWriter, r *http.Request) { - assertRequest(t, r, "GET", "/users/testuser/messages/42") + assertRequest(t, r, "GET", "/users/testuser/messages/42.json") writeJSON(t, w, map[string]interface{}{"id": 42, "subject": "test notification"}) }) defer server.Close() @@ -36,7 +36,7 @@ func TestNotificationView(t *testing.T) { func TestNotificationRead(t *testing.T) { server := newNotificationTestServer(t, func(w http.ResponseWriter, r *http.Request) { - assertRequest(t, r, "PATCH", "/users/testuser/messages/42") + assertRequest(t, r, "PATCH", "/users/testuser/messages/42.json") writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"}) }) defer server.Close() @@ -48,7 +48,7 @@ func TestNotificationRead(t *testing.T) { func TestNotificationDelete(t *testing.T) { server := newNotificationTestServer(t, func(w http.ResponseWriter, r *http.Request) { - assertRequest(t, r, "DELETE", "/users/testuser/messages/42") + assertRequest(t, r, "DELETE", "/users/testuser/messages/42.json") writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"}) }) defer server.Close()