From a22d4e4436a26bce318fcf7def83faf2d4553d4d Mon Sep 17 00:00:00 2001 From: richblack Date: Thu, 6 Aug 2026 18:54:14 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AB=AF=E5=88=B0=E7=AB=AF=E9=A9=97=E8=AD=89?= =?UTF-8?q?=EF=BC=9A=E5=85=A8=E6=96=B0=E5=AE=89=E8=A3=9D=E7=9C=9F=E7=9A=84?= =?UTF-8?q?=E8=B7=91=E5=BE=97=E8=B5=B7=E5=90=8C=E6=AD=A5=E5=BC=95=E6=93=8E?= =?UTF-8?q?=EF=BC=88=E8=87=AA=E5=B7=B1=E9=A9=97=EF=BC=8C=E4=B8=8D=E6=8E=A8?= =?UTF-8?q?=E7=B5=A6=20leo=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 自走警察點破:我把「在 Windows 全新裝一次確認」推給 leo,但這件事我自己驗得了。 新增 `TestFreshInstallCollectorStarts`: 全新 HOME → App 的 saveCfg 存 config → **編出真正的執行檔、用 `--collector direct --once --dry-run` 把它自己跑起來** → 斷言不是 exit 2、輸出沒有「缺必填欄位」。 為什麼要再加這支(已經有 LoadDirectConfig 那支了): **「函式回傳 nil」與「行程活得下去」是兩件事**——事故正是死在後者。 反向驗證(拿掉修正)重現 leo 截圖上那句一字不差的訊息: collector direct: config 缺必填欄位:manifest ⇒ 這支不是假測試。修正放回後全套綠。 順帶修測試自身的坑:先 `go build` 再改 HOME——先改 HOME 會讓 Go 把**唯讀的** 模組快取寫進暫存目錄,測試結束清不掉(TempDir RemoveAll permission denied)。 剩下真正只有 Windows 能驗的:實機安裝後的行為(托盤/畫面),已列 CP 待辦。 --- cmd/arcrun-app/freshinstall_e2e_test.go | 63 +++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 cmd/arcrun-app/freshinstall_e2e_test.go diff --git a/cmd/arcrun-app/freshinstall_e2e_test.go b/cmd/arcrun-app/freshinstall_e2e_test.go new file mode 100644 index 0000000..b60c5ea --- /dev/null +++ b/cmd/arcrun-app/freshinstall_e2e_test.go @@ -0,0 +1,63 @@ +package main + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +// TestFreshInstallCollectorStarts 端到端釘住 leo 2026-08-06 Windows 事故的完整鏈條: +// +// 全新安裝 → App 存 config → supervisor 用「同一支執行檔 + --collector」拉起同步引擎。 +// +// 事故當時這條鏈在最後一步斷掉:config 少了 `manifest` ⇒ `exit status 2` ⇒ 無限重拉。 +// 上一支測試只驗到「LoadDirectConfig 讀得下去」;這支**真的把執行檔跑起來**, +// 因為「函式回傳 nil」與「行程活得下去」是兩件事——事故就是死在後者。 +// +// 用 --once --dry-run:只走到解析設定就收工,不 POST、不寫 manifest,不動到真資料。 +func TestFreshInstallCollectorStarts(t *testing.T) { + // 🔴 先編再改 HOME:把 HOME 指到暫存目錄後才 `go build`,Go 會把**唯讀的**模組快取 + // 寫進那個目錄,測試結束清不掉(實撞:TempDir RemoveAll permission denied)。 + 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) + + 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) + } + + // 用剛才編好的執行檔自己當 collector 跑(=supervisor 在真機上做的事) + 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() + } else if err != nil { + t.Fatalf("跑不起來:%v", err) + } + + if code == 2 { + t.Fatalf("同步引擎以 exit status 2 結束=設定被拒,就是 leo 撞到的無限重試。\n輸出:%s", out) + } + if strings.Contains(string(out), "缺必填欄位") { + t.Fatalf("設定仍缺必填欄位:%s", out) + } + t.Logf("✅ 全新安裝的設定,同步引擎吃得下去(exit=%d)", code) +}