feat: add foreground monitor command with live status + recent logs

- serve now periodically writes account-pool state (active/cooldown/
  disabled, cooldown until, token expiry) to workbuddy-status.json,
  refreshed every 3s and immediately on cooldown/disable/refresh events
- new 'monitor' command runs in foreground: refreshes a live panel of
  all accounts (usable/cooldown/expired + reasons) every N seconds,
  optionally showing recent logs via journalctl (-journal) or a log
  file (-logfile)
- statusSnapshotFile excluded from git via .gitignore
- unit tests for tailLines and status snapshot states
This commit is contained in:
CangShui
2026-09-04 09:36:51 +08:00
parent 86b70baf29
commit 61aa053816
4 changed files with 405 additions and 12 deletions
+67
View File
@@ -354,3 +354,70 @@ func TestCollectConfiguredAuthPathsAutoDiscoverFallback(t *testing.T) {
}
t.Logf("fallback path: %v", paths)
}
// 验证 tailLines 读取文件末尾 N 行
func TestTailLines(t *testing.T) {
dir := t.TempDir()
f := dir + "/test.log"
content := "line1\nline2\nline3\nline4\nline5\n"
if err := os.WriteFile(f, []byte(content), 0600); err != nil {
t.Fatal(err)
}
lines, err := tailLines(f, 2)
if err != nil {
t.Fatal(err)
}
if len(lines) != 2 || lines[0] != "line4" || lines[1] != "line5" {
t.Fatalf("tailLines got %v", lines)
}
t.Logf("tailLines(2) = %v", lines)
}
// 验证状态快照写入:含 active / cooldown / disabled 三种状态
func TestWriteStatusSnapshot(t *testing.T) {
oldDir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
defer os.Chdir(oldDir)
dir := t.TempDir()
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
accountMu.Lock()
accounts = []*Account{
{Path: "a.json", Auth: &StoredAuth{
Auth: StoredTokens{AccessToken: "x", ExpiresAt: time.Now().Add(time.Hour).Unix()},
Account: StoredAccount{Nickname: "alice", UID: "u1"},
}},
{Path: "b.json", Auth: &StoredAuth{
Auth: StoredTokens{AccessToken: "x", ExpiresAt: time.Now().Add(time.Hour).Unix()},
Account: StoredAccount{Nickname: "bob", UID: "u2"},
}, CooldownUntil: time.Now().Add(30 * time.Minute), CooldownMsg: "频率限制"},
{Path: "c.json", Disabled: true, DisabledReason: "revoked", Nickname: "carol", UID: "u3"},
}
accountMu.Unlock()
writeStatusSnapshot()
data, err := os.ReadFile(statusSnapshotFile)
if err != nil {
t.Fatal(err)
}
var snap statusSnapshot
if err := json.Unmarshal(data, &snap); err != nil {
t.Fatal(err)
}
if len(snap.Accounts) != 3 {
t.Fatalf("expected 3 accounts in snapshot, got %d", len(snap.Accounts))
}
states := map[string]bool{}
for _, a := range snap.Accounts {
states[a.State] = true
}
if !states["active"] || !states["cooldown"] || !states["disabled"] {
t.Fatalf("expected all three states in snapshot, got %v", states)
}
t.Logf("snapshot states: %v", states)
}