diff --git a/src/sequentialthinking/__tests__/lib.test.ts b/src/sequentialthinking/__tests__/lib.test.ts index a97e41f5a0..c3d9a7e77e 100644 --- a/src/sequentialthinking/__tests__/lib.test.ts +++ b/src/sequentialthinking/__tests__/lib.test.ts @@ -329,6 +329,190 @@ describe('SequentialThinkingServer', () => { expect(data.nextThoughtNeeded).toBe(false); }); + + it('should reject negative thoughtNumber', () => { + const input = { + thought: 'Test thought', + thoughtNumber: -1, + totalThoughts: 3, + nextThoughtNeeded: true + }; + + const result = server.processThought(input); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('thoughtNumber must be >= 1'); + }); + + it('should reject zero thoughtNumber', () => { + const input = { + thought: 'Test thought', + thoughtNumber: 0, + totalThoughts: 3, + nextThoughtNeeded: true + }; + + const result = server.processThought(input); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('thoughtNumber must be >= 1'); + }); + + it('should reject negative totalThoughts', () => { + const input = { + thought: 'Test thought', + thoughtNumber: 1, + totalThoughts: -5, + nextThoughtNeeded: true + }; + + const result = server.processThought(input); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('totalThoughts must be >= 1'); + }); + + it('should reject zero totalThoughts', () => { + const input = { + thought: 'Test thought', + thoughtNumber: 1, + totalThoughts: 0, + nextThoughtNeeded: true + }; + + const result = server.processThought(input); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('totalThoughts must be >= 1'); + }); + + it('should reject NaN thoughtNumber', () => { + const input = { + thought: 'Test thought', + thoughtNumber: NaN, + totalThoughts: 3, + nextThoughtNeeded: true + }; + + const result = server.processThought(input); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('thoughtNumber must be a valid number'); + }); + + it('should reject Infinity totalThoughts', () => { + const input = { + thought: 'Test thought', + thoughtNumber: 1, + totalThoughts: Infinity, + nextThoughtNeeded: true + }; + + const result = server.processThought(input); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('totalThoughts must be a valid number'); + }); + + it('should reject non-boolean isRevision', () => { + const input = { + thought: 'Test thought', + thoughtNumber: 1, + totalThoughts: 3, + nextThoughtNeeded: true, + isRevision: 'true' + }; + + const result = server.processThought(input); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('isRevision must be a boolean'); + }); + + it('should reject non-number revisesThought', () => { + const input = { + thought: 'Test thought', + thoughtNumber: 2, + totalThoughts: 3, + nextThoughtNeeded: true, + isRevision: true, + revisesThought: '1' + }; + + const result = server.processThought(input); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('revisesThought must be a number'); + }); + + it('should reject negative revisesThought', () => { + const input = { + thought: 'Test thought', + thoughtNumber: 2, + totalThoughts: 3, + nextThoughtNeeded: true, + isRevision: true, + revisesThought: -1 + }; + + const result = server.processThought(input); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('revisesThought must be >= 1'); + }); + + it('should reject non-number branchFromThought', () => { + const input = { + thought: 'Test thought', + thoughtNumber: 2, + totalThoughts: 3, + nextThoughtNeeded: true, + branchFromThought: '1', + branchId: 'branch-a' + }; + + const result = server.processThought(input); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('branchFromThought must be a number'); + }); + + it('should reject non-string branchId', () => { + const input = { + thought: 'Test thought', + thoughtNumber: 2, + totalThoughts: 3, + nextThoughtNeeded: true, + branchFromThought: 1, + branchId: 123 + }; + + const result = server.processThought(input); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('branchId must be a string'); + }); + + it('should reject empty branchId', () => { + const input = { + thought: 'Test thought', + thoughtNumber: 2, + totalThoughts: 3, + nextThoughtNeeded: true, + branchFromThought: 1, + branchId: '' + }; + + const result = server.processThought(input); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain('branchId cannot be empty'); + }); + + it('should accept valid optional fields', () => { + const input = { + thought: 'Test thought', + thoughtNumber: 2, + totalThoughts: 3, + nextThoughtNeeded: true, + isRevision: true, + revisesThought: 1, + branchFromThought: 1, + branchId: 'branch-a', + needsMoreThoughts: true + }; + + const result = server.processThought(input); + expect(result.isError).toBeUndefined(); + }); }); describe('processThought - response format', () => { diff --git a/src/sequentialthinking/lib.ts b/src/sequentialthinking/lib.ts index c5ee9cad3c..4b9a4d0037 100644 --- a/src/sequentialthinking/lib.ts +++ b/src/sequentialthinking/lib.ts @@ -24,19 +24,77 @@ export class SequentialThinkingServer { private validateThoughtData(input: unknown): ThoughtData { const data = input as Record; - if (!data.thought || typeof data.thought !== 'string') { - throw new Error('Invalid thought: must be a string'); + // Validate required fields + if (typeof data.thought !== 'string' || data.thought.length === 0) { + throw new Error('Invalid thought: must be a non-empty string'); } - if (!data.thoughtNumber || typeof data.thoughtNumber !== 'number') { + + if (typeof data.thoughtNumber !== 'number') { throw new Error('Invalid thoughtNumber: must be a number'); } - if (!data.totalThoughts || typeof data.totalThoughts !== 'number') { + if (!Number.isFinite(data.thoughtNumber)) { + throw new Error('Invalid thoughtNumber: thoughtNumber must be a valid number'); + } + if (data.thoughtNumber < 1) { + throw new Error('Invalid thoughtNumber: thoughtNumber must be >= 1'); + } + + if (typeof data.totalThoughts !== 'number') { throw new Error('Invalid totalThoughts: must be a number'); } + if (!Number.isFinite(data.totalThoughts)) { + throw new Error('Invalid totalThoughts: totalThoughts must be a valid number'); + } + if (data.totalThoughts < 1) { + throw new Error('Invalid totalThoughts: totalThoughts must be >= 1'); + } + if (typeof data.nextThoughtNeeded !== 'boolean') { throw new Error('Invalid nextThoughtNeeded: must be a boolean'); } + // Validate optional fields if present + if (data.isRevision !== undefined && typeof data.isRevision !== 'boolean') { + throw new Error('Invalid isRevision: isRevision must be a boolean'); + } + + if (data.revisesThought !== undefined) { + if (typeof data.revisesThought !== 'number') { + throw new Error('Invalid revisesThought: revisesThought must be a number'); + } + if (!Number.isFinite(data.revisesThought)) { + throw new Error('Invalid revisesThought: revisesThought must be a valid number'); + } + if (data.revisesThought < 1) { + throw new Error('Invalid revisesThought: revisesThought must be >= 1'); + } + } + + if (data.branchFromThought !== undefined) { + if (typeof data.branchFromThought !== 'number') { + throw new Error('Invalid branchFromThought: branchFromThought must be a number'); + } + if (!Number.isFinite(data.branchFromThought)) { + throw new Error('Invalid branchFromThought: branchFromThought must be a valid number'); + } + if (data.branchFromThought < 1) { + throw new Error('Invalid branchFromThought: branchFromThought must be >= 1'); + } + } + + if (data.branchId !== undefined) { + if (typeof data.branchId !== 'string') { + throw new Error('Invalid branchId: branchId must be a string'); + } + if (data.branchId.length === 0) { + throw new Error('Invalid branchId: branchId cannot be empty'); + } + } + + if (data.needsMoreThoughts !== undefined && typeof data.needsMoreThoughts !== 'boolean') { + throw new Error('Invalid needsMoreThoughts: needsMoreThoughts must be a boolean'); + } + return { thought: data.thought, thoughtNumber: data.thoughtNumber,