Specif-ai MCP Server
Official
by vj-presidio
import { ServerService } from '../server.service'
import { ConsoleManager } from './test-utils'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
import type { Solution, PRD, UserStory } from '../../types'
// Mock test data
const mockSolution: Solution = {
BRD: [
{
id: 'BRD01',
title: 'Test BRD',
description: 'Test BRD description',
},
],
PRD: [
{
id: 'PRD01',
title: 'Test PRD',
description: 'Test PRD description',
userStories: [
{
id: 'US1',
title: 'User Story 1',
description: 'Test user story',
tasks: [
{
id: 'T1',
title: 'Task 1',
description: 'Test task',
},
],
},
],
},
],
NFR: [
{
id: 'NFR01',
title: 'Test NFR',
description: 'Test NFR description',
},
],
BP: [
{
id: 'BP01',
title: 'Test BP',
description: 'Test BP description',
},
],
UIR: [
{
id: 'UIR01',
title: 'Test UIR',
description: 'Test UIR description',
},
],
}
// Mock fs/promises
jest.mock('fs/promises', () => ({
readdir: jest.fn(),
readFile: jest.fn(),
access: jest.fn(),
constants: {
R_OK: 4,
},
}))
// Import mocked fs/promises to access mock functions
import { readdir, readFile, access } from 'fs/promises'
// Mock DocumentService
jest.mock('../document.service', () => ({
DocumentService: jest.fn().mockImplementation(() => ({
loadSolution: jest.fn().mockImplementation((path) => {
if (path === '/test/path' || path === '/inferred/path') {
return Promise.resolve(mockSolution)
}
if (path === '/test/malformed/path') {
return Promise.resolve({
...mockSolution,
PRD: [
{
id: 'PRD01',
title: '',
description: '',
userStories: [],
},
],
})
}
if (path === '/test/empty/path') {
return Promise.resolve({
...mockSolution,
PRD: [],
})
}
return Promise.reject(new Error('Invalid path'))
}),
findPRD: jest.fn().mockImplementation((solution: Solution, prdId: string) => {
return solution.PRD.find((prd) => prd.id === prdId) || null
}),
findUserStory: jest.fn().mockImplementation((prd: PRD, userStoryId: string) => {
return prd.userStories?.find((story) => story.id === userStoryId) || null
}),
findTask: jest.fn().mockImplementation((userStory: UserStory, taskId: string) => {
return userStory.tasks?.find((task) => task.id === taskId) || null
}),
})),
}))
// Mock FileService
jest.mock('../file.service', () => ({
FileService: jest.fn().mockImplementation(() => ({
readAllJsonFiles: jest.fn(),
isDirectory: jest.fn().mockImplementation((path) => {
if (
path === '/valid/dir' ||
path === '/test/path' ||
path === '/inferred/path' ||
path === '/with/specif-ai-path'
) {
return Promise.resolve(true)
}
return Promise.resolve(false)
}),
})),
}))
// Mock Server and StdioServerTransport
const mockRequestHandlers = new Map()
const mockServer = {
requestHandlers: mockRequestHandlers,
setRequestHandler: (schema: any, handler: any) => {
// For CallToolRequestSchema, use "call-tool" as the name
const name =
schema === CallToolRequestSchema
? 'call-tool'
: schema === ListToolsRequestSchema
? 'list-tools'
: schema.name
mockRequestHandlers.set(name, handler)
},
connect: jest.fn().mockResolvedValue(undefined),
}
// Mock external modules
jest.mock('@modelcontextprotocol/sdk/server/index.js', () => ({
Server: jest.fn().mockImplementation(() => mockServer),
}))
jest.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
StdioServerTransport: jest.fn(),
}))
describe('ServerService', () => {
beforeAll(() => {
ConsoleManager.suppressConsole()
})
afterAll(() => {
ConsoleManager.restoreConsole()
})
let serverService: ServerService
beforeEach(async () => {
mockRequestHandlers.clear()
serverService = new ServerService()
// Initialize server with mock solution using set-project-path
const handler = mockRequestHandlers.get('call-tool')
await handler({
params: {
name: 'set-project-path',
arguments: { path: '/test/path' },
},
})
})
describe('List Tools Handler', () => {
test('should return complete list of tools with schemas', async () => {
const handler = mockRequestHandlers.get('list-tools')
const response = await handler()
expect(response.tools).toHaveLength(9)
expect(response.tools.map((t: { name: string }) => t.name)).toEqual([
'set-project-path',
'get-brds',
'get-prds',
'get-nfrs',
'get-uirs',
'get-bps',
'get-user-stories',
'get-tasks',
'get-task',
])
// Verify schema for get-user-stories
const userStoriesSchema = response.tools.find(
(t: { name: string; inputSchema: any }) => t.name === 'get-user-stories'
)
expect(userStoriesSchema?.inputSchema.required).toEqual(['prdId', 'cwd'])
// Verify schema for get-tasks
const tasksSchema = response.tools.find(
(t: { name: string; inputSchema: any }) => t.name === 'get-tasks'
)
expect(tasksSchema?.inputSchema.required).toEqual(['prdId', 'userStoryId', 'cwd'])
// Verify schema for get-task
const taskSchema = response.tools.find(
(t: { name: string; inputSchema: any }) => t.name === 'get-task'
)
expect(taskSchema?.inputSchema.required).toEqual(['prdId', 'userStoryId', 'taskId', 'cwd'])
// Verify schema for set-project-path
const setProjectPathSchema = response.tools.find(
(t: { name: string; inputSchema: any }) => t.name === 'set-project-path'
)
expect(setProjectPathSchema?.inputSchema.required).toEqual(['path'])
})
})
describe('Project Path Inference', () => {
let mockReaddir: jest.Mock
let mockReadFile: jest.Mock
let mockAccess: jest.Mock
beforeEach(() => {
mockReaddir = readdir as jest.Mock
mockReadFile = readFile as jest.Mock
mockAccess = access as jest.Mock
// Default mock implementations
mockReaddir.mockResolvedValue(['file1.txt', '.specif-ai-path'])
mockReadFile.mockResolvedValue('/inferred/path')
mockAccess.mockResolvedValue(undefined)
// Clear mock calls
mockReaddir.mockClear()
mockReadFile.mockClear()
mockAccess.mockClear()
})
test('should infer project path from directory with .specif-ai-path file', async () => {
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-brds',
arguments: { cwd: '/with/specif-ai-path' },
},
})
expect(mockAccess).toHaveBeenCalled()
expect(mockReadFile).toHaveBeenCalled()
expect(response.content[0].text).toContain('BRD01')
})
test('should handle non-existent directory for inference', async () => {
// Create a new server service instance to avoid state from previous tests
mockRequestHandlers.clear()
const newServerService = new ServerService()
const handler = mockRequestHandlers.get('call-tool')
await expect(
handler({
params: {
name: 'get-brds',
arguments: { cwd: '/non/existent/dir' },
},
})
).rejects.toThrow('No project path set')
})
test('should handle directory without .specif-ai-path file', async () => {
// Create a new server service instance to avoid state from previous tests
mockRequestHandlers.clear()
const newServerService = new ServerService()
const handler = mockRequestHandlers.get('call-tool')
mockAccess.mockRejectedValue(new Error('File not found'))
await expect(
handler({
params: {
name: 'get-brds',
arguments: { cwd: '/valid/dir' },
},
})
).rejects.toThrow('No project path set')
})
test('should handle invalid inferred path', async () => {
// Create a new server service instance to avoid state from previous tests
mockRequestHandlers.clear()
const newServerService = new ServerService()
const handler = mockRequestHandlers.get('call-tool')
mockReadFile.mockResolvedValue('/invalid/inferred/path')
await expect(
handler({
params: {
name: 'get-brds',
arguments: { cwd: '/with/specif-ai-path' },
},
})
).rejects.toThrow('No project path set')
})
test('should handle error during inference', async () => {
// Create a new server service instance to avoid state from previous tests
mockRequestHandlers.clear()
const newServerService = new ServerService()
const handler = mockRequestHandlers.get('call-tool')
mockReadFile.mockRejectedValue(new Error('Read error'))
await expect(
handler({
params: {
name: 'get-brds',
arguments: { cwd: '/with/specif-ai-path' },
},
})
).rejects.toThrow('No project path set')
})
test('should directly access inferProjectPath method', async () => {
// Test with valid path
mockReadFile.mockResolvedValue('/inferred/path')
const result1 = await (serverService as any).inferProjectPath('/with/specif-ai-path')
expect(result1).toBe('/inferred/path')
// Test with empty path
const result2 = await (serverService as any).inferProjectPath('')
expect(result2).toBeNull()
// Test with non-existent directory
const result3 = await (serverService as any).inferProjectPath('/non/existent/dir')
expect(result3).toBeNull()
// Test with directory without .specif-ai-path file
mockAccess.mockRejectedValue(new Error('File not found'))
const result4 = await (serverService as any).inferProjectPath('/valid/dir')
expect(result4).toBeNull()
// Test with invalid inferred path
mockAccess.mockResolvedValue(undefined)
mockReadFile.mockResolvedValue('/invalid/inferred/path')
const result5 = await (serverService as any).inferProjectPath('/with/specif-ai-path')
expect(result5).toBeNull()
// Test with error during file read
mockReadFile.mockRejectedValue(new Error('Read error'))
const result6 = await (serverService as any).inferProjectPath('/with/specif-ai-path')
expect(result6).toBeNull()
})
test('should auto-infer project path for get-prds', async () => {
mockReadFile.mockResolvedValue('/inferred/path')
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-prds',
arguments: { cwd: '/with/specif-ai-path' },
},
})
expect(response.content[0].text).toContain('PRD01')
})
test('should auto-infer project path for get-nfrs', async () => {
mockReadFile.mockResolvedValue('/inferred/path')
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-nfrs',
arguments: { cwd: '/with/specif-ai-path' },
},
})
expect(response.content[0].text).toContain('NFR01')
})
test('should auto-infer project path for get-uirs', async () => {
mockReadFile.mockResolvedValue('/inferred/path')
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-uirs',
arguments: { cwd: '/with/specif-ai-path' },
},
})
expect(response.content[0].text).toContain('UIR01')
})
test('should auto-infer project path for get-bps', async () => {
mockReadFile.mockResolvedValue('/inferred/path')
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-bps',
arguments: { cwd: '/with/specif-ai-path' },
},
})
expect(response.content[0].text).toContain('BP01')
})
test('should auto-infer project path for get-user-stories', async () => {
mockReadFile.mockResolvedValue('/inferred/path')
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-user-stories',
arguments: {
prdId: 'PRD01',
cwd: '/with/specif-ai-path',
},
},
})
expect(response.content[0].text).toContain('US1')
})
test('should auto-infer project path for get-tasks', async () => {
mockReadFile.mockResolvedValue('/inferred/path')
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-tasks',
arguments: {
prdId: 'PRD01',
userStoryId: 'US1',
cwd: '/with/specif-ai-path',
},
},
})
expect(response.content[0].text).toContain('T1')
})
test('should auto-infer project path for get-task', async () => {
mockReadFile.mockResolvedValue('/inferred/path')
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-task',
arguments: {
prdId: 'PRD01',
userStoryId: 'US1',
taskId: 'T1',
cwd: '/with/specif-ai-path',
},
},
})
expect(response.content[0].text).toContain('T1')
})
// Additional tests to cover remaining lines
test('should handle auto-inference with non-existent PRD in get-user-stories', async () => {
mockReadFile.mockResolvedValue('/inferred/path')
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-user-stories',
arguments: {
prdId: 'nonexistent',
cwd: '/with/specif-ai-path',
},
},
})
expect(response.content[0].text).toBe('No PRD found with ID nonexistent')
})
test('should handle auto-inference with non-existent PRD in get-tasks', async () => {
mockReadFile.mockResolvedValue('/inferred/path')
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-tasks',
arguments: {
prdId: 'nonexistent',
userStoryId: 'US1',
cwd: '/with/specif-ai-path',
},
},
})
expect(response.content[0].text).toBe('No PRD found with ID nonexistent')
})
test('should handle auto-inference with non-existent user story in get-tasks', async () => {
mockReadFile.mockResolvedValue('/inferred/path')
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-tasks',
arguments: {
prdId: 'PRD01',
userStoryId: 'nonexistent',
cwd: '/with/specif-ai-path',
},
},
})
expect(response.content[0].text).toBe('No User Story found with ID nonexistent')
})
test('should handle auto-inference with non-existent PRD in get-task', async () => {
mockReadFile.mockResolvedValue('/inferred/path')
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-task',
arguments: {
prdId: 'nonexistent',
userStoryId: 'US1',
taskId: 'T1',
cwd: '/with/specif-ai-path',
},
},
})
expect(response.content[0].text).toBe('No PRD found with ID nonexistent')
})
test('should handle auto-inference with non-existent user story in get-task', async () => {
mockReadFile.mockResolvedValue('/inferred/path')
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-task',
arguments: {
prdId: 'PRD01',
userStoryId: 'nonexistent',
taskId: 'T1',
cwd: '/with/specif-ai-path',
},
},
})
expect(response.content[0].text).toBe('No User Story found with ID nonexistent')
})
test('should handle auto-inference with non-existent task in get-task', async () => {
mockReadFile.mockResolvedValue('/inferred/path')
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-task',
arguments: {
prdId: 'PRD01',
userStoryId: 'US1',
taskId: 'nonexistent',
cwd: '/with/specif-ai-path',
},
},
})
expect(response.content[0].text).toBe('No Task found with ID nonexistent')
})
// Tests to cover remaining lines
// Tests to cover remaining lines
test('should handle Zod validation errors', async () => {
const handler = mockRequestHandlers.get('call-tool')
await expect(
handler({
params: {
name: 'get-user-stories',
arguments: {
// Missing prdId which is required
cwd: '/with/specif-ai-path',
},
},
})
).rejects.toThrow('Invalid arguments')
})
// Test to cover the specific error case when projectPath is provided but inference fails
test('should throw specific error when projectPath is provided but inference fails', async () => {
// Create a new server service instance
mockRequestHandlers.clear()
const newServerService = new ServerService()
const handler = mockRequestHandlers.get('call-tool')
// Mock inferProjectPath to return null (inference failure)
jest.spyOn(newServerService as any, 'inferProjectPath').mockResolvedValue(null)
// Test with get-brds
await expect(
handler({
params: {
name: 'get-brds',
arguments: { cwd: '/some/path' },
},
})
).rejects.toThrow(
'No project path set. Use set-project-path first or provide a valid cwd to auto-infer.'
)
// Test with get-prds
await expect(
handler({
params: {
name: 'get-prds',
arguments: { cwd: '/some/path' },
},
})
).rejects.toThrow(
'No project path set. Use set-project-path first or provide a valid cwd to auto-infer.'
)
// Test with get-nfrs
await expect(
handler({
params: {
name: 'get-nfrs',
arguments: { cwd: '/some/path' },
},
})
).rejects.toThrow(
'No project path set. Use set-project-path first or provide a valid cwd to auto-infer.'
)
// Test with get-uirs
await expect(
handler({
params: {
name: 'get-uirs',
arguments: { cwd: '/some/path' },
},
})
).rejects.toThrow(
'No project path set. Use set-project-path first or provide a valid cwd to auto-infer.'
)
// Test with get-bps
await expect(
handler({
params: {
name: 'get-bps',
arguments: { cwd: '/some/path' },
},
})
).rejects.toThrow(
'No project path set. Use set-project-path first or provide a valid cwd to auto-infer.'
)
// Test with get-user-stories
await expect(
handler({
params: {
name: 'get-user-stories',
arguments: {
prdId: 'PRD01',
cwd: '/some/path',
},
},
})
).rejects.toThrow(
'No project path set. Use set-project-path first or provide a valid cwd to auto-infer.'
)
// Test with get-tasks
await expect(
handler({
params: {
name: 'get-tasks',
arguments: {
prdId: 'PRD01',
userStoryId: 'US1',
cwd: '/some/path',
},
},
})
).rejects.toThrow(
'No project path set. Use set-project-path first or provide a valid cwd to auto-infer.'
)
// Test with get-task
await expect(
handler({
params: {
name: 'get-task',
arguments: {
prdId: 'PRD01',
userStoryId: 'US1',
taskId: 'T1',
cwd: '/some/path',
},
},
})
).rejects.toThrow(
'No project path set. Use set-project-path first or provide a valid cwd to auto-infer.'
)
})
})
describe('Tool Handlers', () => {
test('should format multiple documents correctly', () => {
const docs = [
{
id: 'test1',
title: 'Test 1',
description: 'Description 1',
},
{
id: 'test2',
title: 'Test 2',
description: 'Description 2',
},
]
const formatted = (serverService as any).formatDocuments(docs)
expect(formatted).toBe(
'ID: test1\nTitle: Test 1\nDescription: Description 1\n--------------\n' +
'ID: test2\nTitle: Test 2\nDescription: Description 2\n--------------'
)
})
test('should format single document correctly', () => {
const docs = [
{
id: 'test',
title: 'Test',
description: 'Description',
},
]
const formatted = (serverService as any).formatDocuments(docs)
expect(formatted).toBe('ID: test\nTitle: Test\nDescription: Description\n--------------')
})
test('should handle empty document array', () => {
const docs: Array<{ id: string; title: string; description: string }> = []
const formatted = (serverService as any).formatDocuments(docs)
expect(formatted).toBe('')
})
test('should handle get-brds request', async () => {
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: { name: 'get-brds', arguments: { cwd: '/test/path' } },
})
expect(response.content[0].text).toContain('BRD01')
expect(response.content[0].text).toContain('Test BRD')
})
test('should handle get-user-stories request', async () => {
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-user-stories',
arguments: { prdId: 'PRD01', cwd: '/test/path' },
},
})
expect(response.content[0].text).toContain('US1')
expect(response.content[0].text).toContain('User Story 1')
})
test('should handle get-tasks request', async () => {
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-tasks',
arguments: {
prdId: 'PRD01',
userStoryId: 'US1',
cwd: '/test/path',
},
},
})
expect(response.content[0].text).toContain('T1')
expect(response.content[0].text).toContain('Task 1')
})
test('should handle get-nfrs request', async () => {
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: { name: 'get-nfrs', arguments: { cwd: '/test/path' } },
})
expect(response.content[0].text).toContain('NFR01')
expect(response.content[0].text).toContain('Test NFR')
})
test('should handle get-uirs request', async () => {
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: { name: 'get-uirs', arguments: { cwd: '/test/path' } },
})
expect(response.content[0].text).toContain('UIR01')
expect(response.content[0].text).toContain('Test UIR')
})
test('should handle get-bps request', async () => {
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: { name: 'get-bps', arguments: { cwd: '/test/path' } },
})
expect(response.content[0].text).toContain('BP01')
expect(response.content[0].text).toContain('Test BP')
})
test('should handle get-task request', async () => {
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-task',
arguments: {
prdId: 'PRD01',
userStoryId: 'US1',
taskId: 'T1',
cwd: '/test/path',
},
},
})
expect(response.content[0].text).toContain('T1')
expect(response.content[0].text).toContain('Task 1')
})
test('should handle invalid tool name', async () => {
const handler = mockRequestHandlers.get('call-tool')
await expect(
handler({
params: { name: 'invalid-tool', arguments: {} },
})
).rejects.toThrow('Unknown tool: invalid-tool')
})
test('should handle invalid arguments with detailed error message', async () => {
const handler = mockRequestHandlers.get('call-tool')
await expect(
handler({
params: {
name: 'get-task',
arguments: {
prdId: 'PRD01',
// Missing userStoryId and taskId
},
},
})
).rejects.toThrow('Invalid arguments: userStoryId: Required, taskId: Required')
})
test('should handle malformed PRD in get-user-stories', async () => {
const handler = mockRequestHandlers.get('call-tool')
// Create a new server service instance with malformed path
mockRequestHandlers.clear()
const newServerService = new ServerService()
const malformedHandler = mockRequestHandlers.get('call-tool')
// Set malformed solution path
await malformedHandler({
params: {
name: 'set-project-path',
arguments: { path: '/test/malformed/path' },
},
})
const response = await malformedHandler({
params: {
name: 'get-user-stories',
arguments: { prdId: 'PRD01', cwd: '/test/malformed/path' },
},
})
expect(response.content[0].text).toBe('')
})
test('should handle empty PRD list', async () => {
const handler = mockRequestHandlers.get('call-tool')
// Create a new server service instance with empty PRD path
mockRequestHandlers.clear()
const emptyPrdService = new ServerService()
const emptyPrdHandler = mockRequestHandlers.get('call-tool')
// Set empty PRD solution path
await emptyPrdHandler({
params: {
name: 'set-project-path',
arguments: { path: '/test/empty/path' },
},
})
const response = await emptyPrdHandler({
params: {
name: 'get-prds',
arguments: { cwd: '/test/empty/path' },
},
})
expect(response.content[0].text).toBe('')
})
test('should handle non-existent PRD in get-user-stories', async () => {
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-user-stories',
arguments: { prdId: 'nonexistent', cwd: '/test/path' },
},
})
expect(response.content[0].text).toBe('No PRD found with ID nonexistent')
})
test('should handle non-existent PRD in get-tasks', async () => {
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-tasks',
arguments: {
prdId: 'nonexistent',
userStoryId: 'US1',
cwd: '/test/path',
},
},
})
expect(response.content[0].text).toBe('No PRD found with ID nonexistent')
})
test('should handle non-existent PRD in get-task', async () => {
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-task',
arguments: {
prdId: 'nonexistent',
userStoryId: 'US1',
taskId: 'T1',
cwd: '/test/path',
},
},
})
expect(response.content[0].text).toBe('No PRD found with ID nonexistent')
})
test('should handle non-existent user story in get-tasks when PRD exists', async () => {
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-tasks',
arguments: {
prdId: 'PRD01',
userStoryId: 'nonexistent',
cwd: '/test/path',
},
},
})
expect(response.content[0].text).toBe('No User Story found with ID nonexistent')
})
test('should handle non-existent user story in get-task when PRD exists', async () => {
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-task',
arguments: {
prdId: 'PRD01',
userStoryId: 'nonexistent',
taskId: 'T1',
cwd: '/test/path',
},
},
})
expect(response.content[0].text).toBe('No User Story found with ID nonexistent')
})
test('should handle non-existent task when PRD and user story exist', async () => {
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-task',
arguments: {
prdId: 'PRD01',
userStoryId: 'US1',
taskId: 'nonexistent',
cwd: '/test/path',
},
},
})
expect(response.content[0].text).toBe('No Task found with ID nonexistent')
})
test('should handle non-existent user story', async () => {
const handler = mockRequestHandlers.get('call-tool')
const response = await handler({
params: {
name: 'get-tasks',
arguments: {
prdId: 'PRD01',
userStoryId: 'nonexistent',
cwd: '/test/path',
},
},
})
expect(response.content[0].text).toBe('No User Story found with ID nonexistent')
})
test('should handle get-user-stories before project path is set', async () => {
mockRequestHandlers.clear()
const noPathService = new ServerService()
const noPathHandler = mockRequestHandlers.get('call-tool')
await expect(
noPathHandler({
params: {
name: 'get-user-stories',
arguments: { prdId: 'PRD01' }, // Deliberately omit cwd
},
})
).rejects.toThrow('Invalid arguments: cwd: Required')
})
test('should handle get-brds before project path is set', async () => {
mockRequestHandlers.clear()
const noPathService = new ServerService()
const noPathHandler = mockRequestHandlers.get('call-tool')
await expect(
noPathHandler({
params: {
name: 'get-brds',
arguments: {}, // Deliberately omit cwd
},
})
).rejects.toThrow('Invalid arguments: cwd: Required')
})
test('should handle get-prds before project path is set', async () => {
mockRequestHandlers.clear()
const noPathService = new ServerService()
const noPathHandler = mockRequestHandlers.get('call-tool')
await expect(
noPathHandler({
params: {
name: 'get-prds',
arguments: {}, // Deliberately omit cwd
},
})
).rejects.toThrow('Invalid arguments: cwd: Required')
})
test('should handle get-nfrs before project path is set', async () => {
mockRequestHandlers.clear()
const noPathService = new ServerService()
const noPathHandler = mockRequestHandlers.get('call-tool')
await expect(
noPathHandler({
params: {
name: 'get-nfrs',
arguments: {}, // Deliberately omit cwd
},
})
).rejects.toThrow('Invalid arguments: cwd: Required')
})
test('should handle get-uirs before project path is set', async () => {
mockRequestHandlers.clear()
const noPathService = new ServerService()
const noPathHandler = mockRequestHandlers.get('call-tool')
await expect(
noPathHandler({
params: {
name: 'get-uirs',
arguments: {}, // Deliberately omit cwd
},
})
).rejects.toThrow('Invalid arguments: cwd: Required')
})
test('should handle get-bps before project path is set', async () => {
mockRequestHandlers.clear()
const noPathService = new ServerService()
const noPathHandler = mockRequestHandlers.get('call-tool')
await expect(
noPathHandler({
params: {
name: 'get-bps',
arguments: {}, // Deliberately omit cwd
},
})
).rejects.toThrow('Invalid arguments: cwd: Required')
})
test('should handle get-tasks before project path is set', async () => {
mockRequestHandlers.clear()
const noPathService = new ServerService()
const noPathHandler = mockRequestHandlers.get('call-tool')
await expect(
noPathHandler({
params: {
name: 'get-tasks',
arguments: {
prdId: 'PRD01',
userStoryId: 'US1',
// Deliberately omit cwd
},
},
})
).rejects.toThrow('Invalid arguments: cwd: Required')
})
test('should handle get-task before project path is set', async () => {
mockRequestHandlers.clear()
const noPathService = new ServerService()
const noPathHandler = mockRequestHandlers.get('call-tool')
await expect(
noPathHandler({
params: {
name: 'get-task',
arguments: {
prdId: 'PRD01',
userStoryId: 'US1',
taskId: 'T1',
// Deliberately omit cwd
},
},
})
).rejects.toThrow('Invalid arguments: cwd: Required')
})
test('should handle set-project-path with invalid path', async () => {
const handler = mockRequestHandlers.get('call-tool')
await expect(
handler({
params: {
name: 'set-project-path',
arguments: { path: '/invalid/path' },
},
})
).rejects.toThrow('Invalid path')
})
})
describe('Server Configuration', () => {
test('should initialize with correct server config', () => {
expect(mockServer.requestHandlers.has('list-tools')).toBeTruthy()
expect(mockServer.requestHandlers.has('call-tool')).toBeTruthy()
})
test('should create text response with correct format', () => {
const response = (serverService as any).createTextResponse('test message')
expect(response).toEqual({
content: [
{
type: 'text',
text: 'test message',
},
],
})
})
})
describe('Server Start', () => {
test('should start server successfully', async () => {
await serverService.start()
expect(mockServer.connect).toHaveBeenCalled()
})
})
})