cc6534dd39
解 issue #18+#17(leo 指定併做)。 ## #17 進行中狀態——真兇是「工作時間對用戶隱形」 leo:「按『立刻同步』後很快就回到『看守中』,看起來好像就做完了, 這時使用者一看沒做完啊,就覺得是 bug」。 真兇:collector **只在一輪跑完後**才印 JSON ⇒ 托盤無從得知「正在跑」。 修:開工前先印 phase:"start"、跑完印 phase:"done"; supervisor 新增 StateSyncing;托盤/主視窗顯示「同步中… 正在讀檔並整理成知識卡」。 形狀相容:兩筆都有 at,舊版托盤只會多算一次 round,不會壞。 實測(真的跑一輪 --once): 第1筆 phase='start' at=21:31:26 第2筆 phase='done' at=21:31:27 ## #18 主視窗(新檔 mainwindow.go) leo:「daemon 設定項越來越多,不可能統統塞在下拉選單」 「參考 Google Drive 設定畫面:點擊 daemon 就開一個視窗,佔螢幕約 1/2」 「資料夾清單要可捲動——我每個 gitea 專案都要同步,那就是幾十個,根本塞不下」 - 920x620 視窗,關窗=隱藏(daemon 續跑) - 上:大字狀態+副標(上次同步/已整理幾份/失敗幾份;有錯誤優先顯示) - 中:widget.List 虛擬捲動的資料夾清單(帳號標題+其下資料夾攤平成單層) - 下:白話動作列(加入資料夾/新增知識庫帳號/AI 設定/檢查更新) - 每秒自動刷新 ⇒ 同步中看得到在動 - 無帳號時引導去連線(onboarding),不是丟錯誤或空面板 - **所有動作複用既有 handler**,不在視窗那邊重寫一份邏輯 托盤選單第一項加「開啟 Arcrun…」——leo 已點破兩次 「能力做出來了,入口沒出現在用戶會看的地方」(docs 沒連結/MCP 零處提及), 這次做完就讓它看得見。 測試:t192_test.go 八則(狀態文案四態/同步中要說明在做什麼/ 副標優先顯示錯誤/副標不可空白/清單攤平且 accIdx 正確/60 個資料夾/無帳號空清單)全過。 collector 與 supervisor 全套綠。 ⚠️ 未驗:fyne GUI 的實際外觀需真機開窗(無螢幕環境驗不到), CIS 視覺套用待 leo 看過畫面再調。
122 lines
4.5 KiB
Go
122 lines
4.5 KiB
Go
package main
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"arcrun-rag/collector/supervisor"
|
||
)
|
||
|
||
// t192/#17:正在跑就要**看得出來在跑**。
|
||
// leo:「按『立刻同步』後很快就回到『看守中』,看起來好像就做完了,
|
||
// 這時使用者一看沒做完啊,就覺得是 bug」。
|
||
func TestMainWindowStatusShowsSyncing(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
state supervisor.State
|
||
want string
|
||
}{
|
||
{"同步中要講在做什麼", supervisor.StateSyncing, "同步中"},
|
||
{"看守中", supervisor.StateWatching, "看守中"},
|
||
{"啟動中", supervisor.StateStarting, "啟動中"},
|
||
{"出錯要說會自動重試", supervisor.StateError, "重試"},
|
||
}
|
||
for _, c := range cases {
|
||
t.Run(c.name, func(t *testing.T) {
|
||
got := mainWindowStatusText(supervisor.Status{State: c.state})
|
||
if !strings.Contains(got, c.want) {
|
||
t.Errorf("state=%s → %q,應含 %q", c.state, got, c.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// 同步中的文案必須說明「正在做什麼」,不能只寫兩個字讓人乾等。
|
||
func TestSyncingTextExplainsWhatItIsDoing(t *testing.T) {
|
||
got := mainWindowStatusText(supervisor.Status{State: supervisor.StateSyncing})
|
||
if !strings.Contains(got, "讀檔") && !strings.Contains(got, "知識卡") {
|
||
t.Errorf("同步中應說明正在做什麼,got %q", got)
|
||
}
|
||
}
|
||
|
||
// 副標:有錯誤時**優先顯示錯誤**(不能被「上次同步 12:34」蓋掉)。
|
||
func TestSubTextPrioritisesError(t *testing.T) {
|
||
got := mainWindowSubText(
|
||
supervisor.Status{LastRoundAt: time.Now()},
|
||
traySyncStatus{ExtractorOK: false, ExtractorError: "還沒連上知識庫"},
|
||
)
|
||
if !strings.Contains(got, "還沒連上知識庫") {
|
||
t.Errorf("有錯誤時應優先顯示,got %q", got)
|
||
}
|
||
}
|
||
|
||
// 副標:正常時要看得到「上次同步時間」與「已整理幾份」。
|
||
func TestSubTextShowsProgress(t *testing.T) {
|
||
got := mainWindowSubText(
|
||
supervisor.Status{LastRoundAt: time.Date(2026, 8, 4, 21, 5, 0, 0, time.Local)},
|
||
traySyncStatus{ExtractorOK: true, ExtractedOK: 3, ExtractFailed: 1},
|
||
)
|
||
for _, want := range []string{"21:05", "已整理 3", "1 份失敗"} {
|
||
if !strings.Contains(got, want) {
|
||
t.Errorf("副標應含 %q,got %q", want, got)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 沒同步過時不能顯示空白(空白=用戶不知道是壞了還是還沒開始)。
|
||
func TestSubTextNeverEmpty(t *testing.T) {
|
||
got := mainWindowSubText(supervisor.Status{}, traySyncStatus{ExtractorOK: true})
|
||
if strings.TrimSpace(got) == "" {
|
||
t.Error("副標不可空白——用戶會分不清是壞了還是還沒開始")
|
||
}
|
||
}
|
||
|
||
// t192/#18:資料夾清單攤平——帳號一列標題,其下每個資料夾各一列。
|
||
// 用扁平清單才能吃 widget.List 的虛擬捲動(leo:「幾十個,根本塞不下」)。
|
||
func TestRebuildRowsFlattensAccountsAndFolders(t *testing.T) {
|
||
m := &mainWindow{cfg: &directConfig{Accounts: []accountCfg{
|
||
{InstanceName: "geek6688", CypherURL: "https://a.workers.dev", WatchFolders: []string{"/p/one", "/p/two"}},
|
||
{InstanceName: "youlin", CypherURL: "https://b.workers.dev", WatchFolders: []string{"/p/three"}},
|
||
}}}
|
||
m.rebuildRows()
|
||
|
||
if len(m.rows) != 5 { // 2 標題 + 3 資料夾
|
||
t.Fatalf("應攤平成 5 列,got %d:%+v", len(m.rows), m.rows)
|
||
}
|
||
if !m.rows[0].isHeader || m.rows[0].title != "geek6688" {
|
||
t.Errorf("第 0 列應是帳號標題 geek6688,got %+v", m.rows[0])
|
||
}
|
||
if m.rows[1].isHeader || m.rows[1].title != "/p/one" {
|
||
t.Errorf("第 1 列應是資料夾 /p/one,got %+v", m.rows[1])
|
||
}
|
||
// 刪除要作用在正確帳號上(t101 踩過:刪錯帳號的資料夾)
|
||
if m.rows[4].accIdx != 1 {
|
||
t.Errorf("第 4 列(youlin 的資料夾)accIdx 應為 1,got %d", m.rows[4].accIdx)
|
||
}
|
||
}
|
||
|
||
// 幾十個資料夾也要能攤平(不是為了效能,是確保沒有隱藏上限)。
|
||
func TestRebuildRowsHandlesManyFolders(t *testing.T) {
|
||
folders := make([]string, 60)
|
||
for i := range folders {
|
||
folders[i] = "/gitea/project-" + string(rune('a'+i%26))
|
||
}
|
||
m := &mainWindow{cfg: &directConfig{Accounts: []accountCfg{
|
||
{InstanceName: "leo", CypherURL: "https://x.workers.dev", WatchFolders: folders},
|
||
}}}
|
||
m.rebuildRows()
|
||
if len(m.rows) != 61 {
|
||
t.Errorf("60 個資料夾+1 標題應為 61 列,got %d", len(m.rows))
|
||
}
|
||
}
|
||
|
||
// 沒有任何帳號時清單是空的(走 onboarding,不是顯示壞掉的空面板)。
|
||
func TestRebuildRowsEmptyWhenNoAccount(t *testing.T) {
|
||
m := &mainWindow{cfg: &directConfig{}}
|
||
m.rebuildRows()
|
||
if len(m.rows) != 0 {
|
||
t.Errorf("無帳號時應為空清單,got %d 列", len(m.rows))
|
||
}
|
||
}
|