feat(t104): 多帳號同時看守——切換概念消滅

leo 架構依據:「它只是一個門,幾個帳號通過它同步並沒有影響」+
「我不是 Google Drive……不提供暫存空間,daemon 工作輕巧,多帳號只是頁簽問題」
(D-daemon-not-Drive)。
Accounts[] 每帳號獨立連線+資料夾;舊 config 冪等遷移 accounts[0];
逐帳號同步一敗不擋全;status.json 分帳;托盤每帳號一分組;
「連上知識庫」→「+新增帳號…」(append 非替換);t86 切換清空退役;
t101 刪除作用於正確帳號。+873/-149、collector 5+tray 5 新測試,
兩模組 go test 全綠(總管親跑)。
(實作=子 CC;驗證+commit=總管)
This commit is contained in:
2026-07-28 16:45:14 +08:00
parent f8450815d3
commit bf1f0d9991
5 changed files with 871 additions and 147 deletions
+269 -86
View File
@@ -59,29 +59,47 @@ func versionLabel() string {
return version
}
// directConfig 是 collector direct 的設定(與 collector/direct.go 的 DirectConfig 同結構;
// 這裡只需讀寫 watch_folder,其餘由安裝器帶入)。
// accountCfg 是單一帳號的連線設定(t104 多帳號同時看守)。
type accountCfg struct {
InstanceName string `json:"instance_name,omitempty"`
Email string `json:"email,omitempty"`
CypherURL string `json:"cypher_url"`
Namespace string `json:"namespace"`
APIKey string `json:"api_key,omitempty"`
WatchFolders []string `json:"watch_folders,omitempty"`
Libraries map[string]string `json:"libraries,omitempty"` // t52:資料夾→庫對映
}
// directConfig 是 collector direct 的設定(與 collector/direct.go 的 DirectConfig 同結構)。
type directConfig struct {
WatchFolder string `json:"watch_folder,omitempty"` // 單數舊制(第一個資料夾的鏡像,維持相容
WatchFolders []string `json:"watch_folders,omitempty"` // 多資料夾(daemon-beta task 1;完整勾選 UItask 7
Manifest string `json:"manifest"`
CypherURL string `json:"cypher_url"`
Namespace string `json:"namespace"`
APIKey string `json:"api_key,omitempty"`
Email string `json:"email,omitempty"` // 實例主身分(t26;人人記得自己的 email,CF 全程隱形)
InstanceName string `json:"instance_name,omitempty"` // 暱稱(t26 選配;不取就顯示 email)
Library string `json:"library,omitempty"`
Extractor string `json:"extractor,omitempty"` // t54:連線精靈帶入(claude/gemma
ClaudeBin string `json:"claude_bin,omitempty"` // t92collector fallback 找到後回寫,下次直達
Libraries map[string]string `json:"libraries,omitempty"` // t52:資料夾→庫對映(key=絕對路徑)
IngestWF string `json:"ingest_workflow,omitempty"`
RemovedWF string `json:"removed_workflow,omitempty"`
PollSec int `json:"poll_interval_sec,omitempty"`
MaxRemoved float64 `json:"max_removed_ratio,omitempty"`
// t104:多帳號清單(新制)。有值時頂層連線欄位僅保留讀取相容
Accounts []accountCfg `json:"accounts,omitempty"`
WatchFolder string `json:"watch_folder,omitempty"` // 單數舊制(第一個資料夾的鏡像,維持相容)
WatchFolders []string `json:"watch_folders,omitempty"` // 多資料夾(舊制)
Manifest string `json:"manifest"`
CypherURL string `json:"cypher_url,omitempty"` // 舊制(新制走 Accounts
Namespace string `json:"namespace,omitempty"` // 舊制
APIKey string `json:"api_key,omitempty"`
Email string `json:"email,omitempty"`
InstanceName string `json:"instance_name,omitempty"`
Library string `json:"library,omitempty"`
Extractor string `json:"extractor,omitempty"` // 機器層級:claude/gemma
ClaudeBin string `json:"claude_bin,omitempty"` // t92collector fallback 找到後回寫
Libraries map[string]string `json:"libraries,omitempty"` // t52 舊制:資料夾→庫對映
IngestWF string `json:"ingest_workflow,omitempty"`
RemovedWF string `json:"removed_workflow,omitempty"`
PollSec int `json:"poll_interval_sec,omitempty"`
MaxRemoved float64 `json:"max_removed_ratio,omitempty"`
}
// ── t91 萃取狀態可見性 ──────────────────────────────────────────────────────────
// trayAccountStatus 對映 per-account 狀態(來自 status.json account_details)。
type trayAccountStatus struct {
CloudVersion string `json:"cloud_version,omitempty"`
CloudCheckOK bool `json:"cloud_check_ok"`
}
// traySyncStatus 對映 collector 寫出的 ~/.arcrun-rag/status.json(僅含托盤需要的欄位)。
type traySyncStatus struct {
LastSync string `json:"last_sync,omitempty"`
@@ -90,9 +108,11 @@ type traySyncStatus struct {
Failures []trayExtFail `json:"failures,omitempty"`
ExtractorOK bool `json:"extractor_ok"`
ExtractorError string `json:"extractor_error,omitempty"`
// t103:雲端版本偵測
CloudVersion string `json:"cloud_version,omitempty"` // collector 每輪 GET /health 取得的 bundle_version
CloudCheckOK bool `json:"cloud_check_ok"` // /health 可達才 truefalse 不判定(靜默)
// 頂層雲端欄位(向後相容,單帳號時有值)
CloudVersion string `json:"cloud_version,omitempty"`
CloudCheckOK bool `json:"cloud_check_ok"`
// t104per-account 狀態(key = cypher_url host
AccountDetails map[string]trayAccountStatus `json:"account_details,omitempty"`
}
// ── t103 雲端版本偵測 ──────────────────────────────────────────────────────────
@@ -173,11 +193,22 @@ func loadConfig() *directConfig {
if data, err := os.ReadFile(configPath()); err == nil {
_ = json.Unmarshal(data, c)
}
if c.WatchFolder == "" && len(c.WatchFolders) == 0 {
c.WatchFolder = defaultWatchFolder()
// t104:舊格式遷移——頂層 CypherURL → Accounts[0](冪等)
if len(c.Accounts) == 0 && strings.TrimSpace(c.CypherURL) != "" {
c.Accounts = []accountCfg{{
InstanceName: c.InstanceName,
Email: c.Email,
CypherURL: c.CypherURL,
Namespace: c.Namespace,
APIKey: c.APIKey,
Libraries: c.Libraries,
WatchFolders: c.legacyFolders(),
}}
// 寫回磁碟(冪等:下次讀 Accounts 已存在就不再遷移)
_ = saveConfig(c)
}
if c.WatchFolder == "" && len(c.WatchFolders) > 0 {
c.WatchFolder = c.WatchFolders[0] // 單數欄位=第一根鏡像(舊 collector 相容
if len(c.Accounts) == 0 && c.WatchFolder == "" && len(c.WatchFolders) == 0 {
c.WatchFolder = defaultWatchFolder() // 尚未連線的預設值(不觸發遷移
}
if c.Manifest == "" {
c.Manifest = filepath.Join(appDir(), "manifest.json")
@@ -185,6 +216,20 @@ func loadConfig() *directConfig {
return c
}
// legacyFolders 回傳頂層(舊制)監看根清單(不含 accounts,供遷移使用)。
func (c *directConfig) legacyFolders() []string {
seen := map[string]bool{}
var out []string
add := func(p string) {
if p == "" || seen[p] { return }
seen[p] = true
out = append(out, p)
}
add(c.WatchFolder)
for _, f := range c.WatchFolders { add(f) }
return out
}
// addWatchFolder 把資料夾加進監看清單(去重、保序),並維持單數欄位=第一根的鏡像。
// 完整的「勾選/移除」UI 是 task 7;本函式先保證資料層正確。
func addWatchFolder(c *directConfig, p string) {
@@ -203,22 +248,23 @@ func addWatchFolder(c *directConfig, p string) {
c.WatchFolder = c.WatchFolders[0]
}
// Folders 回傳監看根清單(正規化:單數舊制併入、去重、保序;與 collector DirectConfig 同語意)。
// Folders 回傳監看根清單(t104:多帳號時彙整所有帳號資料夾;舊制時回頂層清單)。
// 供 registerLibraries 等跨帳號操作使用。
func (c *directConfig) Folders() []string {
seen := map[string]bool{}
var out []string
add := func(p string) {
if p == "" || seen[p] {
return
if len(c.Accounts) > 0 {
seen := map[string]bool{}
var out []string
for _, acc := range c.Accounts {
for _, f := range acc.WatchFolders {
if f != "" && !seen[f] {
seen[f] = true
out = append(out, f)
}
}
}
seen[p] = true
out = append(out, p)
return out
}
add(c.WatchFolder)
for _, f := range c.WatchFolders {
add(f)
}
return out
return c.legacyFolders()
}
// removeWatchFolder 把資料夾移出監看清單,並維持單數欄位=第一根鏡像(清單空=兩欄皆空)。
@@ -495,9 +541,97 @@ func libraryNameFor(cfg *directConfig, folder string) string {
// isConnected 判斷「這台機器已經連上某個知識庫」——沒有就該跳連線精靈。
func isConnected(cfg *directConfig) bool {
if len(cfg.Accounts) > 0 {
for _, acc := range cfg.Accounts {
if strings.TrimSpace(acc.CypherURL) != "" && strings.TrimSpace(acc.Namespace) != "" {
return true
}
}
return false
}
return strings.TrimSpace(cfg.CypherURL) != "" && strings.TrimSpace(cfg.Namespace) != ""
}
// addOrUpdateAccount 把連線精靈取回的設定加入(或更新)accounts 清單(t104)。
// 同 host 已存在 → 更新連線欄位,WatchFolders 保留不動;不同 host → append 新帳號。
// 回傳 true 代表是全新帳號(首次加入)。
func addOrUpdateAccount(cfg *directConfig, r *daemonConfigResp) bool {
newHost := shortCypherHost(r.Config.CypherURL)
for i := range cfg.Accounts {
if shortCypherHost(cfg.Accounts[i].CypherURL) == newHost {
// 更新現有帳號(保留 WatchFolders 與 Libraries
cfg.Accounts[i].CypherURL = strings.TrimSuffix(strings.TrimSpace(r.Config.CypherURL), "/")
cfg.Accounts[i].Namespace = r.Config.Namespace
cfg.Accounts[i].Email = r.Config.Email
if r.Config.InstanceName != "" {
cfg.Accounts[i].InstanceName = r.Config.InstanceName
}
if r.Config.Extractor != "" {
cfg.Extractor = r.Config.Extractor // 機器層級
}
return false
}
}
// 新帳號
acc := accountCfg{
InstanceName: r.Config.InstanceName,
Email: r.Config.Email,
CypherURL: strings.TrimSuffix(strings.TrimSpace(r.Config.CypherURL), "/"),
Namespace: r.Config.Namespace,
}
if acc.APIKey == "" {
acc.APIKey = acc.Namespace
}
cfg.Accounts = append(cfg.Accounts, acc)
if r.Config.Extractor != "" {
cfg.Extractor = r.Config.Extractor
}
if cfg.Manifest == "" {
cfg.Manifest = filepath.Join(appDir(), "manifest.json")
}
return true
}
// addAccountWatchFolder 把資料夾加進指定帳號的監看清單(t104)。
func addAccountWatchFolder(cfg *directConfig, accIdx int, p string) {
if p == "" || accIdx >= len(cfg.Accounts) {
return
}
acc := &cfg.Accounts[accIdx]
for _, f := range acc.WatchFolders {
if f == p {
return
}
}
acc.WatchFolders = append(acc.WatchFolders, p)
}
// removeAccountWatchFolder 把資料夾從指定帳號的監看清單移出(t104)。
func removeAccountWatchFolder(cfg *directConfig, accIdx int, p string) {
if accIdx >= len(cfg.Accounts) {
return
}
acc := &cfg.Accounts[accIdx]
var out []string
for _, f := range acc.WatchFolders {
if f != p {
out = append(out, f)
}
}
acc.WatchFolders = out
}
// accountDisplayName 回傳帳號顯示名稱(暱稱 > email > host)。
func accountDisplayName(acc accountCfg) string {
if n := strings.TrimSpace(acc.InstanceName); n != "" {
return n
}
if e := strings.TrimSpace(acc.Email); e != "" {
return e
}
return shortCypherHost(acc.CypherURL)
}
func main() {
a := app.NewWithID("dev.arcrun.rag.tray")
cfg := loadConfig()
@@ -572,28 +706,34 @@ func main() {
go func() {
r, err := fetchConfigByLogin(urlEntry.Text, emailEntry.Text, pwEntry.Text)
if err != nil {
// fyne 的 UI 更新須回主執行緒;用 dialog 顯示即可(它內部處理)
dialog.ShowError(err, win)
// t75:把剛才打的網址與 email 帶回去,讓他只改錯的那個字,不必整段重打。
showConnectWizardWith(urlEntry.Text, emailEntry.Text)
return
}
switched := applyRemoteConfig(cfg, r)
// t104append 新帳號或更新同 host 帳號——不清空其他帳號資料夾(t86 切換清空退役)
isNew := addOrUpdateAccount(cfg, r)
if err := saveConfig(cfg); err != nil {
dialog.ShowError(err, win)
return
}
// t52:把本機資料夾對應的庫報上去自動登記(地端幾個資料夾=雲端幾個庫)
// t52:把本機資料夾對應的庫報上去自動登記
if lerr := registerLibraries(urlEntry.Text, emailEntry.Text, pwEntry.Text, cfg); lerr != nil {
fmt.Println("庫登記略過:", lerr) // 不擋連線(可能是還沒選資料夾)
}
restartWatch()
rebuildTray()
// t86:換了知識庫實例時,說明已清空資料夾清單的原因,請用戶重新選資料夾。
if switched {
dialog.ShowInformation("連上了", "因為換了知識庫,為了避免把舊資料夾誤傳到新知識庫,請重新用「+ 新增知識資料夾…」選擇要同步的資料夾。", win)
accName := strings.TrimSpace(r.Config.InstanceName)
if accName == "" {
accName = strings.TrimSpace(r.Config.Email)
}
if accName == "" {
accName = shortCypherHost(r.Config.CypherURL)
}
if isNew {
dialog.ShowInformation("帳號已加入", "已加入「"+accName+"」。\n接下來用該帳號底下的「+ 新增知識資料夾…」選擇要同步的資料夾就好。", win)
} else {
dialog.ShowInformation("連上了", "已連上「"+connectionStatusLabel(cfg.InstanceName, cfg.Email)+"」。\n接下來用選單「+ 新增知識資料夾…」挑要同步的資料夾就好。", win)
dialog.ShowInformation("帳號已更新", "「"+accName+"」的連線設定已更新。", win)
}
}()
}, win)
@@ -610,7 +750,12 @@ func main() {
if err != nil || uri == nil {
return
}
addWatchFolder(cfg, uri.Path())
// t104:多帳號模式加進第一個帳號;無帳號時走舊制
if len(cfg.Accounts) > 0 {
addAccountWatchFolder(cfg, 0, uri.Path())
} else {
addWatchFolder(cfg, uri.Path())
}
if err := saveConfig(cfg); err != nil {
dialog.ShowError(err, win)
return
@@ -661,18 +806,82 @@ func main() {
supStatus := sup.Status()
statusItem.Label = "狀態:" + buildStatusLabel(supStatus, sync, cfg.Extractor)
// t26:選單最頂顯示「連線中:<暱稱||email>」(CF/cypher_url 全程不現身這一行);
// 有 cypher_url 時再加一行縮短的 hostfyne MenuItem 無 tooltip,取捨見 shortCypherHost 注解)。
connItem := fyne.NewMenuItem(connectionStatusLabel(cfg.InstanceName, cfg.Email), nil)
connItem.Disabled = true
versionItem := fyne.NewMenuItem("版本 "+versionLabel(), nil)
versionItem.Disabled = true
items := []*fyne.MenuItem{connItem}
if host := shortCypherHost(cfg.CypherURL); host != "" {
hostItem := fyne.NewMenuItem("→ "+host, nil)
hostItem.Disabled = true
items = append(items, hostItem)
var items []*fyne.MenuItem
// t104per-account 分組——每個帳號獨立一段(帳號名稱標題 + 資料夾 + 新增按鈕)。
if len(cfg.Accounts) > 0 {
for ai, acc := range cfg.Accounts {
accIdx := ai // capture by value,供 closure 正確引用
accHost := shortCypherHost(acc.CypherURL)
// 帳號標題(不可點)
headerItem := fyne.NewMenuItem("◉ "+accountDisplayName(acc), nil)
headerItem.Disabled = true
items = append(items, headerItem)
// t103 per-account:雲端版本過舊時顯示更新提示(帶帳號識別)
if accSt, ok := sync.AccountDetails[accHost]; ok && trayCloudVersionStale(accSt.CloudVersion, accSt.CloudCheckOK) {
items = append(items, fyne.NewMenuItem(" ⚠ 知識庫需要更新(點我)", func() {
if u, err := neturl.Parse("https://install.arcrun.dev/"); err == nil {
_ = a.OpenURL(u)
}
}))
}
// 此帳號的資料夾(t101:子選單「刪除這個知識庫」作用於正確帳號)
for _, f := range acc.WatchFolders {
folder := f // capture
aidx := accIdx // capture
it := fyne.NewMenuItem(" 📁 "+filepath.Base(folder), func() {
if u, err := neturl.Parse("file://" + folder); err == nil {
_ = a.OpenURL(u)
}
})
it.ChildMenu = fyne.NewMenu("",
fyne.NewMenuItem("刪除這個知識庫", func() {
removeAccountWatchFolder(cfg, aidx, folder)
if err := saveConfig(cfg); err != nil {
dialog.ShowError(err, win)
return
}
restartWatch()
rebuildTray()
}),
)
items = append(items, it)
}
// 此帳號的「+ 新增知識資料夾…」
aidxAdd := accIdx // capture
items = append(items, fyne.NewMenuItem(" 新增知識資料夾…", func() {
win.Show()
win.RequestFocus()
dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) {
if err != nil || uri == nil {
return
}
addAccountWatchFolder(cfg, aidxAdd, uri.Path())
if err := saveConfig(cfg); err != nil {
dialog.ShowError(err, win)
return
}
restartWatch()
rebuildTray()
win.Hide()
}, win)
}))
items = append(items, fyne.NewMenuItemSeparator())
}
} else {
// 尚未連線(accounts 空)——簡化顯示
notConn := fyne.NewMenuItem("未連線到任何知識庫", nil)
notConn.Disabled = true
items = append(items, notConn, fyne.NewMenuItemSeparator())
}
items = append(items, statusItem)
// t91:有萃取失敗時加警告項,點開顯示哪些檔出了什麼問題(白話)。
if cfg.Extractor != "" && sync.ExtractFailed > 0 {
@@ -687,48 +896,22 @@ func main() {
})
items = append(items, failItem)
}
// t103:雲端版本過舊(或老實例)時提示用戶更新,點了開瀏覽器到安裝頁。
if trayCloudVersionStale(sync.CloudVersion, sync.CloudCheckOK) {
// t103 向後相容(單帳號舊 status.json 仍有頂層 CloudVersion):若無 AccountDetails 則看頂層
if len(sync.AccountDetails) == 0 && trayCloudVersionStale(sync.CloudVersion, sync.CloudCheckOK) {
items = append(items, fyne.NewMenuItem("⚠ 知識庫需要更新(點我)", func() {
if u, err := neturl.Parse("https://install.arcrun.dev/"); err == nil {
_ = a.OpenURL(u)
}
}))
}
items = append(items, fyne.NewMenuItemSeparator())
for _, f := range cfg.Folders() {
folder := f // capture
it := fyne.NewMenuItem("📁 "+filepath.Base(folder), func() {
if u, err := neturl.Parse("file://" + folder); err == nil {
_ = a.OpenURL(u)
}
})
it.ChildMenu = fyne.NewMenu("",
fyne.NewMenuItem("刪除這個知識庫", func() {
removeWatchFolder(cfg, folder)
if err := saveConfig(cfg); err != nil {
dialog.ShowError(err, win)
return
}
restartWatch()
rebuildTray()
}),
)
items = append(items, it)
}
items = append(items,
fyne.NewMenuItem(" 新增知識資料夾…", addAction),
fyne.NewMenuItem("🔗 連上知識庫…(換帳號/重新連線)", func() { showConnectWizard() }), // t54
fyne.NewMenuItemSeparator(),
syncNowItem, // t98:立刻同步
pauseItem,
// 版本列(leo 2026-07-27:「daemon 要加上版本編號」):停用態=純顯示不可點。
// 放在「結束」正上方——使用者要回報問題時,眼睛已經在選單底部這一區。
// t104:「連上知識庫」改為「新增帳號」——精靈成功後 append 到 accounts,不換掉舊帳號
fyne.NewMenuItem(" 新增帳號…", func() { showConnectWizard() }),
fyne.NewMenuItemSeparator(),
versionItem,
// t55leo 2026-07-26 實撞:「沒有 exit 的選項,我無法關閉它」):
// 沒有出口的 app 是設計缺陷——用戶只剩「強制結束」可用,而托盤 app 連 Dock 都沒有
// LSUIElement),右鍵結束那條路也不存在。這裡給一條明確的出路:
// 先停子行程(collector),再退整個 app,不留孤兒行程。
fyne.NewMenuItem("結束 Arcrun RAG", func() {
sup.Stop()
a.Quit()
+135
View File
@@ -306,3 +306,138 @@ func TestDirectConfigPreservesClaudeBin(t *testing.T) {
t.Errorf("round-trip 不一致:got %q, want %q", back.ClaudeBin, c.ClaudeBin)
}
}
// ── t104:多帳號同時看守 ──────────────────────────────────────────────────────────
// TestAddOrUpdateAccount_NewAccount:空 accounts → 加入新帳號。
func TestAddOrUpdateAccount_NewAccount(t *testing.T) {
cfg := &directConfig{}
r := &daemonConfigResp{}
r.Config.CypherURL = "https://instance1.workers.dev"
r.Config.Namespace = "ns1"
r.Config.Email = "user@a.com"
r.Config.InstanceName = "Work"
isNew := addOrUpdateAccount(cfg, r)
if !isNew {
t.Error("新帳號應回 true")
}
if len(cfg.Accounts) != 1 {
t.Fatalf("應新增 1 個帳號,got %d", len(cfg.Accounts))
}
if cfg.Accounts[0].Email != "user@a.com" {
t.Errorf("email 錯:%s", cfg.Accounts[0].Email)
}
if cfg.Accounts[0].InstanceName != "Work" {
t.Errorf("instance_name 錯:%s", cfg.Accounts[0].InstanceName)
}
}
// TestAddOrUpdateAccount_SameHostUpdates:同 host → 更新連線資訊,WatchFolders 不清空。
func TestAddOrUpdateAccount_SameHostUpdates(t *testing.T) {
cfg := &directConfig{
Accounts: []accountCfg{{
CypherURL: "https://instance1.workers.dev",
Namespace: "ns1",
Email: "old@a.com",
WatchFolders: []string{"/path/to/folder"},
}},
}
r := &daemonConfigResp{}
r.Config.CypherURL = "https://instance1.workers.dev"
r.Config.Namespace = "ns1"
r.Config.Email = "new@a.com"
isNew := addOrUpdateAccount(cfg, r)
if isNew {
t.Error("同 host 應回 false(更新,非新增)")
}
if len(cfg.Accounts) != 1 {
t.Fatalf("不應新增帳號,got %d", len(cfg.Accounts))
}
if cfg.Accounts[0].Email != "new@a.com" {
t.Errorf("email 未更新:%s", cfg.Accounts[0].Email)
}
if len(cfg.Accounts[0].WatchFolders) == 0 {
t.Error("同 host 更新不應清空資料夾(t86 退役)")
}
}
// TestAddOrUpdateAccount_DifferentHostAdds:不同 host → 兩個帳號並存。
func TestAddOrUpdateAccount_DifferentHostAdds(t *testing.T) {
cfg := &directConfig{
Accounts: []accountCfg{{
CypherURL: "https://instance1.workers.dev",
Namespace: "ns1",
}},
}
r := &daemonConfigResp{}
r.Config.CypherURL = "https://instance2.workers.dev"
r.Config.Namespace = "ns2"
isNew := addOrUpdateAccount(cfg, r)
if !isNew {
t.Error("不同 host 應回 true(新增)")
}
if len(cfg.Accounts) != 2 {
t.Fatalf("應有 2 個帳號,got %d", len(cfg.Accounts))
}
}
// TestRemoveAccountWatchFolder_CorrectAccount:刪帳號 A 的資料夾不影響帳號 B(t104+t101)。
func TestRemoveAccountWatchFolder_CorrectAccount(t *testing.T) {
cfg := &directConfig{
Accounts: []accountCfg{
{CypherURL: "https://a.workers.dev", WatchFolders: []string{"/folder1", "/folder2"}},
{CypherURL: "https://b.workers.dev", WatchFolders: []string{"/folder3"}},
},
}
removeAccountWatchFolder(cfg, 0, "/folder1")
if len(cfg.Accounts[0].WatchFolders) != 1 {
t.Fatalf("account[0] 應剩 1 個資料夾,got %v", cfg.Accounts[0].WatchFolders)
}
if cfg.Accounts[0].WatchFolders[0] != "/folder2" {
t.Errorf("account[0] 剩餘應是 /folder2got %s", cfg.Accounts[0].WatchFolders[0])
}
if len(cfg.Accounts[1].WatchFolders) != 1 || cfg.Accounts[1].WatchFolders[0] != "/folder3" {
t.Errorf("account[1] 資料夾不應改變,got %v", cfg.Accounts[1].WatchFolders)
}
}
// TestAccountDisplayName:暱稱 > email > hostt26 延伸)。
func TestAccountDisplayName(t *testing.T) {
cases := []struct {
acc accountCfg
want string
}{
{accountCfg{InstanceName: "書房", Email: "a@b.com", CypherURL: "https://c.dev"}, "書房"},
{accountCfg{Email: "a@b.com", CypherURL: "https://c.dev"}, "a@b.com"},
{accountCfg{CypherURL: "https://arcrun-cypher-executor.acct.workers.dev"}, "arcrun-cypher-executor.acct.workers.dev"},
}
for _, c := range cases {
got := accountDisplayName(c.acc)
if got != c.want {
t.Errorf("accountDisplayName(%+v) = %q, want %q", c.acc, got, c.want)
}
}
}
// TestDirectConfigFolders_MultiAccount:多帳號 Folders() 彙整所有帳號資料夾。
func TestDirectConfigFolders_MultiAccount(t *testing.T) {
cfg := &directConfig{
Accounts: []accountCfg{
{WatchFolders: []string{"/a", "/b"}},
{WatchFolders: []string{"/c"}},
},
}
got := cfg.Folders()
want := []string{"/a", "/b", "/c"}
if len(got) != len(want) {
t.Fatalf("Folders() = %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Errorf("Folders()[%d] = %q, want %q", i, got[i], want[i])
}
}
}