sync: collector/ 同步自 inkstone/arcrun-rag@99ff817(桌面小幫手 0.18.56)
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user