修正 JSON 位元組相容與 xref Unicode 邊界(審核意見)
依 #12 審核意見修正四項偏差,並補一項同源問題: 1. iterXrefs 前一字元檢查改 Unicode:isWordChar 改用 unicode.IsLetter/IsDigit,且以 utf8 正確解碼前一 rune (原版只認 ASCII 又取單一位元組)。另補齊 Python finditer 的重試語義:起點被 lookbehind 擋下時從下一 rune 重試, 否則「修復bear-cli#9」會整段丟失(Python 匹配後半 cli#9); lookbehind 字元集含 #(##5 不匹配)。 2. writeJSON 改 json.Encoder + SetEscapeHTML(false): Python ensure_ascii=False 不轉義 <、>、&,json.Marshal 預設 會,標題含這些字元即失去位元組相容。 3. stalled_hours 改 strconv.FormatFloat(v,'f',1,64):整數值輸出 48.0(原 %g 輸出 48,極大值還會變 1e+06)。 4. Child.last_activity_at 無可解析時刻時輸出 null(原輸出 ""), 型別改 *string;格式對齊 FormatMoment(UTC 記 +00:00 非 Z)。 5. cmd/xcheck 輸出同樣停用 HTML 轉義(Marshal* 對自訂 MarshalJSON 的輸出仍會轉義)。 驗證:go build/vet/test 全綠(16 測試函式,新增 CJK、##5、 全形數字、HTML 字元、48.0、null 等案例);cmd/xcheck 與 gitea.py stalled 對真實 API 並行掃描輸出位元組相同(1749B, 含 bear-cli#9 父追蹤項與 5 子項摘要);--xrefs 對 CJK 邊界輸入與 Python 一致。
This commit is contained in:
@@ -5,9 +5,11 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -86,6 +88,12 @@ func TestIterXrefsEdges(t *testing.T) {
|
||||
{"斜線後不切半段", "alterminal/bear#47 只應匹配整段", [][2]string{{"alterminal/bear", "47"}}},
|
||||
{"連字號 repo 名", "bear-cli#9", [][2]string{{"bear-cli", "9"}}},
|
||||
{"路徑後的 #N 不匹配", "see https://x/a/#frag not #5", [][2]string{{"", "5"}}},
|
||||
// Python \w 是 Unicode:CJK 緊鄰 #N 時整體視為 \w,lookbehind 擋下。
|
||||
{"CJK 緊鄰 #N 略過", "修復#12的問題,見 #5", [][2]string{{"", "5"}}},
|
||||
{"CJK 緊鄰 repo#N 整段略過", "修復agents#12", nil},
|
||||
{"CJK 緊鄰連字號 repo 匹配後半", "修復bear-cli#9 見 #5", [][2]string{{"cli", "9"}, {"", "5"}}},
|
||||
{"連續井號", "##5", nil},
|
||||
{"全形數字後 #N", "12#5", nil},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -424,30 +432,48 @@ func TestScanStalledSkipsPRsInIssues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestItemJSON 輸出欄位與 gitea.py 對齊(last_activity_at 序列化、omitempty)。
|
||||
// TestItemJSON 與 gitea.py 的 JSON 位元組相容(鍵序、HTML 不轉義、48.0、null)。
|
||||
func TestItemJSON(t *testing.T) {
|
||||
ts := at(-600)
|
||||
activity := FormatMoment(ts)
|
||||
item := Item{
|
||||
Type: "issue", Repo: "alterminal/teai", Number: 7, Title: "t", URL: "u",
|
||||
Type: "issue", Repo: "alterminal/teai", Number: 7, Title: "R,D <v2> & 規劃", URL: "u",
|
||||
Assignees: []string{"ceo"}, LastCommentBy: "max",
|
||||
LastActivityAt: ts, StalledHours: 10, Reason: "assignee-idle",
|
||||
ChildActivity: []Child{{Repo: "alterminal/teai", Number: 3, Title: "父", State: "open", LastActivityAt: ts.Format(time.RFC3339)}},
|
||||
LastActivityAt: ts, StalledHours: 48, Reason: "assignee-idle",
|
||||
ChildActivity: []Child{
|
||||
{Repo: "alterminal/teai", Number: 3, Title: "父", State: "open", LastActivityAt: &activity},
|
||||
{Repo: "alterminal/teai", Number: 4, Title: "無時刻", State: "open"},
|
||||
},
|
||||
}
|
||||
// json.Marshal 會把 <、>、& 重新轉義,必須用停用 HTML 轉義的編碼器。
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
if err := enc.Encode(item); err != nil {
|
||||
t.Fatalf("Encode: %v", err)
|
||||
}
|
||||
got := strings.TrimSuffix(buf.String(), "\n")
|
||||
want := `{"type":"issue","repo":"alterminal/teai","number":7,"title":"R,D <v2> & 規劃","url":"u","assignees":["ceo"],"last_comment_by":"max","last_activity_at":"2026-09-09T22:00:00+00:00","stalled_hours":48.0,"reason":"assignee-idle","child_activity":[{"repo":"alterminal/teai","number":3,"title":"父","state":"open","last_activity_at":"2026-09-09T22:00:00+00:00"},{"repo":"alterminal/teai","number":4,"title":"無時刻","state":"open","last_activity_at":null}]}`
|
||||
if got != want {
|
||||
t.Fatalf("JSON 位元組不相容:\n got %s\nwant %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPullJSON:pull 的鍵序(author、reviewers)與非整數 stalled_hours。
|
||||
func TestPullJSON(t *testing.T) {
|
||||
item := Item{
|
||||
Type: "pull", Repo: "alterminal/teai", Number: 12, Title: "t", URL: "u",
|
||||
Author: "ceo", Reviewers: []string{"queena"},
|
||||
LastActivityAt: at(-600), StalledHours: 10.44, Reason: "reviewer-idle",
|
||||
}
|
||||
b, err := json.Marshal(item)
|
||||
if err != nil {
|
||||
t.Fatalf("Marshal: %v", err)
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
for _, key := range []string{"type", "repo", "number", "title", "url", "assignees", "last_comment_by", "last_activity_at", "stalled_hours", "reason", "child_activity"} {
|
||||
if _, ok := m[key]; !ok {
|
||||
t.Errorf("輸出缺少欄位 %q:%s", key, b)
|
||||
}
|
||||
}
|
||||
if _, ok := m["author"]; ok {
|
||||
t.Errorf("issue 不應輸出 author:%s", b)
|
||||
got := string(b)
|
||||
want := `{"type":"pull","repo":"alterminal/teai","number":12,"title":"t","url":"u","author":"ceo","reviewers":["queena"],"last_comment_by":"","last_activity_at":"2026-09-09T22:00:00+00:00","stalled_hours":10.4,"reason":"reviewer-idle"}`
|
||||
if got != want {
|
||||
t.Fatalf("JSON 位元組不相容:\n got %s\nwant %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user