diff --git a/src/split-brain-status.test.ts b/src/split-brain-status.test.ts index ad3dc38..152a714 100644 --- a/src/split-brain-status.test.ts +++ b/src/split-brain-status.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'; import { collectSplitBrainIssues, deriveSplitBrainReadiness, + buildSplitBrainFixHints, type SplitBrainStatus, } from './split-brain-status.js'; @@ -82,3 +83,29 @@ describe('collectSplitBrainIssues', () => { expect(issues).toEqual(['built-in knowledge artifact is incomplete']); }); }); + +describe('buildSplitBrainFixHints', () => { + it('returns no hints for a ready status', () => { + expect(buildSplitBrainFixHints(readyStatus())).toEqual([]); + }); + + it('returns skills-memory hint for missing artifact', () => { + const hints = buildSplitBrainFixHints({ ...readyStatus(), skillsArtifact: 'missing' }); + expect(hints).toContain('Import knowledge artifact: just setup --step skills-memory'); + }); + + it('returns skills-memory hint for outdated artifact', () => { + const hints = buildSplitBrainFixHints({ ...readyStatus(), skillsArtifact: 'incomplete' }); + expect(hints).toContain('Refresh knowledge artifact: just setup --step skills-memory'); + }); + + it('returns db hint for unavailable skills db', () => { + const hints = buildSplitBrainFixHints({ ...readyStatus(), skillsDb: 'missing' }); + expect(hints).toContain('Fix skills DB: check PostgreSQL, then run: just setup-db'); + }); + + it('returns skills-init hint for missing runtime lookup', () => { + const hints = buildSplitBrainFixHints({ ...readyStatus(), skillsRuntimeLookup: 'missing' }); + expect(hints).toContain('Restore runtime lookup: just setup --step skills-init'); + }); +}); diff --git a/src/split-brain-status.ts b/src/split-brain-status.ts index dd77650..8d485fd 100644 --- a/src/split-brain-status.ts +++ b/src/split-brain-status.ts @@ -221,3 +221,30 @@ export function collectSplitBrainIssues(status: SplitBrainStatus): string[] { return issues; } + +// Returns operator-facing fix hints for each degraded component. +// Kept separate from collectSplitBrainIssues so startup report stays concise +// while /brain can show the full remediation path. +export function buildSplitBrainFixHints(status: SplitBrainStatus): string[] { + const hints: string[] = []; + + if (status.skillsDb !== 'available') { + hints.push('Fix skills DB: check PostgreSQL, then run: just setup-db'); + } + if (status.memoryDb !== 'available') { + hints.push('Fix memory DB: check PostgreSQL, then run: just setup-db'); + } + if (status.skillsArtifact !== 'ready') { + if (status.skillsArtifact === 'missing') { + hints.push('Import knowledge artifact: just setup --step skills-memory'); + } else { + // outdated or incomplete + hints.push('Refresh knowledge artifact: just setup --step skills-memory'); + } + } + if (status.skillsRuntimeLookup !== 'present') { + hints.push('Restore runtime lookup: just setup --step skills-init'); + } + + return hints; +} diff --git a/src/telegram-brain-command.test.ts b/src/telegram-brain-command.test.ts index aa2aeec..312844a 100644 --- a/src/telegram-brain-command.test.ts +++ b/src/telegram-brain-command.test.ts @@ -19,6 +19,7 @@ vi.mock('./split-brain-status.js', () => ({ })), collectSplitBrainIssues: vi.fn(() => []), deriveSplitBrainReadiness: vi.fn(() => 'ready'), + buildSplitBrainFixHints: vi.fn(() => []), })); afterEach(() => { @@ -74,4 +75,37 @@ describe('/brain command', () => { expect(replies[0]?.text).not.toContain('(vv0.7.0)'); expect(replies[0]?.opts).toEqual({ parse_mode: 'HTML' }); }); + + it('shows fix hints when degraded', async () => { + const { collectSplitBrainIssues, deriveSplitBrainReadiness, buildSplitBrainFixHints } = + await import('./split-brain-status.js'); + vi.mocked(deriveSplitBrainReadiness).mockReturnValue('degraded'); + vi.mocked(collectSplitBrainIssues).mockReturnValue([ + 'built-in knowledge artifact is outdated (loaded: v0.6.0, expected: v0.7.0)', + ]); + vi.mocked(buildSplitBrainFixHints).mockReturnValue([ + 'Refresh knowledge artifact: just setup --step skills-memory', + ]); + + process.env.TELEGRAM_ADMIN_IDS = '123'; + process.env.TELEGRAM_OPS_CHAT_ID = 'tg:999'; + + const replies: Array<{ text: string; opts?: unknown }> = []; + const ctxArg = { + from: { id: 123 }, + chat: { id: 999 }, + reply: async (text: string, opts?: unknown) => { + replies.push({ text, opts }); + }, + }; + + const { handleBrainCommand } = await import('./telegram-commands.js'); + await handleBrainCommand(ctxArg, 'tg:999'); + + expect(replies).toHaveLength(1); + const text = replies[0]?.text ?? ''; + expect(text).toContain('⚠ Brain: degraded'); + expect(text).toContain('⚠ built-in knowledge artifact is outdated'); + expect(text).toContain('→ Refresh knowledge artifact: just setup --step skills-memory'); + }); }); diff --git a/src/telegram-commands.ts b/src/telegram-commands.ts index 09c6171..9dc7946 100644 --- a/src/telegram-commands.ts +++ b/src/telegram-commands.ts @@ -74,6 +74,7 @@ import { collectSplitBrainStatus, collectSplitBrainIssues, deriveSplitBrainReadiness, + buildSplitBrainFixHints, } from './split-brain-status.js'; import { buildTestReport, @@ -783,6 +784,13 @@ export async function handleBrainCommand( for (const issue of issues) { lines.push(`⚠ ${issue}`); } + const hints = buildSplitBrainFixHints(status); + if (hints.length > 0) { + lines.push(''); + for (const hint of hints) { + lines.push(`→ ${hint}`); + } + } } await ctxArg.reply(lines.join('\n'), { parse_mode: 'HTML' });