import { describe, it, expect } from 'vitest'; import { baseInputSchema, validateToolInput, assertValidToolInput } from '../../src/tools/schemas.js'; const validMinimal = { question: 'hello' }; const validFull = { question: 'review this', context: 'some context', constraints: ['no external deps'], expectedOutput: 'bullet points', projectSummary: 'MVP phase 1', taskSummary: 'implement schema validation', relevantFiles: [ { path: 'src/tools/schemas.js', content: 'import ...' }, { path: 'test/schemas.test.js', content: 'import ...', language: 'javascript' }, ], logs: 'error: something broke', }; describe('baseInputSchema', () => { it('validates a minimal input successfully', () => { const result = baseInputSchema.safeParse(validMinimal); expect(result.success).toBe(true); if (result.success) expect(result.data.question).toBe('hello'); }); it('validates a full input with all fields', () => { const result = baseInputSchema.safeParse(validFull); expect(result.success).toBe(true); }); it('accepts question with only whitespace (min(1) not trim+min(1))', () => { const result = baseInputSchema.safeParse({ question: ' ' }); expect(result.success).toBe(true); }); it('rejects empty string question', () => { const result = baseInputSchema.safeParse({ question: '' }); expect(result.success).toBe(false); }); it('rejects question with null', () => { const result = baseInputSchema.safeParse({ question: null }); expect(result.success).toBe(false); }); it('rejects question with undefined', () => { const result = baseInputSchema.safeParse({ question: undefined }); expect(result.success).toBe(false); }); it('rejects question with a number', () => { const result = baseInputSchema.safeParse({ question: 123 }); expect(result.success).toBe(false); }); it('rejects missing question field entirely', () => { const result = baseInputSchema.safeParse({ context: 'only context' }); expect(result.success).toBe(false); }); it('accepts constraints as valid string[]', () => { const input = { question: 'q', constraints: ['a', 'b'] }; const result = baseInputSchema.safeParse(input); expect(result.success).toBe(true); }); it('rejects constraints with non-string elements', () => { // @ts-ignore - testing runtime invalid input const input = { question: 'q', constraints: ['a', 123] }; const result = baseInputSchema.safeParse(input); expect(result.success).toBe(false); }); it('accepts relevantFiles with valid file objects', () => { const input = { question: 'q', relevantFiles: [{ path: 'a.js', content: 'code' }] }; const result = baseInputSchema.safeParse(input); expect(result.success).toBe(true); }); it('accepts empty relevantFiles array', () => { const input = { question: 'q', relevantFiles: [] }; const result = baseInputSchema.safeParse(input); expect(result.success).toBe(true); }); it('rejects relevantFiles with missing path', () => { // @ts-ignore - testing runtime invalid input const input = { question: 'q', relevantFiles: [{ content: 'code' }] }; const result = baseInputSchema.safeParse(input); expect(result.success).toBe(false); }); it('rejects relevantFiles with non-string content', () => { // @ts-ignore - testing runtime invalid input const input = { question: 'q', relevantFiles: [{ path: 'a.js', content: 123 }] }; const result = baseInputSchema.safeParse(input); expect(result.success).toBe(false); }); it('rejects input with unknown keys (strict mode)', () => { // @ts-ignore - testing runtime invalid input const input = { question: 'q', unknownField: true }; const result = baseInputSchema.safeParse(input); expect(result.success).toBe(false); }); it('rejects null as raw input', () => { const result = baseInputSchema.safeParse(null); expect(result.success).toBe(false); }); it('rejects a string as raw input', () => { const result = baseInputSchema.safeParse('just a string'); expect(result.success).toBe(false); }); }); describe('validateToolInput', () => { it('returns ok:true with data for valid input', () => { const result = validateToolInput(validMinimal); expect(result.ok).toBe(true); if (result.ok) expect(result.data.question).toBe('hello'); }); it('returns ok:false with errors array for invalid input', () => { const result = validateToolInput({ question: '' }); expect(result.ok).toBe(false); if (!result.ok) expect(Array.isArray(result.errors)).toBe(true); }); it('error messages include dot-notation path segments', () => { const result = validateToolInput({ question: 'q', relevantFiles: [{ content: 1 }] }); expect(!result.ok && result.errors.some((e) => e.includes('.'))).toBe(true); }); it('returns ok:false for null input', () => { const result = validateToolInput(null); expect(result.ok).toBe(false); }); it('returns ok:false for unknown keys', () => { // @ts-ignore - testing runtime invalid input const result = validateToolInput({ question: 'q', extraKey: true }); expect(result.ok).toBe(false); }); }); describe('assertValidToolInput', () => { it('returns validated data for valid input', () => { const result = assertValidToolInput(validMinimal); expect(typeof result).toBe('object'); expect(result.question).toBe('hello'); }); it('throws Error with kind "ValidationError" on invalid input', () => { expect(() => assertValidToolInput({ question: '' })).toThrow('Input validation error'); }); it('thrown error has .kind === "ValidationError"', () => { let caught = null; try { assertValidToolInput(null); } catch (e) { caught = e; } expect(caught).not.toBeNull(); expect(/** @type{Error} */ (caught).kind).toBe('ValidationError'); }); it('thrown error message includes validation details', () => { let caughtMsg = ''; try { // @ts-ignore - testing runtime invalid input assertValidToolInput({ question: 'q', badField: true }); } catch (e) { caughtMsg = /** @type{Error} */ (e).message; } expect(caughtMsg).toContain('badField'); expect(caughtMsg).toContain('Input validation error'); }); });