Files
arcrun-collector/direct_t182_test.go
Leo 4d3a6a09a6 v0.18.9:collector 併進同一支執行檔——磁碟上不再攤出第二支 exe
leo 08-06 裁決:「不要兩支,寫成一支檔案」。

## 為什麼
v0.18.7-8 的「單一 exe」其實是**一支包著另一支**:collector.exe 被 go:embed
進 Arcrun.exe,執行時攤到 ~/.arcrun-rag/bin/ 再跑。
那正是防毒軟體眼中的 dropper 特徵 —— 封測者實撞
`Trojan:Win32/Sabsik.FL.A!ml`,檔案當場被隔離、自動刪除。

⚠️ 誠實界定:`!ml` 結尾=**機器學習判定**,Sabsik 是最常見的通用誤判家族,
   主因是「未簽章+下載次數少」,**不是**特別指向 dropper 行為。
   所以本次改動**不保證**解除誤判——真正的解是上架 MS Store(微軟簽章)。
   但「執行時把第二支 PE 寫到磁碟再執行」本來就該拿掉,這是對的方向且順手變小。

## 怎麼做
- `collector/` 39 個檔 `package main` → `package collector`,`main()` → 匯出的 `Run(args) int`
- 新增 `collector/cmd/collector/`(薄殼 CLI,讓單獨跑 collector 這條路仍可用)
- App 直接 import 該套件;`main()` 第一件事就判 `--collector`,是的話走 `collector.Run` 不碰 GUI
- `supervisor` 加 `ArgPrefix`,App 把 `BinPath` 指向 `os.Executable()` 自己
- 刪掉 `bundled_collector_{windows,other}.go`(embed + 攤檔那套)
- 三支打包腳本不再編/複製第二支;版本注入同時打到兩個 package
- build-win.sh 的機械閘改成**直接問它**:`--collector --version` 回得出版本才放行
  (舊閘是比大小,只能證明「有 embed」,證明不了「分派是對的」)

## 驗(真機實跑)
· `.app/Contents/MacOS/` 只有 **一個** 執行檔(原本兩個)
· 跑起來兩個行程是**同一個 exe**:
    …/MacOS/arcrun-app
    …/MacOS/arcrun-app --collector direct --config …
· `~/.arcrun-rag/bin` **不存在**(沒有任何東西被攤出來)
· 端到端:丟檔進看守資料夾 → collector.log `"status":"ingested","http_status":200`
· `lsappinfo` 仍是 `type="UIElement"`、`Version="0.18.9"`
· collector 39 檔測試全過;app 測試過;go vet 全綠;mac + windows 交叉編譯皆過
· 單檔 26MB → 22MB(不再夾帶第二份完整程式)
2026-08-06 16:00:47 +08:00

