Files
magpie/tests/config/loader-validation.test.ts
Li Liu 2b0e1ba711 refactor: comprehensive codebase improvements across 7 phases
Phase A - Quick fixes:
- Remove debug logging that leaked prompt content (qwen-code)
- Fix orchestrator session leak with try/finally cleanup
- CJK-aware token estimation for better accuracy
- Issue parser validation (line > 0, endLine >= line, non-empty fields)
- Improved similarity matching with stop words filtering and description weight

Phase B - Medium fixes:
- Add retry utility with exponential backoff for API providers
- Config validation at load time (required fields, empty API key warnings)
- GitHub PR comment deduplication (skip already-posted comments)
- Ctrl+C graceful exit for interactive comment review

Phase C - Structured logging:
- Logger class with debug/info/warn/error levels (MAGPIE_LOG_LEVEL env var)

Phase D - Type safety:
- Replace `any` types with proper types across discuss.ts, review.ts,
  issue-parser.ts, commenter.ts, repo-orchestrator.ts, history-collector.ts

Phase E - Session helper extraction:
- CliSessionHelper class shared by 4 CLI providers, reducing duplication

Phase F - Split review.ts (1991 → 6 files):
- review.ts (command + action), interactive.ts, repo-review.ts,
  session-cmds.ts, utils.ts, types.ts

Phase G - Tests:
- 6 new test files (retry, logger, session-helper, issue-parser-enhanced,
  loader-validation, orchestrator-session)
- Fix pre-existing test failures (commenter, anthropic)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 22:46:46 +08:00

108 lines
3.5 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import type { MagpieConfig } from '../../src/config/types.js'
// Mock fs and yaml
vi.mock('fs', () => ({
readFileSync: vi.fn(),
existsSync: vi.fn()
}))
vi.mock('yaml', () => ({
parse: vi.fn()
}))
// Mock logger to suppress output
vi.mock('../../src/utils/logger.js', () => ({
logger: {
warn: vi.fn(),
error: vi.fn(),
info: vi.fn(),
debug: vi.fn()
}
}))
import { loadConfig } from '../../src/config/loader.js'
import { existsSync, readFileSync } from 'fs'
import { parse } from 'yaml'
import { logger } from '../../src/utils/logger.js'
const validConfig: MagpieConfig = {
defaults: { max_rounds: 3, check_convergence: true },
providers: {
anthropic: { api_key: 'test-key' }
},
reviewers: {
claude: { model: 'anthropic:claude-3-5-sonnet', prompt: 'Review this code' }
},
summarizer: { model: 'anthropic:claude-3-5-sonnet', prompt: 'Summarize' },
analyzer: { model: 'anthropic:claude-3-5-sonnet', prompt: 'Analyze' }
}
describe('loadConfig - validation', () => {
beforeEach(() => {
vi.resetAllMocks()
vi.mocked(existsSync).mockReturnValue(true)
vi.mocked(readFileSync).mockReturnValue('yaml content')
})
it('loads valid config without error', () => {
vi.mocked(parse).mockReturnValue(structuredClone(validConfig))
expect(() => loadConfig('/path/to/config.yaml')).not.toThrow()
})
it('throws when max_rounds <= 0', () => {
const bad = structuredClone(validConfig)
bad.defaults.max_rounds = 0
vi.mocked(parse).mockReturnValue(bad)
expect(() => loadConfig('/path/to/config.yaml')).toThrow('max_rounds must be > 0')
})
it('throws when no reviewers defined', () => {
const bad = structuredClone(validConfig)
bad.reviewers = {}
vi.mocked(parse).mockReturnValue(bad)
expect(() => loadConfig('/path/to/config.yaml')).toThrow('at least one reviewer')
})
it('throws when reviewer missing model', () => {
const bad = structuredClone(validConfig)
bad.reviewers = { claude: { model: '', prompt: 'test' } }
vi.mocked(parse).mockReturnValue(bad)
expect(() => loadConfig('/path/to/config.yaml')).toThrow('missing a "model" field')
})
it('throws when reviewer missing prompt', () => {
const bad = structuredClone(validConfig)
bad.reviewers = { claude: { model: 'test:model', prompt: '' } }
vi.mocked(parse).mockReturnValue(bad)
expect(() => loadConfig('/path/to/config.yaml')).toThrow('missing a "prompt" field')
})
it('throws when summarizer missing model', () => {
const bad = structuredClone(validConfig)
bad.summarizer = { model: '', prompt: 'summarize' }
vi.mocked(parse).mockReturnValue(bad)
expect(() => loadConfig('/path/to/config.yaml')).toThrow('summarizer is missing a "model"')
})
it('warns on empty API key', () => {
const config = structuredClone(validConfig)
config.providers = { anthropic: { api_key: '' } }
vi.mocked(parse).mockReturnValue(config)
loadConfig('/path/to/config.yaml')
expect(vi.mocked(logger.warn)).toHaveBeenCalledWith(
expect.stringContaining('api_key is empty')
)
})
it('does not warn when API key is set', () => {
vi.mocked(parse).mockReturnValue(structuredClone(validConfig))
loadConfig('/path/to/config.yaml')
expect(vi.mocked(logger.warn)).not.toHaveBeenCalled()
})
it('throws when config file not found', () => {
vi.mocked(existsSync).mockReturnValue(false)
expect(() => loadConfig('/path/to/missing.yaml')).toThrow('Config file not found')
})
})