✨ feat(spx): 按 git origin 支持 GitHub 仓库,state 评论改成隐藏注释格式,新增 issue view / pr view / pr create

This commit is contained in:
cruldra committed 2026-10-19 07:41:13 +11:00
1 parent 64d5961b62
commit c90d84d7f2
17 files changed
+2683 -400

No files matched your search

+241
View File
@@ -0,0 +1,241 @@
package forge_test
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/cruldra/superpowers-vscode/cli/internal/forge"
"github.com/cruldra/superpowers-vscode/cli/internal/forge/forgetest"
)
// flavors runs fn once against a fake Gitea and once against a fake GitHub, each wired to a matching client.
func flavors(t *testing.T, fn func(t *testing.T, srv *forgetest.Server, f forge.Forge)) {
cases := []struct {
name string
flavor forgetest.Flavor
}{
{"gitea", forgetest.Gitea},
{"github", forgetest.GitHub},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
srv := forgetest.NewServer(t, c.flavor)
fn(t, srv, clientFor(srv))
})
}
}
func clientFor(srv *forgetest.Server) forge.Forge {
if srv.Flavor == forgetest.GitHub {
return forge.NewGitHub(srv.URL, srv.Token, forgetest.Owner, forgetest.Repo)
}
return forge.NewGitea(srv.URL, srv.Token, forgetest.Owner, forgetest.Repo)
}
func TestKindMatchesConstructor(t *testing.T) {
if k := forge.NewGitea("https://g.example.com", "t", "o", "r").Kind(); k != forge.KindGitea {
t.Fatalf("NewGitea().Kind() = %s", k)
}
if k := forge.NewGitHub(forge.GitHubAPIBaseURL, "t", "o", "r").Kind(); k != forge.KindGitHub {
t.Fatalf("NewGitHub().Kind() = %s", k)
}
}
func TestCurrentUserLogin(t *testing.T) {
flavors(t, func(t *testing.T, srv *forgetest.Server, f forge.Forge) {
login, err := f.CurrentUserLogin()
if err != nil || login != "alice" {
t.Fatalf("CurrentUserLogin() = (%q, %v)", login, err)
}
})
}
func TestWrongTokenSurfacesHTTPError(t *testing.T) {
flavors(t, func(t *testing.T, srv *forgetest.Server, _ forge.Forge) {
var f forge.Forge
if srv.Flavor == forgetest.GitHub {
f = forge.NewGitHub(srv.URL, "wrong", forgetest.Owner, forgetest.Repo)
} else {
f = forge.NewGitea(srv.URL, "wrong", forgetest.Owner, forgetest.Repo)
}
_, err := f.CurrentUserLogin()
if err == nil || !strings.Contains(err.Error(), "401") {
t.Fatalf("错误 token 应返回带 401 的错误,得到 %v", err)
}
})
}
func TestCreateGetAndUpdateIssue(t *testing.T) {
flavors(t, func(t *testing.T, srv *forgetest.Server, f forge.Forge) {
created, err := f.CreateIssue("标题", "正文", []string{"alice"})
if err != nil {
t.Fatalf("CreateIssue 出错: %v", err)
}
if created.Number <= 0 || created.HTMLURL == "" {
t.Fatalf("CreateIssue 返回不完整: %+v", created)
}
if err := f.UpdateIssueBody(created.Number, "新正文"); err != nil {
t.Fatalf("UpdateIssueBody 出错: %v", err)
}
got, err := f.GetIssue(created.Number)
if err != nil {
t.Fatalf("GetIssue 出错: %v", err)
}
want := &forge.Issue{
Number: created.Number,
Title: "标题",
Body: "新正文",
State: "open",
Author: "alice",
Assignees: []string{"alice"},
HTMLURL: fmt.Sprintf("%s/acme/widgets/issues/%d", srv.URL, created.Number),
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("GetIssue = %+v\nwant %+v", got, want)
}
})
}
func TestGetIssueWithoutAssigneesGivesEmptyList(t *testing.T) {
flavors(t, func(t *testing.T, srv *forgetest.Server, f forge.Forge) {
n := srv.AddIssue(forgetest.Issue{Title: "t", Author: "bob"})
got, err := f.GetIssue(n)
if err != nil {
t.Fatalf("GetIssue 出错: %v", err)
}
if got.Assignees == nil || len(got.Assignees) != 0 {
t.Fatalf("没有指派人时应是非 nil 空切片,得到 %#v", got.Assignees)
}
})
}
func TestCommentsRoundTrip(t *testing.T) {
flavors(t, func(t *testing.T, srv *forgetest.Server, f forge.Forge) {
n := srv.AddIssue(forgetest.Issue{Title: "t", Author: "bob"})
srv.AddComment(n, "bob", "第一条")
posted, err := f.CreateIssueComment(n, "第二条")
if err != nil {
t.Fatalf("CreateIssueComment 出错: %v", err)
}
if posted.ID == 0 || posted.HTMLURL == "" || posted.Author != "alice" {
t.Fatalf("CreateIssueComment 返回不完整: %+v", posted)
}
list, err := f.ListIssueComments(n)
if err != nil {
t.Fatalf("ListIssueComments 出错: %v", err)
}
if len(list) != 2 || list[0].Body != "第一条" || list[0].Author != "bob" || list[1].Body != "第二条" {
t.Fatalf("评论列表不对: %+v", list)
}
if list[0].CreatedAt == "" {
t.Fatalf("评论应带 created_at: %+v", list[0])
}
})
}
func TestListIssueCommentsReadsEveryPageOnGitHub(t *testing.T) {
srv := forgetest.NewServer(t, forgetest.GitHub)
f := clientFor(srv)
n := srv.AddIssue(forgetest.Issue{Title: "t", Author: "bob"})
for i := 1; i <= 250; i++ {
srv.AddComment(n, "bob", fmt.Sprintf("c%d", i))
}
list, err := f.ListIssueComments(n)
if err != nil {
t.Fatalf("ListIssueComments 出错: %v", err)
}
if len(list) != 250 || list[0].Body != "c1" || list[249].Body != "c250" {
t.Fatalf("应读全 250 条且保持顺序,得到 %d 条", len(list))
}
var commentCalls []string
for _, r := range srv.Requests() {
if strings.Contains(r, "/comments") {
commentCalls = append(commentCalls, r)
}
}
if len(commentCalls) != 3 {
t.Fatalf("250 条应按 per_page=100 分 3 页取,实际请求: %v", commentCalls)
}
if !strings.Contains(commentCalls[0], "per_page=100") {
t.Fatalf("第一页应带 per_page=100: %s", commentCalls[0])
}
}
func TestGetPullRequest(t *testing.T) {
flavors(t, func(t *testing.T, srv *forgetest.Server, f forge.Forge) {
n := srv.AddPull(forgetest.Pull{
Title: "加功能", Body: "说明", State: "closed", Merged: true, Author: "bob",
HeadBranch: "feat/x", HeadSHA: "aaa111", BaseBranch: "main", BaseSHA: "bbb222",
})
got, err := f.GetPullRequest(n)
if err != nil {
t.Fatalf("GetPullRequest 出错: %v", err)
}
if got.Number != n || got.Title != "加功能" || got.Body != "说明" || got.State != "closed" || !got.Merged || got.Author != "bob" {
t.Fatalf("PR 字段不对: %+v", got)
}
if got.Head != (forge.BranchRef{Branch: "feat/x", SHA: "aaa111"}) || got.Base != (forge.BranchRef{Branch: "main", SHA: "bbb222"}) {
t.Fatalf("head/base 不对: %+v / %+v", got.Head, got.Base)
}
if got.HTMLURL == "" {
t.Fatal("PR 应带 html_url")
}
})
}
func TestGetPullRequestReportsHeadBranchAfterItIsDeleted(t *testing.T) {
flavors(t, func(t *testing.T, srv *forgetest.Server, f forge.Forge) {
n := srv.AddPull(forgetest.Pull{
Title: "t", State: "closed", Merged: true, Author: "bob",
HeadBranch: "feat/gone", HeadSHA: "aaa111", BaseBranch: "main", BaseSHA: "bbb222", HeadDeleted: true,
})
got, err := f.GetPullRequest(n)
if err != nil {
t.Fatalf("GetPullRequest 出错: %v", err)
}
if got.Head.Branch != "feat/gone" || got.Base.Branch != "main" {
t.Fatalf("head 分支删掉后仍应报分支名,得到 head=%+v base=%+v", got.Head, got.Base)
}
})
}
func TestCreatePullRequestAndDefaultBranch(t *testing.T) {
flavors(t, func(t *testing.T, srv *forgetest.Server, f forge.Forge) {
srv.DefaultBranch = "trunk"
base, err := f.DefaultBranch()
if err != nil || base != "trunk" {
t.Fatalf("DefaultBranch() = (%q, %v)", base, err)
}
pr, err := f.CreatePullRequest(forge.NewPullRequest{Title: "T", Body: "B", Head: "feat/y", Base: base})
if err != nil {
t.Fatalf("CreatePullRequest 出错: %v", err)
}
stored, ok := srv.Pull(pr.Number)
if !ok || stored.Title != "T" || stored.Body != "B" || stored.HeadBranch != "feat/y" || stored.BaseBranch != "trunk" {
t.Fatalf("服务端收到的 PR 不对: %+v", stored)
}
if pr.HTMLURL == "" || pr.Head.Branch != "feat/y" || pr.Base.Branch != "trunk" {
t.Fatalf("CreatePullRequest 返回不完整: %+v", pr)
}
})
}
func TestPullRequestCommentsUseIssueCommentsEndpoint(t *testing.T) {
flavors(t, func(t *testing.T, srv *forgetest.Server, f forge.Forge) {
n := srv.AddPull(forgetest.Pull{Title: "t", Author: "bob", HeadBranch: "h", BaseBranch: "main"})
if _, err := f.CreateIssueComment(n, "看过了"); err != nil {
t.Fatalf("CreateIssueComment 出错: %v", err)
}
reqs := srv.Requests()
prefix := ""
if srv.Flavor == forgetest.Gitea {
prefix = "/api/v1"
}
want := fmt.Sprintf("POST %s/repos/acme/widgets/issues/%d/comments", prefix, n)
if reqs[len(reqs)-1] != want {
t.Fatalf("PR 评论应走 issues comments 接口,实际 %s,want %s", reqs[len(reqs)-1], want)
}
})
}