feat(daemon-beta t14/t15/t17): supervisor log 檔+removed 清本地卡+托盤建新資料夾

- t14 supervisor:子行程 stdout(tee)/stderr 全落 ~/.arcrun-rag/collector.log,
  >5MB 改名 .old 重開(兩代輪替);落 log 失敗一律吞掉不擋看守。新增 2 測試
  (輸出進檔+輪替觸發)。
- t15 direct removed(extractor 模式):雲端 takedown 2xx 後同步刪本地
  system-dev/wiki/cards/<頁名>.md(存在才刪;刪失敗只記 warning 不擋)。
  新增 2 測試(刪原檔→本地卡被清/卡已不在照常下架)。
- t17 arcrun-tray:tray 選單+設定視窗加「建立新資料夾並看守…」——
  widget.Entry 輸入名稱→ ~/ 底下 mkdir(已存在沿用;擋路徑分隔與 ..)→
  addWatchFolder+saveConfig+重啟看守+rebuildTray。

collector: go vet ✓、go test -count=1 ./... 43/43 綠;arcrun-tray: go build ✓(產物已刪)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 15:06:21 +08:00
parent 6e5009ff35
commit b9d880413d
5 changed files with 317 additions and 8 deletions
+66
View File
@@ -4,6 +4,7 @@ import (
"os"
"path/filepath"
"runtime"
"strings"
"sync/atomic"
"testing"
"time"
@@ -110,6 +111,71 @@ func TestSupervisorStopThenStartNotLeftStopped(t *testing.T) {
}
}
// t14:子行程 stdoutJSON 輪)與 stderr 都要落進 log 檔。
func TestSupervisorWritesChildOutputToLog(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("fake collector script 走 POSIX shell")
}
dir := t.TempDir()
bin := filepath.Join(dir, "fake-collector.sh")
body := "#!/bin/sh\n" +
"printf '{\"at\":\"2026-07-24T00:00:00Z\",\"folder\":\"/tmp/kb\",\"results\":[]}\\n'\n" +
"echo 'boom-stderr-line' 1>&2\n" +
"sleep 2\n"
if err := os.WriteFile(bin, []byte(body), 0o755); err != nil {
t.Fatal(err)
}
logPath := filepath.Join(t.TempDir(), "collector.log")
s := New(bin, "cfg.json")
s.LogPath = logPath
s.Backoff = 20 * time.Millisecond
s.Start()
defer s.Stop()
waitFor(t, 2*time.Second, func() bool {
data, _ := os.ReadFile(logPath)
return strings.Contains(string(data), "2026-07-24T00:00:00Z") &&
strings.Contains(string(data), "[stderr] boom-stderr-line")
}, "stdout JSON 與 stderr 行都寫進 log 檔")
}
// t14log 檔超過上限 → 改名 .old 重開(簡單兩代輪替)。
func TestSupervisorLogRotation(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("fake collector script 走 POSIX shell")
}
dir := t.TempDir()
bin := filepath.Join(dir, "fake-collector.sh")
// 一輪印 ~120 bytes × 20 輪 上限 400 bytes → 必觸發輪替
body := "#!/bin/sh\ni=0\nwhile [ $i -lt 20 ]; do\n" +
"printf '{\"at\":\"2026-07-24T00:00:00Z\",\"folder\":\"/tmp/kb-padding-padding-padding-padding-padding\",\"results\":[]}\\n'\n" +
"i=$((i+1))\ndone\nsleep 2\n"
if err := os.WriteFile(bin, []byte(body), 0o755); err != nil {
t.Fatal(err)
}
logPath := filepath.Join(t.TempDir(), "collector.log")
s := New(bin, "cfg.json")
s.LogPath = logPath
s.LogMaxBytes = 400
s.Backoff = 20 * time.Millisecond
s.Start()
defer s.Stop()
waitFor(t, 2*time.Second, func() bool {
_, err := os.Stat(logPath + ".old")
return err == nil
}, "輪替後 .old 檔存在")
// 現役檔已重開=大小不會無限長(上限 + 單次寫入緩衝的餘裕)
waitFor(t, 2*time.Second, func() bool { return s.Status().Rounds >= 20 }, "20 rounds parsed")
fi, err := os.Stat(logPath)
if err != nil {
t.Fatalf("現役 log 檔應存在:%v", err)
}
if fi.Size() > 2*400+4096 {
t.Fatalf("現役 log 檔未受控:size=%d", fi.Size())
}
}
func TestSupervisorOnChangeFires(t *testing.T) {
bin := fakeCollector(t, 2, true)
s := New(bin, "cfg.json")