148 lines
5.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// direct_t182_test.go — t182:老 config 必須被**抹除**成 Workers AIleo 2026-08-04)。
//
// 「如果是我的 config 保持舊的,那新版裝上就要檢查,因為已經是 default worker AI
// **就要抹除改成用 Workers AI**,如果保持 Gemini 它不會改掉,**那就是失敗的**」
//
// 這組測試守的是 leo 08-04 實撞的三個真 bug
// 1. 更新到 v0.15.5 後托盤**兩個帳號都還顯示 Gemini**(帳號層舊值沒清)
// 2. 丟 PDF 進去**產不出卡**(因為根本沒走到 Workers AI,還在跑 Gemini
// 3. `workers-ai` 不在合法值清單裡 ⇒ 一旦寫進 config,daemon 直接起不來
package collector
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
// t182①:老 configextractor=gemma、帳號層也是 gemma、無 explicit
// → 每一層都要被抹成 workers-ai,而且**要寫回檔案**。
func TestT182ErasesLegacyExtractorAllLayers(t *testing.T) {
dir := t.TempDir()
p := writeDirectConfig(t, dir, map[string]any{
"manifest": filepath.Join(dir, "m.json"),
"extractor": "gemma", // 舊值:頂層
"gemini_api_key": "old-key",
// 注意:**沒有** extractor_explicit ⇒ 使用者從沒主動選過
"accounts": []map[string]any{
{
"cypher_url": "https://a.example", "namespace": "nsA",
"watch_folders": []string{"/tmp/a"},
"extractor": "gemma", // 舊值:帳號層(leo 的 config 正是這樣)
},
{
"cypher_url": "https://b.example", "namespace": "nsB",
"watch_folders": []string{"/tmp/b"},
"extractor": "claude", // 更舊的殘留值
},
},
})
cfg, err := LoadDirectConfig(p)
if err != nil {
t.Fatalf("LoadDirectConfig: %v", err)
}
if cfg.Extractor != "workers-ai" {
t.Errorf("① 頂層沒被抹除:got %q want workers-ai", cfg.Extractor)
}
for i, a := range cfg.Accounts {
if a.Extractor != "workers-ai" {
t.Errorf("① 帳號[%d] 沒被抹除:got %q want workers-aileo 實撞:兩個帳號都還顯示 Gemini)", i, a.Extractor)
}
}
// 金鑰要留著——Gemini 只是變選配,不是廢除(leo:「客戶說他要用 Gemini」)
if cfg.GeminiAPIKey != "old-key" {
t.Errorf("① Gemini 金鑰不該被清掉:got %q", cfg.GeminiAPIKey)
}
// 真的寫回檔案了嗎?(只改記憶體 ⇒ 托盤是另一個行程,還是會念 Gemini)
raw, err := os.ReadFile(p)
if err != nil {
t.Fatal(err)
}
var onDisk struct {
Extractor string `json:"extractor"`
Accounts []struct {
Extractor string `json:"extractor"`
} `json:"accounts"`
}
if err := json.Unmarshal(raw, &onDisk); err != nil {
t.Fatal(err)
}
if onDisk.Extractor != "workers-ai" {
t.Errorf("① **沒寫回檔案**:磁碟上頂層仍為 %q ⇒ 托盤下次讀還是念 Gemini", onDisk.Extractor)
}
for i, a := range onDisk.Accounts {
if a.Extractor != "workers-ai" {
t.Errorf("① **沒寫回檔案**:磁碟上帳號[%d] 仍為 %q", i, a.Extractor)
}
}
}
// t182②:主動選過 Gemini 的人不被動到(Gemini 是選配、不是廢除)。
func TestT182KeepsExplicitGeminiChoice(t *testing.T) {
dir := t.TempDir()
p := writeDirectConfig(t, dir, map[string]any{
"manifest": filepath.Join(dir, "m.json"),
"extractor": "gemma",
"extractor_explicit": true, // ← 使用者在「AI 設定…」主動選過
"gemini_api_key": "my-key",
"accounts": []map[string]any{
{"cypher_url": "https://a.example", "namespace": "nsA", "watch_folders": []string{"/tmp/a"}},
},
})
cfg, err := LoadDirectConfig(p)
if err != nil {
t.Fatalf("LoadDirectConfig: %v", err)
}
if cfg.Extractor != "gemma" {
t.Errorf("② 主動選過 Gemini 不該被抹掉:got %q", cfg.Extractor)
}
}
// t182③:`workers-ai` 必須是合法值——否則寫進 config 後 daemon 直接起不來。
// (v0.15.5 就踩到這顆:預設改了、驗證器沒跟上。)
func TestT182WorkersAIIsValidExtractor(t *testing.T) {
dir := t.TempDir()
p := writeDirectConfig(t, dir, map[string]any{
"manifest": filepath.Join(dir, "m.json"),
"extractor": "workers-ai",
"extractor_explicit": true,
"accounts": []map[string]any{
{"cypher_url": "https://a.example", "namespace": "nsA", "watch_folders": []string{"/tmp/a"}},
},
})
if _, err := LoadDirectConfig(p); err != nil {
t.Fatalf("③ workers-ai 應為合法值,卻載入失敗:%v", err)
}
}
// t182④:冪等——已是 workers-ai 再載一次不應改動任何東西。
func TestT182MigrationIdempotent(t *testing.T) {
dir := t.TempDir()
p := writeDirectConfig(t, dir, map[string]any{
"manifest": filepath.Join(dir, "m.json"),
"extractor": "workers-ai",
"accounts": []map[string]any{
{"cypher_url": "https://a.example", "namespace": "nsA",
"watch_folders": []string{"/tmp/a"}, "extractor": "workers-ai"},
},
})
before, err := os.ReadFile(p)
if err != nil {
t.Fatal(err)
}
if _, err := LoadDirectConfig(p); err != nil {
t.Fatal(err)
}
after, err := os.ReadFile(p)
if err != nil {
t.Fatal(err)
}
if string(before) != string(after) {
t.Error("④ 已是 workers-ai 不該再改寫檔案(非冪等=每次啟動都寫一次磁碟)")
}
}