v0.18.13:舊設定檔自我修復——我第一次修錯了,只修好全新安裝
leo 裝了 v0.18.12 **仍然是同一句錯**(實拍:重試 30 次、
「collector direct: config 缺必填欄位:manifest」),並提供了他的 config.json。
## 我錯在哪
v0.18.11 只在 `saveCfg`(存檔)補必填欄位。但使用者打開程式時
**只是「讀 config → 啟動引擎」,saveCfg 根本不會被呼叫**
⇒ 已經存在的壞設定永遠修不好。全新安裝好了,**現有使用者一個都沒被救到**。
leo 的 config 鐵證:頂層只有 `accounts`/`extractor`/`extractor_explicit`/
`gemini_api_key` —— **正好是 saveCfg 寫的那四個鍵**,沒有 manifest。
## 為什麼我的測試沒抓到
`TestFreshInstallCollectorStarts` 是**從 saveCfg 出發**建 config ——
它測的是「我修的那條路」,不是「leo 正在走的那條路」。
**測試照著修正寫,就只會證明修正存在,不會證明問題解決。**
## 這版
- `fillRequired()` 抽出來,**讀取時也補**(升級路徑)+存檔時也補(新建路徑)
- 讀取時補完**寫回磁碟**——collector 是另一個行程,讀的是磁碟那份不是記憶體
- 補完寫進 app.log 留痕
## 驗
新測試 `TestExistingBrokenConfigSelfHeals` **從磁碟上已存在的壞 config 出發**
(內容逐字取自 leo 那份,只把值改假),驗三件:讀取後磁碟上有 manifest、
JSON 仍合法、**真的把執行檔當 collector 跑起來不再 exit 2**。
反向驗證:拿掉自我修復即紅(訊息就是 leo 撞到的那句)。全套測試綠。
⚠️ 更正 changelog:v0.18.11 原本寫「已經裝過舊版的人不用做任何事,更新後會自動補好」
—— **那句是假的**,已改成指向本版。
This commit is contained in:
@@ -2,5 +2,6 @@
|
||||
"v0.18.9": "8be9260eb542f527",
|
||||
"v0.18.10": "a440f044a849b7f2",
|
||||
"v0.18.11": "1538689e181444f6",
|
||||
"v0.18.12": "ce37982f05d8cb5b"
|
||||
"v0.18.12": "ce37982f05d8cb5b",
|
||||
"v0.18.13": "fbf7342d0d0e27b4"
|
||||
}
|
||||
|
||||
+31
-3
@@ -140,9 +140,39 @@ func loadCfg() (*directConfig, error) {
|
||||
return &directConfig{raw: map[string]any{}}, err
|
||||
}
|
||||
_ = json.Unmarshal(b, &c.raw)
|
||||
// 🔴 自我修復:舊版存出來的 config 可能少了 collector 的必填欄位。
|
||||
// 只補進記憶體不夠——collector 讀的是**磁碟上那份**,所以要寫回去。
|
||||
if fillRequired(c.raw) {
|
||||
if out, err := json.MarshalIndent(c.raw, "", " "); err == nil {
|
||||
_ = os.WriteFile(configPath(), out, 0o600)
|
||||
}
|
||||
if m, ok := c.raw["manifest"].(string); ok {
|
||||
c.Manifest = m
|
||||
}
|
||||
appLog("設定檔缺必填欄位,已自動補上 manifest=%v", c.raw["manifest"])
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// fillRequired 補上 collector 的必填欄位,回報有沒有真的補過。
|
||||
//
|
||||
// 🔴 leo 2026-08-06 兩輪教訓:
|
||||
//
|
||||
// 第一輪:`saveCfg` 從來不寫 `manifest`(collector 必填)⇒ 全新安裝一啟動就 exit 2。
|
||||
// 第二輪(**我第一次修錯**):只在 saveCfg 補 ⇒ **已經存在的壞設定永遠修不好**——
|
||||
// App 開起來只是**讀** config 然後啟動引擎,saveCfg 根本沒被呼叫。
|
||||
// leo 的 v0.18.12 實測仍是同一句「缺必填欄位:manifest」,重試 30 次。
|
||||
// 他的 config 剛好只有 saveCfg 寫的那四個鍵 ⇒ 鐵證。
|
||||
// ⇒ 補必填要在**讀取時**做(升級路徑),存檔時也做(新建路徑),兩條都要。
|
||||
func fillRequired(raw map[string]any) bool {
|
||||
changed := false
|
||||
if v, ok := raw["manifest"].(string); !ok || strings.TrimSpace(v) == "" {
|
||||
raw["manifest"] = filepath.Join(appDir(), "manifest.json")
|
||||
changed = true
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
func saveCfg(c *directConfig) error {
|
||||
if c.raw == nil {
|
||||
c.raw = map[string]any{}
|
||||
@@ -162,9 +192,7 @@ func saveCfg(c *directConfig) error {
|
||||
// ⇒ 畫面在「看守中/沒有在跑」之間閃、加資料夾也沒反應。
|
||||
// 為什麼開發機沒撞到:leo 的 Mac config 是舊版留下的、早就有這一欄
|
||||
// ——**「我這台好好的」正是這個 bug 能活到封測的原因**。
|
||||
if v, ok := c.raw["manifest"].(string); !ok || strings.TrimSpace(v) == "" {
|
||||
c.raw["manifest"] = filepath.Join(appDir(), "manifest.json")
|
||||
}
|
||||
fillRequired(c.raw)
|
||||
|
||||
out, err := json.MarshalIndent(c.raw, "", " ")
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// leo 2026-08-06 在 Windows 上真實壞掉的那份 config(逐字,只把值改成假的)。
|
||||
// 它剛好只有 saveCfg 會寫的四個鍵 —— 那就是「App 自己存出來、但少了必填欄位」的鐵證。
|
||||
const leoBrokenConfig = `{
|
||||
"accounts": [{"cypher_url":"https://x.workers.dev","namespace":"abc123","api_key":"abc123"}],
|
||||
"extractor": "workers-ai",
|
||||
"extractor_explicit": true,
|
||||
"gemini_api_key": ""
|
||||
}`
|
||||
|
||||
// TestExistingBrokenConfigSelfHeals 釘住我**第一次修錯**的那條路:
|
||||
//
|
||||
// 只在 saveCfg 補必填欄位 ⇒ 全新安裝好了,但**已經存在的壞設定永遠修不好**,
|
||||
// 因為 App 開起來只是「讀 config → 啟動引擎」,saveCfg 根本沒被呼叫。
|
||||
// leo 裝了 v0.18.12 仍看到同一句「缺必填欄位:manifest」、重試 30 次。
|
||||
//
|
||||
// 所以這支測試從**磁碟上已存在的壞 config** 出發,而不是從 saveCfg 出發。
|
||||
func TestExistingBrokenConfigSelfHeals(t *testing.T) {
|
||||
bin := filepath.Join(t.TempDir(), "arcrun-app-test")
|
||||
if out, err := exec.Command("go", "build", "-o", bin, ".").CombinedOutput(); err != nil {
|
||||
t.Fatalf("編不出執行檔:%v\n%s", err, out)
|
||||
}
|
||||
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
if err := os.MkdirAll(appDir(), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(configPath(), []byte(leoBrokenConfig), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// App 讀一次 config —— 這就是使用者「打開程式」會發生的事
|
||||
if _, err := loadCfg(); err != nil {
|
||||
t.Fatalf("讀不了 config:%v", err)
|
||||
}
|
||||
|
||||
// 修復必須**寫回磁碟**:collector 是另一個行程,讀的是磁碟那份,不是記憶體
|
||||
raw, err := os.ReadFile(configPath())
|
||||
if err != nil {
|
||||
t.Fatal(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("讀取後沒把 manifest 補進磁碟 ⇒ collector 仍會 exit 2(就是 leo v0.18.12 撞到的)")
|
||||
}
|
||||
|
||||
// 終極驗收:真的把執行檔當 collector 跑,不能再是 exit 2
|
||||
cmd := exec.Command(bin, collectorModeFlag, "direct", "--once", "--dry-run", "--config", configPath())
|
||||
cmd.Env = append(os.Environ(), "HOME="+home, "USERPROFILE="+home)
|
||||
out, err := cmd.CombinedOutput()
|
||||
code := 0
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
code = ee.ExitCode()
|
||||
}
|
||||
if code == 2 || strings.Contains(string(out), "缺必填欄位") {
|
||||
t.Fatalf("舊 config 修復後同步引擎仍被拒(exit=%d):%s", code, out)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user