fix(workflow): use status/category list filters in health and report fetchers

Signed-off-by: 林晨 (Leo Cheng) <chengkelfan@qq.com>
This commit is contained in:
林晨 (Leo Cheng) 2026-07-08 12:11:46 +08:00
parent 9749a4c832
commit e4b03e0053
No known key found for this signature in database
GPG Key ID: 24FCF87A069356B9
8 changed files with 109 additions and 11 deletions

View File

@ -0,0 +1,9 @@
# workflow 健康评分列表筛选修正
`workflow +health`、`+repo-report`、`+triage` 的远程抓取此前用 `state=open` 过滤 Issue/PR 列表,但 GitLink v1 列表接口对 PR 用 `status`0 开启 / 1 合并 / 2 关闭),对 Issue 用 `category`opened / closed / all`state` 会被服务端静默忽略并返回全部状态。结果 open+merged+closed 全被计入,`OpenPRs`/`OpenIssues` 与由此推导的健康分被污染。
现在 PR 抓取发送 `status=0`Issue 抓取发送 `category=opened`,与 `shortcuts/health`、`shortcuts/pr`、`shortcuts/issue` 已有的映射保持一致;`+triage` 的本地态客户端二次过滤保持不变。
同时修正 `updateRecentActivity``RecentActivityDays==0` 表示"今天有活动"`apiAgeInDays` 对 24 小时内返回 0旧逻辑里 `|| input.RecentActivityDays == 0` 会让更早的信号覆盖"今天",使仓库显得更陈旧。移除该项后,首次赋值仍由 `!RecentActivityKnown` 分支处理,之后仅当发现更近的活动才更新。
测试断言列表请求发出的键为 `status`/`category`(不再是 `state`),并验证"今天"的活动信号不会被更早的信号覆盖。

View File

