fcf00bb5df
leo 08-06 提供決定性線索:**exit status 2,已自動重試 21 次失敗**。
## 真兇
`app.go` 的 `saveCfg` 只寫四個鍵(accounts/extractor/extractor_explicit/
gemini_api_key),**從來不寫 `manifest`**。
而 `manifest` 是 collector 的必填欄位(`direct.go`:
`if c.Manifest == "" { missing = append(missing, "manifest") }`)
⇒ 讀 config 失敗 → `runDirect` 回 2 → supervisor 無限重拉
⇒ ①「看守中/沒有在跑」一直閃、重開無效(死因沒變)
⇒ ②加資料夾沒反應(collector 從沒跑完一輪)
## 為什麼一路活到封測
**開發機的 config 是舊版留下的、早就有這一欄。**
只有「從零長出 config」的機器才會撞到——也就是**每一台封測者的機器**。
「我這台好好的」正是這個 bug 能活下來的原因。
⇒ 新測試 `TestFreshConfigIsUsableByCollector` **從零建 config**,
並且不只驗欄位在不在,而是**真的餵給 `collector.LoadDirectConfig`**。
已反向驗證:把修正拿掉,測試會紅(不是假測試)。
## 順帶修好「為什麼只看得到 exit status 2」
supervisor 在行程結束時用 `cmd.Wait()` 的錯誤當訊息,
**蓋掉**了 stderr 剛寫進 `LastError` 的那句真話
(`collector direct: config 缺必填欄位:manifest`)。
⇒ 改成以 stderr 為主、退出碼放括號補充。
(v0.18.10 已讓死因上畫面+進 app.log,這版讓那句話是**對的**那一句。)
## 三種 exit 2 的實測輸出(重現測試打出來的,非推測)
- config 不是合法 JSON → `config JSON 解析失敗:invalid character …`
- 缺必填欄位 → `config 缺必填欄位:manifest, accounts(或 cypher_url 連線設定)`
- Windows 反斜線路徑 → 正常解析(**排除「反斜線害的」這個猜測**)
## 驗
collector 全測試過、app 測試過、mac+windows 皆編得過;
產物 `Arcrun-win-v0.18.11.exe`(22M)已放 leo 桌面。
⚠️ **尚未真機驗**——要 leo 在 Windows 全新裝一次才算通。
61 lines
2.0 KiB
Go
61 lines
2.0 KiB
Go
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
collector "arcrun-rag/collector"
|
||
)
|
||
|
||
// TestFreshConfigIsUsableByCollector 釘住 leo 2026-08-06 Windows 封測的真兇:
|
||
//
|
||
// App 存出來的**全新** config 少了 `manifest` 這個必填欄位
|
||
// ⇒ collector 一啟動就 exit status 2 ⇒ supervisor 無限重拉
|
||
// ⇒ 畫面在「看守中/沒有在跑」之間閃、加資料夾也沒反應。
|
||
//
|
||
// 為什麼一路活到封測:開發機的 config 是舊版留下的、早就有這一欄
|
||
// ——「我這台好好的」正是它能活下來的原因。所以這支測試**從零建 config**。
|
||
func TestFreshConfigIsUsableByCollector(t *testing.T) {
|
||
home := t.TempDir()
|
||
t.Setenv("HOME", home)
|
||
t.Setenv("USERPROFILE", home) // Windows 的家目錄變數
|
||
|
||
// 模擬全新安裝:完全沒有 config,使用者連上一個知識庫後存檔
|
||
cfg := &directConfig{
|
||
Accounts: []accountCfg{{
|
||
CypherURL: "https://example.workers.dev",
|
||
Namespace: "abc123",
|
||
APIKey: "abc123",
|
||
}},
|
||
Extractor: "workers-ai",
|
||
}
|
||
if err := saveCfg(cfg); err != nil {
|
||
t.Fatalf("存檔失敗:%v", err)
|
||
}
|
||
|
||
raw, err := os.ReadFile(configPath())
|
||
if err != nil {
|
||
t.Fatalf("讀不回剛存的 config:%v", err)
|
||
}
|
||
var m map[string]any
|
||
if err := json.Unmarshal(raw, &m); err != nil {
|
||
t.Fatalf("存出來的不是合法 JSON:%v", err)
|
||
}
|
||
if s, _ := m["manifest"].(string); strings.TrimSpace(s) == "" {
|
||
t.Fatal("全新 config 沒有 manifest ⇒ collector 會 exit 2 無限重試(就是 leo 撞到的那個)")
|
||
}
|
||
|
||
// 真正的驗收不是「欄位在不在」,是**collector 吃不吃得下去**。
|
||
if _, err := collector.LoadDirectConfig(configPath()); err != nil {
|
||
t.Fatalf("collector 讀不了 App 存的 config:%v", err)
|
||
}
|
||
|
||
// manifest 要落在 app 目錄下,不能指到別人家
|
||
if got, _ := m["manifest"].(string); filepath.Dir(got) != appDir() {
|
||
t.Errorf("manifest 應該放在 %s,實得 %s", appDir(), got)
|
||
}
|
||
}
|