diff --git a/tests/docker-diagnostic.test.ts b/tests/docker-diagnostic.test.ts index a542d13..a64795f 100644 --- a/tests/docker-diagnostic.test.ts +++ b/tests/docker-diagnostic.test.ts @@ -107,4 +107,26 @@ describe('Docker diagnostic', () => { expect(diagnostic.status).toBe('warn'); expect(diagnostic.message).toContain('unrecognized client version'); }); + + it.each([ + ['daemon exit error', result(1, '', 'daemon unreachable')], + ['whitespace-only daemon output', result(0, ' \n')], + ['daemon execution error object', result(null, '', '', new Error('EPIPE'))], + ])( + 'classifies daemon failure as unavailable: %s', + (_label, daemonOutcome) => { + const execute: RunCommand = (_executable, args) => { + if (args[0] === '--version') { + return result(0, 'Docker version 28.4.0, build abcdef\n'); + } + + return daemonOutcome; + }; + + const diagnostic = createDockerDiagnostic({ execute }).run(); + + expect(diagnostic.status).toBe('warn'); + expect(diagnostic.message).toContain('daemon'); + expect(diagnostic.message).not.toContain('unreachable'); + }); }); diff --git a/tests/exit-code-reference.test.ts b/tests/exit-code-reference.test.ts new file mode 100644 index 0000000..a36f2ec --- /dev/null +++ b/tests/exit-code-reference.test.ts @@ -0,0 +1,54 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { EXIT_CODES } from '../src/errors/errors.js'; + +const REFERENCE = readFileSync( + join(process.cwd(), 'docs', 'reference', 'exit-codes.md'), + 'utf8', +); + +function documentedCodes(markdown: string): Map { + const codes = new Map(); + const rowPattern = /^\|\s*`(\d+)`\s*\|\s*([^|]+?)\s*\|/gm; + + let match: RegExpExecArray | null; + while ((match = rowPattern.exec(markdown)) !== null) { + codes.set(Number(match[1]), match[2].trim()); + } + + return codes; +} + +describe('exit-code reference conformance', () => { + const codes = documentedCodes(REFERENCE); + + it('documents every stable exit code from the source constants', () => { + for (const value of Object.values(EXIT_CODES)) { + expect(codes.has(value)).toBe(true); + } + }); + + it('keeps source constants aligned with the published policy', () => { + expect(EXIT_CODES).toEqual({ + success: 0, + unexpected: 1, + validation: 2, + subprocess: 3, + internal: 4, + diagnostic: 5, + }); + }); + + it('labels each documented code with a non-empty category', () => { + for (const value of Object.values(EXIT_CODES)) { + expect((codes.get(value) ?? '').length).toBeGreaterThan(0); + } + }); + + it('documents help/version success and Commander parser mapping', () => { + expect(REFERENCE).toContain('--help'); + expect(REFERENCE).toContain('--version'); + expect(REFERENCE).toContain('Commander parser failures'); + }); +}); diff --git a/tests/git-diagnostic.test.ts b/tests/git-diagnostic.test.ts index 94ffed4..1bfa9db 100644 --- a/tests/git-diagnostic.test.ts +++ b/tests/git-diagnostic.test.ts @@ -68,4 +68,28 @@ describe('Git diagnostic', () => { expect(diagnostic.message).toBe('Git returned an unrecognized version.'); expect(diagnostic.remediation).toContain('git --version'); }); + + it.each([ + ['non-zero exit with valid stdout', result(128, 'git version 2.47.1\n')], + ['empty stdout', result(0, '')], + ['null status without error', result(null, 'git version 2.47.1\n')], + ['unexpected prefix', result(0, 'fatal: not a git repository\n')], + ])('fails safely for %s', (_label, outcome) => { + const execute: RunCommand = () => outcome; + + const diagnostic = createGitDiagnostic(execute).run(); + + expect(diagnostic.status).toBe('fail'); + expect(diagnostic.message).not.toContain('fatal:'); + }); + + it('tolerates surrounding whitespace and newlines in version output', () => { + const execute: RunCommand = () => + result(0, '\n git version 2.43.0 \n\n'); + + const diagnostic = createGitDiagnostic(execute).run(); + + expect(diagnostic.status).toBe('pass'); + expect(diagnostic.message).toContain('2.43.0'); + }); }); diff --git a/tests/rust-diagnostics.test.ts b/tests/rust-diagnostics.test.ts index 6924282..ff02e22 100644 --- a/tests/rust-diagnostics.test.ts +++ b/tests/rust-diagnostics.test.ts @@ -66,6 +66,31 @@ describe('Rust diagnostic', () => { expect(diagnostic.status).toBe('fail'); expect(diagnostic.message).toBe('Rust returned an unrecognized version.'); }); + + it.each([ + ['empty output', result(0, '')], + ['missing release segment', result(0, 'rustc 1.84\n')], + ['non-numeric version', result(0, 'rustc a.b.c\n')], + ['binary garbage', result(0, '\u0000\u0001rustc ?\n')], + ['no version prefix', result(0, '1.84.0\n')], + ])('rejects malformed rustc fixture: %s', (_label, outcome) => { + const execute: RunCommand = () => outcome; + + const diagnostic = createRustDiagnostic(execute).run(); + + expect(diagnostic.status).toBe('fail'); + expect(JSON.stringify(diagnostic)).not.toContain('\u0000'); + }); + + it('accepts newline-terminated rustc version with trailing metadata', () => { + const execute: RunCommand = () => + result(0, 'rustc 1.85.0-nightly (abc1234 2025-02-01)\n'); + + const diagnostic = createRustDiagnostic(execute).run(); + + expect(diagnostic.status).toBe('pass'); + expect(diagnostic.message).toContain('1.85.0'); + }); }); describe('Cargo diagnostic', () => { @@ -101,4 +126,17 @@ describe('Cargo diagnostic', () => { expect(diagnostic.status).toBe('fail'); expect(diagnostic.message).toBe('Cargo returned an unrecognized version.'); }); + + it.each([ + ['empty output', result(0, '')], + ['missing release segment', result(0, 'cargo 1.89\n')], + ['no version prefix', result(0, '1.89.0 (c24e10642 2025-06-23)\n')], + ])('rejects malformed cargo fixture: %s', (_label, outcome) => { + const execute: RunCommand = () => outcome; + + const diagnostic = createCargoDiagnostic(execute).run(); + + expect(diagnostic.status).toBe('fail'); + expect(diagnostic.message).toBe('Cargo returned an unrecognized version.'); + }); });