@ -495,8 +495,36 @@ func TestQueryWithPageLimit(t *testing.T) {
func TestIssueListQuery(t *testing.T) {
q := issueListQuery("open")
if q.Get("state") != "open" {
t.Fatalf("issueListQuery state = %q", q.Get("state"))
if q.Get("state") != "" {
t.Fatalf("issueListQuery must not send state, got %q", q.Get("state"))
}
if q.Get("category") != "opened" {
t.Fatalf("issueListQuery category = %q, want opened", q.Get("category"))
}
if got := issueListQuery("closed").Get("category"); got != "closed" {
t.Fatalf("issueListQuery(closed) category = %q, want closed", got)
}
if got := issueListQuery("all").Get("category"); got != "all" {
t.Fatalf("issueListQuery(all) category = %q, want all", got)
}
}
func TestPullListQuery(t *testing.T) {
q := pullListQuery("open")
if q.Get("state") != "" {
t.Fatalf("pullListQuery must not send state, got %q", q.Get("state"))
}
if q.Get("status") != "0" {
t.Fatalf("pullListQuery status = %q, want 0", q.Get("status"))
}
if got := pullListQuery("merged").Get("status"); got != "1" {
t.Fatalf("pullListQuery(merged) status = %q, want 1", got)
}
if got := pullListQuery("closed").Get("status"); got != "2" {
t.Fatalf("pullListQuery(closed) status = %q, want 2", got)
}
if _, ok := pullListQuery("all")["status"]; ok {
t.Fatal("pullListQuery(all) should omit status so the API returns every state")
}
}
@ -582,6 +610,14 @@ func TestUpdateRecentActivity(t *testing.T) {
if !known2 || days2 != days {
t.Fatalf("zero time update should not change: known=%v days=%d", known2, days2)
}
// A signal from today (days==0) must not be overwritten by an older one.
today := HealthInput{RecentActivityKnown: true, RecentActivityDays: 0}
old := time.Now().Add(-45 * 24 * time.Hour)
_, keptDays, _ := updateRecentActivity(today, old)
if keptDays != 0 {
t.Fatalf("today signal overwritten by older one: days=%d, want 0", keptDays)
}
}
func TestAPIIntStringFallback(t *testing.T) {

View File

@ -39,7 +39,7 @@ func FetchHealthInput(ctx *common.RuntimeContext, opts HealthFetchOptions) (Heal
input.RecentActivityKnown, input.RecentActivityDays, input = updateRecentActivity(input, latestTimeFromItems(issues))
}
if prs, err := fetchAllListItems(ctx, workflowRepoPath(owner, repo)+"/pulls", issueListQuery("open"), 100); err != nil {
if prs, err := fetchAllListItems(ctx, workflowRepoPath(owner, repo)+"/pulls", pullListQuery("open"), 100); err != nil {
notes = append(notes, ScoringNote{Metric: "open_prs", Note: fmt.Sprintf("pull request probe failed: %v", err)})
} else {
input.OpenPRs = len(prs)
@ -200,7 +200,7 @@ func updateRecentActivity(input HealthInput, latest time.Time) (bool, int, Healt
return input.RecentActivityKnown, input.RecentActivityDays, input
}
days := apiAgeInDays(latest)
if !input.RecentActivityKnown || days < input.RecentActivityDays || input.RecentActivityDays == 0 {
if !input.RecentActivityKnown || days < input.RecentActivityDays {
input.RecentActivityKnown = true
input.RecentActivityDays = days
}
@ -220,12 +220,46 @@ func queryWithPageLimit(base url.Values, page, limit int) url.Values {
return base
}
// The GitLink v1 list API filters issues by category and pulls by status; a
// stray "state" param is silently ignored and every state is returned.
func issueListQuery(state string) url.Values {
q := url.Values{}
q.Set("state", state)
q.Set("category", normalizeIssueListCategory(state))
return q
}
func pullListQuery(state string) url.Values {
q := url.Values{}
if status := normalizePullListStatus(state); status != "" {
q.Set("status", status)
}
return q
}
func normalizeIssueListCategory(state string) string {
switch strings.ToLower(strings.TrimSpace(state)) {
case "open", "opened":
return "opened"
case "closed":
return "closed"
default:
return "all"
}
}
func normalizePullListStatus(state string) string {
switch strings.ToLower(strings.TrimSpace(state)) {
case "open", "opened":
return "0"
case "merged":
return "1"
case "closed":
return "2"
default:
return ""
}
}
func fetchAllListItems(ctx *common.RuntimeContext, path string, baseQuery url.Values, pageSize int) ([]map[string]interface{}, error) {
if pageSize <= 0 {
pageSize = 100

View File

@ -22,11 +22,23 @@ func TestFetchHealthInputCollectsSignals(t *testing.T) {
"has_contributing": true,
})
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues.json":
if got := r.URL.Query().Get("category"); got != "opened" {
t.Fatalf("issue category query = %q, want opened", got)
}
if got := r.URL.Query().Get("state"); got != "" {
t.Fatalf("issue list must not send state, got %q", got)
}
writeWorkflowJSON(t, w, map[string]interface{}{"issues": []map[string]interface{}{
{"id": 1, "subject": "fresh issue", "updated_at": now.AddDate(0, 0, -1).Format(time.RFC3339)},
{"id": 2, "subject": "stale issue", "updated_at": old.Format(time.RFC3339)},
}})
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/pulls.json":
if got := r.URL.Query().Get("status"); got != "0" {
t.Fatalf("pull status query = %q, want 0", got)
}
if got := r.URL.Query().Get("state"); got != "" {
t.Fatalf("pull list must not send state, got %q", got)
}
writeWorkflowJSON(t, w, map[string]interface{}{"pulls": []map[string]interface{}{
{"id": 3, "title": "stale pr", "updated_at": old.Format(time.RFC3339)},
}})

View File

@ -2,7 +2,6 @@ package workflow
import (
"fmt"
"net/url"
"strings"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
@ -104,8 +103,7 @@ func fetchPRListForReport(ctx *common.RuntimeContext, owner, repo string, limit
if limit <= 0 {
limit = 10
}
query := url.Values{}
query.Set("state", "open")
query := pullListQuery("open")
query.Set("page", "1")
query.Set("limit", fmt.Sprintf("%d", limit))

View File

@ -146,6 +146,12 @@ func TestFetchRepoReportInputPRListMetadata(t *testing.T) {
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/pulls.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
if got := r.URL.Query().Get("status"); got != "0" {
t.Fatalf("PR status query = %q, want 0", got)
}
if got := r.URL.Query().Get("state"); got != "" {
t.Fatalf("PR report list must not send state, got %q", got)
}
if got := r.URL.Query().Get("limit"); got != "1" {
t.Fatalf("PR limit = %q, want 1", got)
}

View File

@ -29,7 +29,7 @@ func FetchIssuesForTriage(ctx *common.RuntimeContext, opts TriageFetchOptions) (
}
query := url.Values{}
query.Set("state", state)
query.Set("category", normalizeIssueListCategory(state))
query.Set("limit", fmt.Sprintf("%d", limit))
query.Set("page", fmt.Sprintf("%d", page))
if len(opts.Labels) > 0 {

View File

@ -16,8 +16,11 @@ func TestFetchIssuesForTriageNormalizesAPIResponse(t *testing.T) {
if r.Method != "GET" || r.URL.Path != "/v1/owner/repo/issues.json" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
if got := r.URL.Query().Get("state"); got != "open" {
t.Fatalf("state query = %q, want open", got)
if got := r.URL.Query().Get("category"); got != "opened" {
t.Fatalf("category query = %q, want opened", got)
}
if got := r.URL.Query().Get("state"); got != "" {
t.Fatalf("issue triage must not send state, got %q", got)
}
if got := r.URL.Query().Get("limit"); got != "30" {
t.Fatalf("limit query = %q, want 30", got)