246 lines
8.5 KiB
Go
246 lines
8.5 KiB
Go
package collector
|
||
|
||
// inkstone/arcrun-rag#196:雲端 1.4.64 起修掉「單次呼叫子請求太多」那面牆,
|
||
// 但修之前被記滿 8 次、暫停自動重試的檔不會自己動——用戶不改檔、不按「立刻同步」就永遠送不上去。
|
||
// 09-17 實查 geek6688:13 份掛著這種暫停,含本票量測用的兩張卡。
|
||
// 本檔用那兩張卡在 geek6688 帳本上的**原樣病歷**(testdata/196_geek6688_paused.json)重演。
|
||
|
||
import (
|
||
"encoding/json"
|
||
"io"
|
||
"net"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
type pausedFixture struct {
|
||
Entries map[string]struct {
|
||
FailCount int `json:"fail_count"`
|
||
LastFailAt int64 `json:"last_fail_at"`
|
||
NextRetry int64 `json:"next_retry"`
|
||
LastError string `json:"last_error"`
|
||
} `json:"entries"`
|
||
}
|
||
|
||
const (
|
||
card196Sample = "system-dev/wiki/cards/arcrun-sample-knowledge.md"
|
||
card196N8n = "system-dev/wiki/cards/arcrun-n8n版本比較表新-2025.11-範本.md"
|
||
)
|
||
|
||
// fakeCloud196 =一台假知識庫:萃取一律成功;收卡依 wall 決定回成功或回 geek6688 那句 500。
|
||
type fakeCloud196 struct {
|
||
mu sync.Mutex
|
||
version string // /health 的 bundle_version
|
||
wall map[string]bool // 這些檔收卡時回「Too many subrequests」
|
||
posts map[string]int // 每個檔實際打到 rag_ingest_card 幾次
|
||
wallErr string
|
||
}
|
||
|
||
func (f *fakeCloud196) handler(w http.ResponseWriter, r *http.Request) {
|
||
body, _ := io.ReadAll(r.Body)
|
||
var req struct {
|
||
PageName string `json:"page_name"`
|
||
Text string `json:"text"`
|
||
Path string `json:"path"`
|
||
}
|
||
_ = json.Unmarshal(body, &req)
|
||
f.mu.Lock()
|
||
defer f.mu.Unlock()
|
||
switch {
|
||
case strings.HasSuffix(r.URL.Path, "/portal/daemon/extract"):
|
||
_ = json.NewEncoder(w).Encode(map[string]any{"success": true, "output": cardFixture(req.PageName, "測試")})
|
||
case strings.HasSuffix(r.URL.Path, "/rag_ingest_card/trigger"):
|
||
f.posts[req.Path]++
|
||
if f.wall[req.Path] {
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
_, _ = io.WriteString(w, f.wallErr)
|
||
return
|
||
}
|
||
_ = json.NewEncoder(w).Encode(map[string]any{"success": true})
|
||
default:
|
||
_ = json.NewEncoder(w).Encode(map[string]any{"success": true})
|
||
}
|
||
}
|
||
|
||
func (f *fakeCloud196) postsOf(p string) int {
|
||
f.mu.Lock()
|
||
defer f.mu.Unlock()
|
||
return f.posts[p]
|
||
}
|
||
|
||
// setup196 建出「geek6688 今天的樣子」:兩張卡在帳本上是舊版留下的暫停病歷(沒記雲端版本)。
|
||
func setup196(t *testing.T) (cfg *DirectConfig, mp, root string, cloud *fakeCloud196, clock *time.Time) {
|
||
t.Helper()
|
||
cloudRoutes.reset()
|
||
t.Cleanup(cloudRoutes.reset)
|
||
withCloudCheckInterval(t, 0)
|
||
|
||
raw, err := os.ReadFile(filepath.Join("testdata", "196_geek6688_paused.json"))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var fx pausedFixture
|
||
if err := json.Unmarshal(raw, &fx); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(fx.Entries) != 2 {
|
||
t.Fatalf("fixture 應有兩張卡:%d", len(fx.Entries))
|
||
}
|
||
|
||
root = t.TempDir()
|
||
writeFile(t, root, card196Sample, "# Arcrun 範例知識\n\nArcrun 是跑在 Cloudflare 上的工作流引擎。", baseTime)
|
||
writeFile(t, root, card196N8n, "# n8n 版本比較表\n\n| 版本 | 說明 |\n|---|---|\n| 1.0 | 測試 |", baseTime)
|
||
|
||
cloud = &fakeCloud196{wall: map[string]bool{}, posts: map[string]int{},
|
||
wallErr: fx.Entries[card196Sample].LastError[len("HTTP 500:"):]}
|
||
oldFetch := fetchCloudVersion
|
||
fetchCloudVersion = func(string) (string, bool) {
|
||
cloud.mu.Lock()
|
||
defer cloud.mu.Unlock()
|
||
return cloud.version, true
|
||
}
|
||
t.Cleanup(func() { fetchCloudVersion = oldFetch })
|
||
|
||
// 先占位址再關掉:第一輪連不上(只為了建出帳本),之後在同一個位址開假雲端。
|
||
ln0, err := net.Listen("tcp", "127.0.0.1:0")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
addr := ln0.Addr().String()
|
||
_ = ln0.Close()
|
||
|
||
c := time.Unix(fx.Entries[card196Sample].NextRetry, 0).Add(4 * 24 * time.Hour) // 09-17:暫停早已過了退避窗口
|
||
clock = &c
|
||
oldNow := directNow
|
||
directNow = func() time.Time { return *clock }
|
||
t.Cleanup(func() { directNow = oldNow })
|
||
|
||
cfg = &DirectConfig{
|
||
Manifest: filepath.Join(t.TempDir(), "m.json"),
|
||
CardIngestWF: "rag_ingest_card", IngestWF: "rag_ingest_direct", RemovedWF: "rag_takedown_direct",
|
||
Extractor: "workers-ai", ExtractorExplicit: true, MachineLabel: "測試機",
|
||
MaxRemoved: DefaultMaxRemovedRatio,
|
||
Accounts: []AccountConfig{{CypherURL: "http://" + addr, Namespace: "demo", APIKey: "demo",
|
||
InstanceName: "geek6688", WatchFolders: []string{root}}},
|
||
}
|
||
RunDirectOnce(cfg, false)
|
||
cloudRoutes.reset() // 建帳本那一輪的連線失敗不要帶進正式情境
|
||
acc := cfg.makeAccountSubConfig(cfg.Accounts[0])
|
||
mp = acc.manifestPathFor(root)
|
||
m, err := LoadManifest(mp, root)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for p, f := range fx.Entries {
|
||
e := m.Entries[p]
|
||
if e == nil {
|
||
t.Fatalf("帳本裡沒有 %s", p)
|
||
}
|
||
e.IngestedHash, e.IngestedAt, e.NoCloudCard = "", 0, false
|
||
e.FailCount, e.LastFailAt, e.NextRetry, e.LastError = f.FailCount, f.LastFailAt, f.NextRetry, f.LastError
|
||
e.FailCloudVersion = "" // 舊版小幫手不記這欄
|
||
}
|
||
if err := m.Save(mp); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
ln, err := net.Listen("tcp", addr)
|
||
if err != nil {
|
||
t.Skipf("無法重新綁定同一個埠(%v)", err)
|
||
}
|
||
srv := &http.Server{Handler: http.HandlerFunc(cloud.handler)}
|
||
go func() { _ = srv.Serve(ln) }()
|
||
t.Cleanup(func() { _ = srv.Close() })
|
||
if p := mustLoad(t, mp, root).Progress(); p.Stuck != 2 {
|
||
t.Fatalf("前置條件:兩張卡都該是暫停:%+v", p)
|
||
}
|
||
return cfg, mp, root, cloud, clock
|
||
}
|
||
|
||
func mustLoad(t *testing.T, mp, root string) *Manifest {
|
||
t.Helper()
|
||
m, err := LoadManifest(mp, root)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return m
|
||
}
|
||
|
||
// 雲端還是舊版 ⇒ 照舊暫停、不重撞;雲端更新到 1.4.64 以上 ⇒ 不用人動手,兩張卡自己送上去、算完成。
|
||
func TestDirect196_SubrequestPausedResumesAfterCloudFix(t *testing.T) {
|
||
cfg, mp, root, cloud, clock := setup196(t)
|
||
|
||
cloud.version = "1.4.63"
|
||
results, _, _ := RunDirectOnce(cfg, false)
|
||
if n := cloud.postsOf(card196Sample) + cloud.postsOf(card196N8n); n != 0 {
|
||
t.Fatalf("雲端還是 1.4.63(沒修那面牆)時不該重撞,卻送了 %d 發", n)
|
||
}
|
||
for _, r := range results {
|
||
if (r.Path == card196Sample || r.Path == card196N8n) && !strings.Contains(r.Error, "已暫停自動重試") {
|
||
t.Fatalf("舊雲端上 %s 應維持暫停說明,拿到 %q", r.Path, r.Error)
|
||
}
|
||
}
|
||
if p := mustLoad(t, mp, root).Progress(); p.Stuck != 2 {
|
||
t.Fatalf("舊雲端上兩張都該維持暫停:%+v", p)
|
||
}
|
||
|
||
cloud.version = "1.4.67" // geek6688 09-17 的 /health
|
||
*clock = clock.Add(time.Minute)
|
||
RunDirectOnce(cfg, false)
|
||
if cloud.postsOf(card196Sample) == 0 || cloud.postsOf(card196N8n) == 0 {
|
||
t.Fatalf("雲端更新後兩張都要自己送出:sample=%d n8n=%d", cloud.postsOf(card196Sample), cloud.postsOf(card196N8n))
|
||
}
|
||
m := mustLoad(t, mp, root)
|
||
if p := m.Progress(); p.Stuck != 0 || p.Done != p.Total {
|
||
t.Fatalf("兩張都要算完成:%+v", p)
|
||
}
|
||
for _, p := range []string{card196Sample, card196N8n} {
|
||
if e := m.Entries[p]; e.FailCount != 0 || e.LastError != "" || e.FailCloudVersion != "" {
|
||
t.Fatalf("%s 送成功後病歷要清乾淨:%+v", p, e)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 在已經修好的雲端上**又**撞同一面牆(卡真的太大)⇒ 記下這一版,之後不再自動重撞燒額度。
|
||
func TestDirect196_StillTooBigOnFixedCloudStaysPaused(t *testing.T) {
|
||
cfg, mp, root, cloud, clock := setup196(t)
|
||
cloud.version = "1.4.67"
|
||
cloud.wall[card196Sample] = true
|
||
|
||
RunDirectOnce(cfg, false)
|
||
first := cloud.postsOf(card196Sample)
|
||
if first == 0 {
|
||
t.Fatal("雲端已修好時,舊暫停的卡應該要再試一次")
|
||
}
|
||
m := mustLoad(t, mp, root)
|
||
e := m.Entries[card196Sample]
|
||
if e.FailCount < MaxFailBeforeSkip || e.FailCloudVersion != "1.4.67" || !isSubrequestLimitText(e.LastError) {
|
||
t.Fatalf("在新雲端上又撞牆,病歷要記下 1.4.67:%+v", e)
|
||
}
|
||
if p := m.Progress(); p.Done != 1 || p.Stuck != 1 {
|
||
t.Fatalf("n8n 那張送成、sample 那張暫停:%+v", p)
|
||
}
|
||
|
||
*clock = clock.Add(7 * 24 * time.Hour) // 遠超過最長退避
|
||
cloudRoutes.reset()
|
||
RunDirectOnce(cfg, false)
|
||
if got := cloud.postsOf(card196Sample); got != first {
|
||
t.Fatalf("已在修好的雲端上撞過牆,不該再自動重撞:%d → %d", first, got)
|
||
}
|
||
}
|
||
|
||
func TestCloudHasSubrequestFix196(t *testing.T) {
|
||
for v, want := range map[string]bool{
|
||
"": false, "1.4.63": false, "1.4.64": true, "1.4.67+b08a8cee": true, "1.10.0": true,
|
||
"2026-09-13+1eb26c9": false, "unknown": false,
|
||
} {
|
||
if got := cloudHasSubrequestFix(v); got != want {
|
||
t.Errorf("cloudHasSubrequestFix(%q)=%v,要 %v", v, got, want)
|
||
}
|
||
}
|
||
}
|