sync: collector/ 同步自 inkstone/arcrun-rag@1c31ac2(桌面小幫手 0.18.46)

This commit is contained in:
2026-08-28 05:07:35 +08:00
parent 80877af60c
commit e8800d18ef
12 changed files with 540 additions and 46 deletions
+93 -4
View File
@@ -187,10 +187,13 @@ func TestStallSaysWhichAccountWhichStepHowLong(t *testing.T) {
}
// 交給呼叫端的錯誤是產品文案:講哪件事、等多久、他會怎樣,不出現狀態碼/內部名詞。
msg := err.Error()
for _, want := range []string{stepRepairOrigin.Name, "沒有回應", "會自動恢復"} {
if !strings.Contains(msg, want) {
t.Fatalf("錯誤訊息少了「%s」:%s", want, msg)
}
if !strings.Contains(msg, stepRepairOrigin.Name) {
t.Fatalf("錯誤訊息沒講是哪件事:%s", msg)
}
// 🔴 不再寫死「會自動恢復」這幾個字——改用產生端與消費端**共用**的那個判準。
// 寫死字串正是第三輪的病:我改了措辭,兩邊就對不上了(見 explainsWhySkipped)。
if !explainsWhySkipped(msg) {
t.Fatalf("🔴 這句話不會被 status.json 收進失敗清單 ⇒ 畫面上會沒有原因:%s", msg)
}
for _, banned := range []string{"context deadline", "HTTP", "timeout", "Client.Timeout"} {
if strings.Contains(msg, banned) {
@@ -336,3 +339,89 @@ func TestGateAlwaysHasADeadlineEvenWithoutGuard(t *testing.T) {
t.Fatalf("🔴 沒有 guard 就沒有上限了(等了 %v", el)
}
}
// 🔴 慢但**成功**的雲端,不准被講成「沒有回應」,也不准害後面的檔被跳過。
//
// 這一條是 2026-08-28 第三輪的實撞:`送出一份筆記` 實測 33〜57 秒,而我第一輪
// 憑「零 LLM 應該很快」的**推論**給了 60 秒上限 ⇒ 尾巴被剪斷 2 發 ⇒ 斷路器跳 ⇒
// **後面 9 個檔全被跳過**,畫面還對使用者說「知識庫現在沒有回應」。
// 那台知識庫一路都在回應(同一輪成功送出 8 份筆記),只是慢。
func TestSlowButSucceedingCloudIsNotCalledUnresponsive(t *testing.T) {
// 上限給 2 秒,伺服器每發 300ms——**遠低於上限**,就是「慢但會成功」。
stallTestTimings(t, 2*time.Second, 50*time.Millisecond)
origFetch := fetchCloudVersion
fetchCloudVersion = func(string) (string, bool) { return "", false }
defer func() { fetchCloudVersion = origFetch }()
var hits int64
slow := slowButHealthyServer(t, 300*time.Millisecond, &hits)
root := t.TempDir()
for i := range [8]struct{}{} {
name := filepath.Join(root, "note"+string(rune('a'+i))+".md")
if err := os.WriteFile(name, []byte("# 筆記\n內容"), 0o644); err != nil {
t.Fatal(err)
}
}
manifestPath := filepath.Join(t.TempDir(), "m.json")
cfg := &DirectConfig{
Manifest: manifestPath,
Accounts: []AccountConfig{{CypherURL: slow.URL, Namespace: "n", APIKey: "k",
WatchFolders: []string{root}}},
Library: "kb", MaxRemoved: DefaultMaxRemovedRatio,
CardIngestWF: "rag_ingest_card", IngestWF: "rag_ingest_direct", RemovedWF: "rag_takedown_direct",
ForceSync: true,
}
results, _, _ := RunDirectOnce(cfg, false)
// ① 斷路器不准跳:它一路都在回應。
if note := cfg.unreachableNote(); note != "" {
t.Fatalf("🔴 慢但成功的雲端被判成不能打了:%s", note)
}
for _, s := range cfg.guard.Stalls() {
if s.Skipped {
t.Fatalf("🔴 有一筆標成「這一輪跳過該帳號」:%+v", s)
}
}
// ② 一個檔都不准被「帳號沒回應」這個理由跳過。
skipped := 0
for _, r := range results {
if r.Status == "skipped" && (strings.Contains(r.Error, "沒有回應") ||
strings.Contains(r.Error, "回得太慢")) {
skipped++
}
}
if skipped > 0 {
t.Fatalf("🔴 有 %d 個檔因為「帳號沒回應」被跳過,但雲端每一發都成功了", skipped)
}
// ③ 就算真的有一發逾時,只要它這一輪回應過,措辭就不准是「沒有回應」。
g := newRoundGuard()
g.announce = func(StalledCall) {}
g.succeeded("h")
msg := g.strike("h", stepIngestCard, 3*time.Second)
if strings.Contains(msg, "沒有回應") {
t.Fatalf("🔴 它回應過,卻還是說「沒有回應」:%s", msg)
}
if !strings.Contains(msg, "回得太慢") {
t.Fatalf("措辭應該是「回得太慢」:%s", msg)
}
}
// 「連續」兩個字必須是真的:成功一次就把計數歸零。
func TestStrikesAreActuallyConsecutive(t *testing.T) {
g := newRoundGuard()
g.announce = func(StalledCall) {}
g.strike("h", stepIngestCard, time.Second) // 第 1 次逾時
g.succeeded("h") // 中間成功了一次
g.strike("h", stepIngestCard, time.Second) // 又逾時一次 ⇒ 這是「第 1 次連續」
if r := g.skipReason("h"); r != "" {
t.Fatalf("🔴 中間成功過,卻仍然跳閘了——訊息裡的「連續」是假的:%s", r)
}
g.strike("h", stepIngestCard, time.Second) // 真的連續第 2 次 ⇒ 才該跳
if g.skipReason("h") == "" {
t.Fatal("真的連續兩次逾時了,該跳閘卻沒跳")
}
}