import { describe, it, expect, vi, beforeEach } from 'vitest'; import { createBackupCommand } from '../../src/commands/backup.js'; const mockClient = { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn(), }; const log = vi.fn(); function makeCmd() { return createBackupCommand({ client: mockClient as never, log }); } describe('backup command', () => { beforeEach(() => { vi.resetAllMocks(); }); it('creates backup command', () => { expect(makeCmd().name()).toBe('backup'); }); it('shows status when enabled', async () => { mockClient.get.mockResolvedValue({ enabled: true, repoUrl: 'ssh://git@10.0.0.194:2222/michal/mcp-backup.git', gitReachable: true, lastSyncAt: new Date().toISOString(), lastPushAt: null, lastError: null, pendingCount: 0, }); await makeCmd().parseAsync([], { from: 'user' }); expect(mockClient.get).toHaveBeenCalledWith('/api/v1/backup/status'); expect(log).toHaveBeenCalledWith(expect.stringContaining('ssh://git@10.0.0.194:2222/michal/mcp-backup.git')); expect(log).toHaveBeenCalledWith(expect.stringContaining('synced')); }); it('shows disabled when not configured', async () => { mockClient.get.mockResolvedValue({ enabled: false, repoUrl: null, gitReachable: false, lastSyncAt: null, lastPushAt: null, lastError: null, pendingCount: 0, }); await makeCmd().parseAsync([], { from: 'user' }); expect(log).toHaveBeenCalledWith(expect.stringContaining('disabled')); }); it('shows pending count', async () => { mockClient.get.mockResolvedValue({ enabled: true, repoUrl: 'ssh://git@host/repo.git', gitReachable: true, lastSyncAt: null, lastPushAt: null, lastError: null, pendingCount: 5, }); await makeCmd().parseAsync([], { from: 'user' }); expect(log).toHaveBeenCalledWith(expect.stringContaining('5 changes pending')); }); it('shows SSH public key in status when enabled', async () => { mockClient.get.mockResolvedValue({ enabled: true, repoUrl: 'ssh://git@host/repo.git', publicKey: 'ssh-ed25519 AAAA... mcpd@mcpctl.local', gitReachable: true, lastSyncAt: null, lastPushAt: null, lastError: null, pendingCount: 0, }); await makeCmd().parseAsync([], { from: 'user' }); expect(log).toHaveBeenCalledWith(expect.stringContaining('ssh-ed25519 AAAA... mcpd@mcpctl.local')); }); it('shows setup instructions when disabled', async () => { mockClient.get.mockResolvedValue({ enabled: false, repoUrl: null, publicKey: null, gitReachable: false, lastSyncAt: null, lastPushAt: null, lastError: null, pendingCount: 0, }); await makeCmd().parseAsync([], { from: 'user' }); expect(log).toHaveBeenCalledWith(expect.stringContaining('mcpctl create secret backup-ssh')); }); it('shows commit log', async () => { mockClient.get.mockResolvedValue({ entries: [ { hash: 'abc1234567890', date: '2026-03-08T10:00:00Z', author: 'mcpd ', message: 'Update server grafana', manual: false }, { hash: 'def4567890123', date: '2026-03-07T09:00:00Z', author: 'Michal ', message: 'Manual fix', manual: true }, ], }); await makeCmd().parseAsync(['log'], { from: 'user' }); expect(mockClient.get).toHaveBeenCalledWith('/api/v1/backup/log?limit=20'); expect(log).toHaveBeenCalledWith(expect.stringContaining('COMMIT')); expect(log).toHaveBeenCalledWith(expect.stringContaining('abc1234')); expect(log).toHaveBeenCalledWith(expect.stringContaining('[manual]')); }); }); describe('backup restore subcommands', () => { beforeEach(() => { vi.resetAllMocks(); }); it('lists restore points', async () => { mockClient.get.mockResolvedValue({ entries: [ { hash: 'abc1234567890', date: '2026-03-08T10:00:00Z', author: 'mcpd ', message: 'Sync' }, ], }); await makeCmd().parseAsync(['restore', 'list'], { from: 'user' }); expect(mockClient.get).toHaveBeenCalledWith('/api/v1/backup/log?limit=30'); expect(log).toHaveBeenCalledWith(expect.stringContaining('abc1234')); }); it('shows restore diff preview', async () => { mockClient.post.mockResolvedValue({ targetCommit: 'abc1234567890', targetDate: '2026-03-08T10:00:00Z', targetMessage: 'Snapshot', added: ['servers/new.yaml'], removed: ['servers/old.yaml'], modified: ['projects/default.yaml'], }); await makeCmd().parseAsync(['restore', 'diff', 'abc1234'], { from: 'user' }); expect(mockClient.post).toHaveBeenCalledWith('/api/v1/backup/restore/preview', { commit: 'abc1234' }); expect(log).toHaveBeenCalledWith(expect.stringContaining('+ servers/new.yaml')); expect(log).toHaveBeenCalledWith(expect.stringContaining('- servers/old.yaml')); expect(log).toHaveBeenCalledWith(expect.stringContaining('~ projects/default.yaml')); }); it('requires --force for restore', async () => { mockClient.post.mockResolvedValue({ targetCommit: 'abc1234567890', targetDate: '2026-03-08T10:00:00Z', targetMessage: 'Snapshot', added: ['servers/new.yaml'], removed: [], modified: [], }); await makeCmd().parseAsync(['restore', 'to', 'abc1234'], { from: 'user' }); expect(mockClient.post).toHaveBeenCalledWith('/api/v1/backup/restore/preview', { commit: 'abc1234' }); expect(mockClient.post).not.toHaveBeenCalledWith('/api/v1/backup/restore', expect.anything()); expect(log).toHaveBeenCalledWith(expect.stringContaining('--force')); }); it('executes restore with --force', async () => { mockClient.post .mockResolvedValueOnce({ targetCommit: 'abc1234567890', targetDate: '2026-03-08T10:00:00Z', targetMessage: 'Snapshot', added: ['servers/new.yaml'], removed: [], modified: [], }) .mockResolvedValueOnce({ branchName: 'timeline/20260308-100000', applied: 1, deleted: 0, errors: [], }); await makeCmd().parseAsync(['restore', 'to', 'abc1234', '--force'], { from: 'user' }); expect(mockClient.post).toHaveBeenCalledWith('/api/v1/backup/restore', { commit: 'abc1234' }); expect(log).toHaveBeenCalledWith(expect.stringContaining('1 applied')); expect(log).toHaveBeenCalledWith(expect.stringContaining('timeline/20260308-100000')); }); it('reports restore errors', async () => { mockClient.post .mockResolvedValueOnce({ targetCommit: 'abc1234567890', targetDate: '2026-03-08T10:00:00Z', targetMessage: 'Snapshot', added: [], removed: [], modified: ['servers/broken.yaml'], }) .mockResolvedValueOnce({ branchName: 'timeline/20260308-100000', applied: 0, deleted: 0, errors: ['Failed to apply servers/broken.yaml: invalid YAML'], }); await makeCmd().parseAsync(['restore', 'to', 'abc1234', '--force'], { from: 'user' }); expect(log).toHaveBeenCalledWith('Errors:'); expect(log).toHaveBeenCalledWith(expect.stringContaining('invalid YAML')); }); });