Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
301 changes: 301 additions & 0 deletions src/sequentialthinking/__tests__/lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -436,4 +620,121 @@ describe('SequentialThinkingServer', () => {
expect(result.isError).toBeUndefined();
});
});

describe('processThought - state isolation (concurrent requests)', () => {
it('should isolate state between different server instances', () => {
const server1 = new SequentialThinkingServer();
const server2 = new SequentialThinkingServer();

const input1 = {
thought: 'Server 1 thought',
thoughtNumber: 1,
totalThoughts: 2,
nextThoughtNeeded: true
};

const input2 = {
thought: 'Server 2 thought',
thoughtNumber: 1,
totalThoughts: 3,
nextThoughtNeeded: true
};

server1.processThought(input1);
server2.processThought(input2);

const result1 = server1.processThought({
thought: 'Server 1 second thought',
thoughtNumber: 2,
totalThoughts: 2,
nextThoughtNeeded: false
});

const result2 = server2.processThought({
thought: 'Server 2 second thought',
thoughtNumber: 2,
totalThoughts: 3,
nextThoughtNeeded: true
});

const data1 = JSON.parse(result1.content[0].text);
const data2 = JSON.parse(result2.content[0].text);

// Each server should track only its own history
expect(data1.thoughtHistoryLength).toBe(2);
expect(data2.thoughtHistoryLength).toBe(2);
});

it('should not leak branches between server instances', () => {
const server1 = new SequentialThinkingServer();
const server2 = new SequentialThinkingServer();

server1.processThought({
thought: 'Branch A',
thoughtNumber: 1,
totalThoughts: 1,
nextThoughtNeeded: false,
branchFromThought: 1,
branchId: 'branch-a'
});

const result2 = server2.processThought({
thought: 'No branch',
thoughtNumber: 1,
totalThoughts: 1,
nextThoughtNeeded: false
});

const data2 = JSON.parse(result2.content[0].text);

// Server 2 should have no branches from server 1
expect(data2.branches.length).toBe(0);
});

it('should handle concurrent interleaved requests without state pollution', () => {
const server1 = new SequentialThinkingServer();
const server2 = new SequentialThinkingServer();

// Interleave requests as if they were concurrent
server1.processThought({
thought: 'Conversation A - thought 1',
thoughtNumber: 1,
totalThoughts: 3,
nextThoughtNeeded: true
});

server2.processThought({
thought: 'Conversation B - thought 1',
thoughtNumber: 1,
totalThoughts: 2,
nextThoughtNeeded: true
});

server1.processThought({
thought: 'Conversation A - thought 2',
thoughtNumber: 2,
totalThoughts: 3,
nextThoughtNeeded: true
});

server2.processThought({
thought: 'Conversation B - thought 2',
thoughtNumber: 2,
totalThoughts: 2,
nextThoughtNeeded: false
});

const result1 = server1.processThought({
thought: 'Conversation A - thought 3',
thoughtNumber: 3,
totalThoughts: 3,
nextThoughtNeeded: false
});

const data1 = JSON.parse(result1.content[0].text);

// Server 1 should have exactly 3 thoughts, not 5
expect(data1.thoughtHistoryLength).toBe(3);
});
});
});
4 changes: 2 additions & 2 deletions src/sequentialthinking/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,14 @@ const server = new Server(
}
);

const thinkingServer = new SequentialThinkingServer();

server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [SEQUENTIAL_THINKING_TOOL],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "sequentialthinking") {
// Create new instance per request to avoid shared state
const thinkingServer = new SequentialThinkingServer();
return thinkingServer.processThought(request.params.arguments);
}

Expand Down
Loading