import { describe, it, expect } from 'vitest';
import { evaluateQualityGate } from './quality-gate.js';
import type { PuzzleData } from './types.js';
function makePuzzle(overrides?: Partial<PuzzleData>): PuzzleData {
return {
id: 'quality-test',
name: 'Quality Test',
theme: 'ice',
width: 8,
height: 6,
par: 2,
start: { x: 1, y: 1 },
goal: { x: 6, y: 1 },
obstacles: [{ x: 4, y: 1, type: 'hot_coals' }],
...overrides,
};
}
function makeTiePuzzle(overrides?: Partial<PuzzleData>): PuzzleData {
return {
id: 'tie-test',
name: 'Tie Test',
theme: 'ice',
width: 7,
height: 7,
par: 3,
start: { x: 1, y: 3 },
goal: { x: 5, y: 3 },
obstacles: [{ x: 3, y: 3, type: 'rock' }],
...overrides,
};
}
describe('evaluateQualityGate', () => {
it('passes when par equals shortest', () => {
const puzzle = makePuzzle({ par: 2 });
const report = evaluateQualityGate(puzzle, undefined, { requirePar: true });
expect(report.pass).toBe(true);
expect(report.parEqualsShortest).toBe(true);
expect(report.shorterThanParExists).toBe(false);
expect(report.longerThanParConfigured).toBe(false);
});
it('counts tied shortest paths when they exist', () => {
const puzzle = makeTiePuzzle();
const report = evaluateQualityGate(puzzle, undefined, { requirePar: true });
expect(report.pass).toBe(true);
expect(report.shortest).toBe(3);
expect(report.tiedOptimalPathsCount).toBe(1);
});
it('fails when par is required but unset', () => {
const puzzle = makePuzzle({ par: 0 });
const report = evaluateQualityGate(puzzle, undefined, { requirePar: true });
expect(report.pass).toBe(false);
expect(report.failures.some((failure) => failure.includes('par is not set'))).toBe(true);
});
it('fails when shortest path is below par', () => {
const puzzle = makePuzzle({ par: 3 });
const report = evaluateQualityGate(puzzle, undefined, { requirePar: true });
expect(report.pass).toBe(false);
expect(report.shorterThanParExists).toBe(true);
});
it('fails when par is below shortest path', () => {
const puzzle = makePuzzle({ par: 1 });
const report = evaluateQualityGate(puzzle, undefined, { requirePar: true });
expect(report.pass).toBe(false);
expect(report.longerThanParConfigured).toBe(true);
});
it('surfaces hot coals shortcut diagnostics', () => {
const puzzle = makePuzzle({
par: 2,
obstacles: [{ x: 4, y: 1, type: 'hot_coals' }],
});
const report = evaluateQualityGate(puzzle, undefined, { requirePar: true });
expect(report.hotCoalsDiagnostics.hotCoalsTileCount).toBe(1);
expect(report.hotCoalsDiagnostics.hotCoalsHitsOnShortest).toBeGreaterThan(0);
expect(report.hotCoalsDiagnostics.shortestPathUsesHotCoalsShortcut).toBe(true);
expect(report.warnings.some((warning) => warning.includes('hot coals'))).toBe(true);
});
it('treats spike obstacle as hot coals alias in diagnostics', () => {
const puzzle = makePuzzle({
par: 2,
obstacles: [{ x: 4, y: 1, type: 'spike' as any }],
});
const report = evaluateQualityGate(puzzle, undefined, { requirePar: true });
expect(report.hotCoalsDiagnostics.hotCoalsTileCount).toBe(1);
expect(report.hotCoalsDiagnostics.hotCoalsHitsOnShortest).toBeGreaterThan(0);
});
it('fails on legacy thin_ice obstacle encoding', () => {
const puzzle = makePuzzle({
par: 2,
obstacles: [{ x: 3, y: 1, type: 'thin_ice' }],
});
const report = evaluateQualityGate(puzzle, undefined, { requirePar: true });
expect(report.pass).toBe(false);
expect(report.failures.some((failure) => failure.includes('Legacy obstacle encodings'))).toBe(true);
});
});