fix(t126): 萃取引擎/金鑰改每帳號一份——多帳號不再互相覆蓋
leo:「2 個 CF 帳號一個填 gemma4 一個填 Mistral,daemon 會用誰的?別人也加就會有一樣的疑問」 實證:金鑰只有機器層一份 ⇒ 後連的覆蓋前一個。 修:AccountConfig 加 Extractor/GeminiAPIKey/LLMModel;帳號層優先、空值繼承機器層; 既有 config 遷移(頂層金鑰複製到各帳號,冪等);連線只寫該帳號不覆蓋機器層; 托盤帳號標題顯示「· Gemini」/「· Claude」(一眼看出誰用誰)。 三模組 go test 全綠(總管親跑)。(實作=子 CC;驗證+commit=總管)
This commit is contained in:
@@ -622,6 +622,142 @@ func TestRunDirectOnceAccountResultsTagged(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── t126:每帳號獨立萃取設定 ──────────────────────────────────────────────────
|
||||
|
||||
// t126①:帳號層 Extractor/GeminiAPIKey/LLMModel 有值時優先覆蓋機器層。
|
||||
func TestMakeAccountSubConfigExtractorOverride(t *testing.T) {
|
||||
parent := &DirectConfig{
|
||||
Manifest: "/tmp/m.json",
|
||||
Extractor: "claude",
|
||||
GeminiAPIKey: "machine-key",
|
||||
LLMModel: "model-machine",
|
||||
PollSec: 5,
|
||||
MaxRemoved: DefaultMaxRemovedRatio,
|
||||
}
|
||||
acc := AccountConfig{
|
||||
CypherURL: "https://a.example",
|
||||
Namespace: "ns",
|
||||
WatchFolders: []string{"/tmp"},
|
||||
Extractor: "gemma", // 帳號層覆蓋
|
||||
GeminiAPIKey: "account-key", // 帳號層覆蓋
|
||||
LLMModel: "model-acc", // 帳號層覆蓋
|
||||
}
|
||||
sub := parent.makeAccountSubConfig(acc)
|
||||
if sub.Extractor != "gemma" {
|
||||
t.Errorf("① 帳號層 Extractor 應優先,got %q", sub.Extractor)
|
||||
}
|
||||
if sub.GeminiAPIKey != "account-key" {
|
||||
t.Errorf("① 帳號層 GeminiAPIKey 應優先,got %q", sub.GeminiAPIKey)
|
||||
}
|
||||
if sub.LLMModel != "model-acc" {
|
||||
t.Errorf("① 帳號層 LLMModel 應優先,got %q", sub.LLMModel)
|
||||
}
|
||||
}
|
||||
|
||||
// t126②:帳號層空字串不算「有值」,應繼承機器層。
|
||||
func TestMakeAccountSubConfigExtractorFallback(t *testing.T) {
|
||||
parent := &DirectConfig{
|
||||
Manifest: "/tmp/m.json",
|
||||
Extractor: "gemma",
|
||||
GeminiAPIKey: "machine-key",
|
||||
LLMModel: "model-machine",
|
||||
PollSec: 5,
|
||||
MaxRemoved: DefaultMaxRemovedRatio,
|
||||
}
|
||||
acc := AccountConfig{
|
||||
CypherURL: "https://a.example",
|
||||
Namespace: "ns",
|
||||
WatchFolders: []string{"/tmp"},
|
||||
// Extractor/GeminiAPIKey/LLMModel 全空 → 繼承機器層
|
||||
}
|
||||
sub := parent.makeAccountSubConfig(acc)
|
||||
if sub.Extractor != "gemma" {
|
||||
t.Errorf("② 帳號層空時應繼承機器層 Extractor,got %q", sub.Extractor)
|
||||
}
|
||||
if sub.GeminiAPIKey != "machine-key" {
|
||||
t.Errorf("② 帳號層空時應繼承機器層 GeminiAPIKey,got %q", sub.GeminiAPIKey)
|
||||
}
|
||||
if sub.LLMModel != "model-machine" {
|
||||
t.Errorf("② 帳號層空時應繼承機器層 LLMModel,got %q", sub.LLMModel)
|
||||
}
|
||||
}
|
||||
|
||||
// t126③ 遷移:機器層金鑰複製到沒有金鑰的帳號;已有金鑰的帳號不覆蓋。
|
||||
func TestLoadDirectConfigMigratesCopyKeyToAccounts(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
p := writeDirectConfig(t, dir, map[string]any{
|
||||
"manifest": filepath.Join(dir, "m.json"),
|
||||
"extractor": "gemma",
|
||||
"gemini_api_key": "top-level-key",
|
||||
"llm_model": "model-x",
|
||||
"accounts": []map[string]any{
|
||||
{
|
||||
"cypher_url": "https://a.example",
|
||||
"namespace": "nsA",
|
||||
"watch_folders": []string{"/tmp/a"},
|
||||
// 沒有 gemini_api_key → 應複製頂層
|
||||
},
|
||||
{
|
||||
"cypher_url": "https://b.example",
|
||||
"namespace": "nsB",
|
||||
"watch_folders": []string{"/tmp/b"},
|
||||
"gemini_api_key": "b-own-key", // 已有自己的 key → 不覆蓋
|
||||
},
|
||||
},
|
||||
})
|
||||
cfg, err := LoadDirectConfig(p)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadDirectConfig: %v", err)
|
||||
}
|
||||
// account A:無 key → 應複製頂層
|
||||
if cfg.Accounts[0].GeminiAPIKey != "top-level-key" {
|
||||
t.Errorf("③ account A 應複製頂層 key,got %q", cfg.Accounts[0].GeminiAPIKey)
|
||||
}
|
||||
if cfg.Accounts[0].Extractor != "gemma" {
|
||||
t.Errorf("③ account A 應複製頂層 extractor,got %q", cfg.Accounts[0].Extractor)
|
||||
}
|
||||
if cfg.Accounts[0].LLMModel != "model-x" {
|
||||
t.Errorf("③ account A 應複製頂層 llm_model,got %q", cfg.Accounts[0].LLMModel)
|
||||
}
|
||||
// account B:已有自己的 key → 不覆蓋
|
||||
if cfg.Accounts[1].GeminiAPIKey != "b-own-key" {
|
||||
t.Errorf("③ account B 已有 key 不應被頂層覆蓋,got %q", cfg.Accounts[1].GeminiAPIKey)
|
||||
}
|
||||
// 頂層保留(複製非搬移)
|
||||
if cfg.GeminiAPIKey != "top-level-key" {
|
||||
t.Errorf("③ 頂層 key 應保留(複製非搬移),got %q", cfg.GeminiAPIKey)
|
||||
}
|
||||
}
|
||||
|
||||
// t126④ 冪等:帳號已有 extractor/key 時再跑遷移不覆蓋。
|
||||
func TestLoadDirectConfigMigrationIdempotent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
p := writeDirectConfig(t, dir, map[string]any{
|
||||
"manifest": filepath.Join(dir, "m.json"),
|
||||
"extractor": "gemma",
|
||||
"gemini_api_key": "top-key",
|
||||
"accounts": []map[string]any{
|
||||
{
|
||||
"cypher_url": "https://a.example",
|
||||
"namespace": "nsA",
|
||||
"watch_folders": []string{"/tmp/a"},
|
||||
"extractor": "claude", // 帳號已有 → 不應被頂層 "gemma" 覆蓋
|
||||
"gemini_api_key": "own-key", // 帳號已有 → 不應被頂層 key 覆蓋
|
||||
},
|
||||
},
|
||||
})
|
||||
cfg, err := LoadDirectConfig(p)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadDirectConfig: %v", err)
|
||||
}
|
||||
if cfg.Accounts[0].Extractor != "claude" {
|
||||
t.Errorf("④ 帳號已有 extractor 不應被頂層覆蓋,got %q", cfg.Accounts[0].Extractor)
|
||||
}
|
||||
if cfg.Accounts[0].GeminiAPIKey != "own-key" {
|
||||
t.Errorf("④ 帳號已有 key 不應被頂層覆蓋,got %q", cfg.Accounts[0].GeminiAPIKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandHomeEdgeCases(t *testing.T) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil || home == "" {
|
||||
|
||||
Reference in New Issue
Block a user