mcp-tools.test.ts•47.9 kB
/**
* MCP Tools Integration Tests
*
* Tests all MCP tools directly to ensure proper functionality,
* parameter validation, and response formatting.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { ScratchpadDatabase } from '../src/database/index.js';
import {
createWorkflowTool,
listWorkflowsTool,
getLatestActiveWorkflowTool,
getWorkflowTool,
updateWorkflowStatusTool,
createScratchpadTool,
getScratchpadTool,
getScratchpadOutlineTool,
appendScratchpadTool,
tailScratchpadTool,
listScratchpadsTool,
searchScratchpadsTool,
type CreateWorkflowArgs,
type GetLatestActiveWorkflowArgs,
type GetWorkflowArgs,
type UpdateWorkflowStatusArgs,
type CreateScratchpadArgs,
type GetScratchpadArgs,
type GetScratchpadOutlineArgs,
type AppendScratchpadArgs,
type TailScratchpadArgs,
type ListScratchpadsArgs,
type SearchScratchpadsArgs,
} from '../src/tools/index.js';
/**
* Test helper class for MCP tools
*/
class MCPToolsTestHelper {
private db: ScratchpadDatabase;
// Tool handlers
private createWorkflow: ReturnType<typeof createWorkflowTool>;
private listWorkflows: ReturnType<typeof listWorkflowsTool>;
private getLatestActiveWorkflow: ReturnType<typeof getLatestActiveWorkflowTool>;
private getWorkflow: ReturnType<typeof getWorkflowTool>;
private updateWorkflowStatus: ReturnType<typeof updateWorkflowStatusTool>;
private createScratchpad: ReturnType<typeof createScratchpadTool>;
private getScratchpad: ReturnType<typeof getScratchpadTool>;
private getScratchpadOutline: ReturnType<typeof getScratchpadOutlineTool>;
private appendScratchpad: ReturnType<typeof appendScratchpadTool>;
private tailScratchpad: ReturnType<typeof tailScratchpadTool>;
private listScratchpads: ReturnType<typeof listScratchpadsTool>;
private searchScratchpads: ReturnType<typeof searchScratchpadsTool>;
constructor() {
this.db = new ScratchpadDatabase({ filename: ':memory:' });
// Initialize all tool handlers
this.createWorkflow = createWorkflowTool(this.db);
this.listWorkflows = listWorkflowsTool(this.db);
this.getLatestActiveWorkflow = getLatestActiveWorkflowTool(this.db);
this.getWorkflow = getWorkflowTool(this.db);
this.updateWorkflowStatus = updateWorkflowStatusTool(this.db);
this.createScratchpad = createScratchpadTool(this.db);
this.getScratchpad = getScratchpadTool(this.db);
this.getScratchpadOutline = getScratchpadOutlineTool(this.db);
this.appendScratchpad = appendScratchpadTool(this.db);
this.tailScratchpad = tailScratchpadTool(this.db);
this.listScratchpads = listScratchpadsTool(this.db);
this.searchScratchpads = searchScratchpadsTool(this.db);
}
// Tool wrapper methods with error handling
async callCreateWorkflow(args: CreateWorkflowArgs) {
try {
return await this.createWorkflow(args);
} catch (error) {
return { error: error instanceof Error ? error.message : 'Unknown error' };
}
}
async callListWorkflows() {
try {
return await this.listWorkflows({});
} catch (error) {
return { error: error instanceof Error ? error.message : 'Unknown error' };
}
}
async callGetLatestActiveWorkflow(args: GetLatestActiveWorkflowArgs) {
try {
return await this.getLatestActiveWorkflow(args);
} catch (error) {
return { error: error instanceof Error ? error.message : 'Unknown error' };
}
}
async callGetWorkflow(args: GetWorkflowArgs) {
try {
return await this.getWorkflow(args);
} catch (error) {
return { error: error instanceof Error ? error.message : 'Unknown error' };
}
}
async callUpdateWorkflowStatus(args: UpdateWorkflowStatusArgs) {
try {
return await this.updateWorkflowStatus(args);
} catch (error) {
return { error: error instanceof Error ? error.message : 'Unknown error' };
}
}
async callCreateScratchpad(args: CreateScratchpadArgs) {
try {
return await this.createScratchpad(args);
} catch (error) {
return { error: error instanceof Error ? error.message : 'Unknown error' };
}
}
async callGetScratchpad(args: GetScratchpadArgs) {
try {
return await this.getScratchpad(args);
} catch (error) {
return { error: error instanceof Error ? error.message : 'Unknown error' };
}
}
async callGetScratchpadOutline(args: GetScratchpadOutlineArgs) {
try {
return await this.getScratchpadOutline(args);
} catch (error) {
return { error: error instanceof Error ? error.message : 'Unknown error' };
}
}
async callAppendScratchpad(args: AppendScratchpadArgs) {
try {
return await this.appendScratchpad(args);
} catch (error) {
return { error: error instanceof Error ? error.message : 'Unknown error' };
}
}
async callTailScratchpad(args: TailScratchpadArgs) {
try {
return await this.tailScratchpad(args);
} catch (error) {
return { error: error instanceof Error ? error.message : 'Unknown error' };
}
}
async callListScratchpads(args: ListScratchpadsArgs) {
try {
return await this.listScratchpads(args);
} catch (error) {
return { error: error instanceof Error ? error.message : 'Unknown error' };
}
}
async callSearchScratchpads(args: SearchScratchpadsArgs) {
try {
return await this.searchScratchpads(args);
} catch (error) {
return { error: error instanceof Error ? error.message : 'Unknown error' };
}
}
getDatabase(): ScratchpadDatabase {
return this.db;
}
close(): void {
this.db.close();
}
}
describe('MCP Tools Integration Tests', () => {
let helper: MCPToolsTestHelper;
beforeEach(() => {
helper = new MCPToolsTestHelper();
});
afterEach(() => {
helper.close();
});
describe('Workflow Management Tools', () => {
describe('create-workflow tool', () => {
it('should create a workflow with valid parameters', async () => {
const result = await helper.callCreateWorkflow({
name: 'Test Workflow',
description: 'A test workflow',
});
expect(result).not.toHaveProperty('error');
expect(result).toHaveProperty('workflow');
expect(result).toHaveProperty('message');
expect(result.workflow.name).toBe('Test Workflow');
expect(result.workflow.description).toBe('A test workflow');
expect(result.workflow.id).toBeDefined();
expect(result.workflow.scratchpad_count).toBe(0);
});
it('should create a workflow without description', async () => {
const result = await helper.callCreateWorkflow({
name: 'Minimal Workflow',
});
expect(result).not.toHaveProperty('error');
expect(result.workflow.name).toBe('Minimal Workflow');
expect(result.workflow.description).toBeNull();
});
it('should handle missing name parameter', async () => {
const result = await helper.callCreateWorkflow({} as CreateWorkflowArgs);
expect(result).toHaveProperty('error');
expect(result.error).toContain('name');
});
it('should handle invalid parameter types', async () => {
const result = await helper.callCreateWorkflow({
name: null as any,
});
expect(result).toHaveProperty('error');
});
});
describe('list-workflows tool', () => {
it('should list empty workflows initially', async () => {
const result = await helper.callListWorkflows();
expect(result).not.toHaveProperty('error');
expect(result).toHaveProperty('workflows');
expect(result).toHaveProperty('count');
expect(result.workflows).toHaveLength(0);
expect(result.count).toBe(0);
});
it('should list created workflows', async () => {
// Create some workflows first
await helper.callCreateWorkflow({ name: 'Workflow 1' });
await helper.callCreateWorkflow({ name: 'Workflow 2' });
const result = await helper.callListWorkflows();
expect(result).not.toHaveProperty('error');
expect(result.workflows).toHaveLength(2);
expect(result.count).toBe(2);
const names = result.workflows.map((w: any) => w.name);
expect(names).toContain('Workflow 1');
expect(names).toContain('Workflow 2');
// Verify workflow structure
for (const workflow of result.workflows) {
expect(workflow).toHaveProperty('id');
expect(workflow).toHaveProperty('name');
expect(workflow).toHaveProperty('description');
expect(workflow).toHaveProperty('created_at');
expect(workflow).toHaveProperty('updated_at');
expect(workflow).toHaveProperty('scratchpad_count');
}
});
});
describe('get-workflow tool', () => {
it('should retrieve workflow with valid ID', async () => {
// Create a test workflow first
const createResult = await helper.callCreateWorkflow({
name: 'Test Workflow for Get',
description: 'A workflow for testing get functionality',
});
expect(createResult).not.toHaveProperty('error');
const workflowId = createResult.workflow.id;
// Test getting the workflow
const result = await helper.callGetWorkflow({
workflow_id: workflowId,
});
expect(result).not.toHaveProperty('error');
expect(result).toHaveProperty('workflow');
expect(result).toHaveProperty('message');
expect(result.workflow).not.toBeNull();
expect(result.workflow.id).toBe(workflowId);
expect(result.workflow.name).toBe('Test Workflow for Get');
expect(result.workflow.description).toBe('A workflow for testing get functionality');
expect(result.workflow).toHaveProperty('created_at');
expect(result.workflow).toHaveProperty('updated_at');
expect(result.workflow).toHaveProperty('scratchpad_count');
expect(result.workflow).toHaveProperty('is_active');
expect(result.message).toContain('Found workflow');
});
it('should return null for non-existent workflow ID', async () => {
const result = await helper.callGetWorkflow({
workflow_id: 'non-existent-workflow-id',
});
expect(result).not.toHaveProperty('error');
expect(result).toHaveProperty('workflow');
expect(result).toHaveProperty('message');
expect(result.workflow).toBeNull();
expect(result.message).toContain('Workflow not found');
});
it('should handle missing workflow_id parameter', async () => {
const result = await helper.callGetWorkflow({} as GetWorkflowArgs);
expect(result).toHaveProperty('error');
expect(result.error).toContain('workflow_id');
});
it('should handle invalid workflow_id type', async () => {
const result = await helper.callGetWorkflow({
workflow_id: null as any,
});
expect(result).toHaveProperty('error');
expect(result.error).toContain('workflow_id');
});
it('should include scratchpads_summary by default', async () => {
// Create workflow and scratchpad for testing
const workflowResult = await helper.callCreateWorkflow({
name: 'Workflow with Scratchpads',
});
expect(workflowResult).not.toHaveProperty('error');
const workflowId = workflowResult.workflow.id;
await helper.callCreateScratchpad({
workflow_id: workflowId,
title: 'Test Scratchpad',
content: 'Test content',
});
// Test default behavior (should include summary)
const result = await helper.callGetWorkflow({
workflow_id: workflowId,
});
expect(result).not.toHaveProperty('error');
expect(result.workflow).not.toBeNull();
expect(result.workflow).toHaveProperty('scratchpads_summary');
expect(Array.isArray(result.workflow.scratchpads_summary)).toBe(true);
});
it('should exclude scratchpads_summary when explicitly set to false', async () => {
// Create a test workflow
const workflowResult = await helper.callCreateWorkflow({
name: 'Workflow without Summary',
});
expect(workflowResult).not.toHaveProperty('error');
const workflowId = workflowResult.workflow.id;
// Test with include_scratchpads_summary: false
const result = await helper.callGetWorkflow({
workflow_id: workflowId,
include_scratchpads_summary: false,
});
expect(result).not.toHaveProperty('error');
expect(result.workflow).not.toBeNull();
expect(result.workflow).not.toHaveProperty('scratchpads_summary');
});
it('should handle invalid include_scratchpads_summary type', async () => {
const result = await helper.callGetWorkflow({
workflow_id: 'any-id',
include_scratchpads_summary: 'invalid' as any,
});
expect(result).toHaveProperty('error');
expect(result.error).toContain('include_scratchpads_summary');
});
});
});
describe('Scratchpad CRUD Tools', () => {
let workflowId: string;
beforeEach(async () => {
// Create a test workflow for scratchpad tests
const workflowResult = await helper.callCreateWorkflow({
name: 'Test Workflow for Scratchpads',
});
expect(workflowResult).not.toHaveProperty('error');
workflowId = workflowResult.workflow.id;
});
describe('create-scratchpad tool', () => {
it('should create a scratchpad with valid parameters', async () => {
const result = await helper.callCreateScratchpad({
workflow_id: workflowId,
title: 'Test Scratchpad',
content: 'This is test content for the scratchpad.',
include_content: true,
});
expect(result).not.toHaveProperty('error');
expect(result).toHaveProperty('scratchpad');
expect(result).toHaveProperty('message');
expect(result.scratchpad.title).toBe('Test Scratchpad');
expect(result.scratchpad.content).toBe('This is test content for the scratchpad.');
expect(result.scratchpad.workflow_id).toBe(workflowId);
expect(result.scratchpad.size_bytes).toBeGreaterThan(0);
expect(result.scratchpad.id).toBeDefined();
expect(result.scratchpad.created_at).toBeDefined();
expect(result.scratchpad.updated_at).toBeDefined();
});
it('should handle missing required parameters', async () => {
const result = await helper.callCreateScratchpad({
workflow_id: workflowId,
title: 'Missing Content',
} as CreateScratchpadArgs);
expect(result).toHaveProperty('error');
});
it('should handle invalid workflow_id', async () => {
const result = await helper.callCreateScratchpad({
workflow_id: 'invalid-workflow-id',
title: 'Test Scratchpad',
content: 'Test content',
});
expect(result).toHaveProperty('error');
expect(result.error).toMatch(/workflow|not found|invalid/i);
});
it('should handle large content (approaching 1MB limit)', async () => {
const largeContent = 'x'.repeat(100000); // 100KB content
const result = await helper.callCreateScratchpad({
workflow_id: workflowId,
title: 'Large Scratchpad',
content: largeContent,
});
expect(result).not.toHaveProperty('error');
expect(result.scratchpad.size_bytes).toBe(largeContent.length);
});
});
describe('get-scratchpad tool', () => {
let scratchpadId: string;
beforeEach(async () => {
const createResult = await helper.callCreateScratchpad({
workflow_id: workflowId,
title: 'Test Scratchpad for Get',
content: 'Content for get test',
include_content: true,
});
expect(createResult).not.toHaveProperty('error');
scratchpadId = createResult.scratchpad.id;
});
it('should retrieve scratchpad by id', async () => {
const result = await helper.callGetScratchpad({
id: scratchpadId,
});
expect(result).not.toHaveProperty('error');
expect(result).toHaveProperty('scratchpad');
expect(result.scratchpad).not.toBeNull();
expect(result.scratchpad.id).toBe(scratchpadId);
expect(result.scratchpad.title).toBe('Test Scratchpad for Get');
expect(result.scratchpad.content).toBe('Content for get test');
expect(result.scratchpad.workflow_id).toBe(workflowId);
});
it('should handle invalid scratchpad id', async () => {
const result = await helper.callGetScratchpad({
id: 'invalid-scratchpad-id',
});
expect(result).not.toHaveProperty('error');
expect(result).toHaveProperty('scratchpad');
expect(result.scratchpad).toBeNull();
});
it('should handle missing id parameter', async () => {
const result = await helper.callGetScratchpad({} as GetScratchpadArgs);
expect(result).not.toHaveProperty('error');
expect(result).toHaveProperty('scratchpad');
expect(result.scratchpad).toBeNull();
});
});
describe('get-scratchpad-outline tool', () => {
let scratchpadId: string;
beforeEach(async () => {
const markdownContent = `# Main Title
This is the introduction.
## Section 1
Some content for section 1.
### Subsection 1.1
Detailed content here.
## Section 2
More content in section 2.
#### Deep Section
Very detailed content.`;
const createResult = await helper.callCreateScratchpad({
workflow_id: workflowId,
title: 'Test Scratchpad for Outline',
content: markdownContent,
include_content: true,
});
expect(createResult).not.toHaveProperty('error');
scratchpadId = createResult.scratchpad.id;
});
it('should parse markdown headers correctly', async () => {
const result = await helper.callGetScratchpadOutline({
id: scratchpadId,
});
expect(result).not.toHaveProperty('error');
expect(result).toHaveProperty('outline');
expect(result.outline.headers).toHaveLength(5);
expect(result.outline.headers[0]).toMatchObject({
level: 1,
text: 'Main Title',
line: 1,
});
expect(result.outline.headers[1]).toMatchObject({
level: 2,
text: 'Section 1',
line: 4,
});
expect(result.outline.total_headers).toBe(5);
expect(result.outline.max_depth_found).toBe(4);
});
it('should respect max_depth parameter', async () => {
const result = await helper.callGetScratchpadOutline({
id: scratchpadId,
max_depth: 2,
});
expect(result).not.toHaveProperty('error');
expect(result.outline.headers).toHaveLength(3); // Only levels 1 and 2
expect(result.outline.headers.every(h => h.level <= 2)).toBe(true);
});
it('should include content preview when requested', async () => {
const result = await helper.callGetScratchpadOutline({
id: scratchpadId,
include_content_preview: true,
});
expect(result).not.toHaveProperty('error');
expect(result.outline.headers[0]).toHaveProperty('content_preview');
expect(result.outline.headers[1]).toHaveProperty('content_preview');
expect(result.outline.headers[0].content_preview).toContain('This is the introduction');
});
it('should handle empty content', async () => {
const emptyResult = await helper.callCreateScratchpad({
workflow_id: workflowId,
title: 'Empty Scratchpad',
content: '',
});
expect(emptyResult).not.toHaveProperty('error');
const result = await helper.callGetScratchpadOutline({
id: emptyResult.scratchpad.id,
});
expect(result).not.toHaveProperty('error');
expect(result.outline.headers).toHaveLength(0);
expect(result.outline.total_headers).toBe(0);
expect(result.outline.max_depth_found).toBe(0);
});
it('should handle non-existent scratchpad', async () => {
const result = await helper.callGetScratchpadOutline({
id: 'non-existent-id',
});
expect(result).toHaveProperty('error');
expect(result.error).toContain('Scratchpad not found');
});
});
describe('get-scratchpad with range selection', () => {
let scratchpadId: string;
beforeEach(async () => {
const multiLineContent = `Line 1: First line
Line 2: Second line
Line 3: Third line
Line 4: Fourth line
Line 5: Fifth line
Line 6: Sixth line
Line 7: Seventh line
Line 8: Eighth line
Line 9: Ninth line
Line 10: Tenth line`;
const createResult = await helper.callCreateScratchpad({
workflow_id: workflowId,
title: 'Test Scratchpad for Range',
content: multiLineContent,
include_content: true,
});
expect(createResult).not.toHaveProperty('error');
scratchpadId = createResult.scratchpad.id;
});
it('should extract line range correctly', async () => {
const result = await helper.callGetScratchpad({
id: scratchpadId,
line_range: { start: 3, end: 6 },
});
expect(result).not.toHaveProperty('error');
expect(result.scratchpad).not.toBeNull();
const expectedContent = `Line 3: Third line
Line 4: Fourth line
Line 5: Fifth line
Line 6: Sixth line`;
expect(result.scratchpad.content).toBe(expectedContent);
expect(result.message).toContain('Lines 3-6');
});
it('should extract line context correctly', async () => {
const result = await helper.callGetScratchpad({
id: scratchpadId,
line_context: { line: 5, before: 1, after: 2 },
});
expect(result).not.toHaveProperty('error');
expect(result.scratchpad).not.toBeNull();
const expectedContent = `Line 4: Fourth line
Line 5: Fifth line
Line 6: Sixth line
Line 7: Seventh line`;
expect(result.scratchpad.content).toBe(expectedContent);
expect(result.message).toContain('±1/2 around line 5');
});
it('should reject multiple range parameters', async () => {
const result = await helper.callGetScratchpad({
id: scratchpadId,
line_range: { start: 1, end: 3 },
line_context: { line: 2 },
} as any);
expect(result).toHaveProperty('error');
expect(result.error).toContain('Only one range parameter can be specified');
});
it('should validate line_range parameters', async () => {
// Test invalid start
const result1 = await helper.callGetScratchpad({
id: scratchpadId,
line_range: { start: 0 },
} as any);
expect(result1).toHaveProperty('error');
// Test end < start
const result2 = await helper.callGetScratchpad({
id: scratchpadId,
line_range: { start: 5, end: 3 },
} as any);
expect(result2).toHaveProperty('error');
});
it('should validate line_context parameters', async () => {
// Test line out of bounds
const result = await helper.callGetScratchpad({
id: scratchpadId,
line_context: { line: 15 }, // > 10 lines
});
expect(result).toHaveProperty('error');
expect(result.error).toContain('line_context.line must be between 1 and 10');
});
it('should work with existing parameters (max_content_chars)', async () => {
const result = await helper.callGetScratchpad({
id: scratchpadId,
line_range: { start: 1, end: 5 },
max_content_chars: 50,
});
expect(result).not.toHaveProperty('error');
expect(result.scratchpad).not.toBeNull();
expect(result.scratchpad.content.length).toBeLessThanOrEqual(50);
});
});
describe('append-scratchpad tool', () => {
let scratchpadId: string;
beforeEach(async () => {
const createResult = await helper.callCreateScratchpad({
workflow_id: workflowId,
title: 'Test Scratchpad for Append',
content: 'Initial content',
include_content: true,
});
expect(createResult).not.toHaveProperty('error');
scratchpadId = createResult.scratchpad.id;
});
it('should append content to existing scratchpad', async () => {
const appendResult = await helper.callAppendScratchpad({
id: scratchpadId,
content: '\nAppended content',
include_content: true,
});
expect(appendResult).not.toHaveProperty('error');
expect(appendResult).toHaveProperty('scratchpad');
expect(appendResult.scratchpad.content).toBe('Initial content\n\n---\n<!--- block start --->\n\nAppended content');
expect(appendResult.scratchpad.size_bytes).toBeGreaterThan(15); // Original size
// Verify by getting the scratchpad
const getResult = await helper.callGetScratchpad({ id: scratchpadId });
expect(getResult).not.toHaveProperty('error');
expect(getResult.scratchpad.content).toBe('Initial content\n\n---\n<!--- block start --->\n\nAppended content');
// Verify updated_at changed (allow for same timestamp in fast tests) - now comparing ISO strings
const updatedAt = new Date(appendResult.scratchpad.updated_at).getTime();
const createdAt = new Date(appendResult.scratchpad.created_at).getTime();
expect(updatedAt).toBeGreaterThanOrEqual(createdAt);
});
it('should handle invalid scratchpad id', async () => {
const result = await helper.callAppendScratchpad({
id: 'invalid-id',
content: 'Should not append',
});
expect(result).toHaveProperty('error');
});
it('should handle missing content parameter', async () => {
const result = await helper.callAppendScratchpad({
id: scratchpadId,
content: undefined as any,
});
// Append with undefined content might just append empty string or succeed
// Let's check if this is actually an error condition
if (result.error) {
expect(result).toHaveProperty('error');
} else {
expect(result).not.toHaveProperty('error');
}
});
it('should handle multiple appends in sequence', async () => {
await helper.callAppendScratchpad({
id: scratchpadId,
content: '\nSecond append',
include_content: true,
});
const result = await helper.callAppendScratchpad({
id: scratchpadId,
content: '\nThird append',
include_content: true,
});
expect(result).not.toHaveProperty('error');
expect(result.scratchpad.content).toBe(
'Initial content\n\n---\n<!--- block start --->\n\nSecond append\n\n---\n<!--- block start --->\n\nThird append'
);
});
});
describe('tail-scratchpad tool', () => {
let scratchpadId: string;
const testContent =
'Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10';
beforeEach(async () => {
const createResult = await helper.callCreateScratchpad({
workflow_id: workflowId,
title: 'Test Scratchpad for Tail',
content: testContent,
include_content: true,
});
expect(createResult).not.toHaveProperty('error');
scratchpadId = createResult.scratchpad.id;
});
it('should get tail content by lines (default 50)', async () => {
const result = await helper.callTailScratchpad({
id: scratchpadId,
});
expect(result).not.toHaveProperty('error');
expect(result).toHaveProperty('scratchpad');
expect(result.scratchpad).not.toBeNull();
expect(result.scratchpad.id).toBe(scratchpadId);
expect(result.scratchpad.content).toBe(testContent);
expect(result.scratchpad.is_tail_content).toBe(true);
expect(result.scratchpad.tail_lines).toBe(10);
expect(result.scratchpad.total_lines).toBe(10);
expect(result.scratchpad.tail_chars).toBe(testContent.length);
});
it('should get tail content by specific number of lines', async () => {
const result = await helper.callTailScratchpad({
id: scratchpadId,
tail_size: { lines: 3 },
});
expect(result).not.toHaveProperty('error');
expect(result.scratchpad).not.toBeNull();
expect(result.scratchpad.content).toBe('Line 8\nLine 9\nLine 10');
expect(result.scratchpad.tail_lines).toBe(3);
expect(result.scratchpad.total_lines).toBe(10);
});
it('should get tail content by character count', async () => {
const result = await helper.callTailScratchpad({
id: scratchpadId,
tail_size: { chars: 20 },
});
expect(result).not.toHaveProperty('error');
expect(result.scratchpad).not.toBeNull();
// chars 取最後 20 個字符,實際會取得 "ine 8\nLine 9\nLine 10" (20 字符)
expect(result.scratchpad.content).toBe('ine 8\nLine 9\nLine 10');
expect(result.scratchpad.tail_chars).toBe(20);
expect(result.scratchpad.tail_lines).toBe(3);
});
it('should handle chars-based tail extraction correctly', async () => {
const result = await helper.callTailScratchpad({
id: scratchpadId,
tail_size: { chars: 10 },
});
expect(result).not.toHaveProperty('error');
expect(result.scratchpad).not.toBeNull();
// chars 取最後 10 個字符,實際會取得 " 9\nLine 10" (10 字符)
expect(result.scratchpad.content).toBe(' 9\nLine 10');
expect(result.scratchpad.tail_chars).toBe(10);
expect(result.scratchpad.tail_lines).toBe(2);
});
it('should handle tail with specific lines count', async () => {
const result = await helper.callTailScratchpad({
id: scratchpadId,
tail_size: { lines: 5 },
});
expect(result).not.toHaveProperty('error');
expect(result.scratchpad).not.toBeNull();
expect(result.scratchpad.content).toBe('Line 6\nLine 7\nLine 8\nLine 9\nLine 10');
expect(result.scratchpad.tail_lines).toBe(5);
expect(result.scratchpad.total_lines).toBe(10);
});
it('should handle include_content false', async () => {
const result = await helper.callTailScratchpad({
id: scratchpadId,
include_content: false,
});
expect(result).not.toHaveProperty('error');
expect(result.scratchpad).not.toBeNull();
expect(result.scratchpad.content).toBe('');
expect(result.message).toContain('Content excluded (include_content=false)');
});
it('should handle full content tail extraction', async () => {
const result = await helper.callTailScratchpad({
id: scratchpadId,
tail_size: { lines: 10 },
});
expect(result).not.toHaveProperty('error');
expect(result.scratchpad).not.toBeNull();
expect(result.scratchpad.content).toBe(testContent);
expect(result.scratchpad.tail_lines).toBe(10);
expect(result.scratchpad.total_lines).toBe(10);
});
it('should handle invalid scratchpad id', async () => {
const result = await helper.callTailScratchpad({
id: 'invalid-scratchpad-id',
});
expect(result).not.toHaveProperty('error');
expect(result).toHaveProperty('scratchpad');
expect(result.scratchpad).toBeNull();
});
it('should handle edge case with more lines requested than available', async () => {
const result = await helper.callTailScratchpad({
id: scratchpadId,
tail_size: { lines: 100 }, // More than the 10 lines available
});
expect(result).not.toHaveProperty('error');
expect(result.scratchpad).not.toBeNull();
expect(result.scratchpad.content).toBe(testContent);
expect(result.scratchpad.tail_lines).toBe(10); // Should return all available lines
expect(result.scratchpad.total_lines).toBe(10);
});
it('should handle edge case with more chars requested than available', async () => {
const result = await helper.callTailScratchpad({
id: scratchpadId,
tail_size: { chars: 1000 }, // More than the content length
});
expect(result).not.toHaveProperty('error');
expect(result.scratchpad).not.toBeNull();
expect(result.scratchpad.content).toBe(testContent);
expect(result.scratchpad.tail_chars).toBe(testContent.length);
});
// Full content mode tests
it('should get full content when full_content is true', async () => {
const result = await helper.callTailScratchpad({
id: scratchpadId,
full_content: true,
});
expect(result).not.toHaveProperty('error');
expect(result.scratchpad).not.toBeNull();
expect(result.scratchpad.content).toBe(testContent);
expect(result.scratchpad.is_tail_content).toBe(false); // Should be false for full content
expect(result.scratchpad.tail_lines).toBe(10); // All lines
expect(result.scratchpad.total_lines).toBe(10);
expect(result.scratchpad.tail_chars).toBe(testContent.length);
expect(result.message).toContain('Retrieved full content from');
});
it('should override tail_size when full_content is true', async () => {
const result = await helper.callTailScratchpad({
id: scratchpadId,
tail_size: { lines: 3 }, // This should be ignored
full_content: true,
});
expect(result).not.toHaveProperty('error');
expect(result.scratchpad).not.toBeNull();
expect(result.scratchpad.content).toBe(testContent); // Full content, not just 3 lines
expect(result.scratchpad.is_tail_content).toBe(false);
expect(result.scratchpad.tail_lines).toBe(10); // All lines, not 3
expect(result.message).toContain('Retrieved full content from');
expect(result.message).toContain('full content');
});
it('should respect include_content=false even with full_content=true', async () => {
const result = await helper.callTailScratchpad({
id: scratchpadId,
full_content: true,
include_content: false,
});
expect(result).not.toHaveProperty('error');
expect(result.scratchpad).not.toBeNull();
expect(result.scratchpad.content).toBe(''); // Content should be excluded
expect(result.scratchpad.is_tail_content).toBe(false);
expect(result.scratchpad.tail_lines).toBe(10);
expect(result.message).toContain('Content excluded (include_content=false)');
});
});
});
describe('Search and List Tools', () => {
let workflowId: string;
let scratchpadIds: string[] = [];
beforeEach(async () => {
// Create test workflow
const workflowResult = await helper.callCreateWorkflow({
name: 'Search Test Workflow',
});
expect(workflowResult).not.toHaveProperty('error');
workflowId = workflowResult.workflow.id;
// Create test scratchpads
const testScratchpads = [
{
title: 'JavaScript Notes',
content: 'JavaScript is a programming language used for web development',
},
{
title: 'Python Guide',
content: 'Python is great for data science and machine learning applications',
},
{
title: 'API Documentation',
content: 'REST API endpoints for user management and authentication',
},
{
title: 'React Components',
content: 'React functional components with hooks for state management',
},
{
title: 'Database Schema',
content: 'SQL database schema design for user accounts and permissions',
},
];
for (const pad of testScratchpads) {
const result = await helper.callCreateScratchpad({
workflow_id: workflowId,
title: pad.title,
content: pad.content,
});
expect(result).not.toHaveProperty('error');
scratchpadIds.push(result.scratchpad.id);
}
});
describe('list-scratchpads tool', () => {
it('should list all scratchpads in workflow', async () => {
const result = await helper.callListScratchpads({
workflow_id: workflowId,
});
expect(result).not.toHaveProperty('error');
expect(result).toHaveProperty('scratchpads');
expect(result).toHaveProperty('count');
expect(result.scratchpads).toHaveLength(5);
expect(result.count).toBe(5);
const titles = result.scratchpads.map((s: any) => s.title);
expect(titles).toContain('JavaScript Notes');
expect(titles).toContain('Python Guide');
expect(titles).toContain('API Documentation');
// Verify scratchpad structure
for (const scratchpad of result.scratchpads) {
expect(scratchpad).toHaveProperty('id');
expect(scratchpad).toHaveProperty('title');
expect(scratchpad).toHaveProperty('content');
expect(scratchpad).toHaveProperty('workflow_id');
expect(scratchpad).toHaveProperty('size_bytes');
expect(scratchpad).toHaveProperty('created_at');
expect(scratchpad).toHaveProperty('updated_at');
}
});
it('should handle limit parameter', async () => {
const result = await helper.callListScratchpads({
workflow_id: workflowId,
limit: 2,
});
expect(result).not.toHaveProperty('error');
expect(result.scratchpads).toHaveLength(2);
expect(result.count).toBe(2);
});
it('should handle invalid workflow_id', async () => {
const result = await helper.callListScratchpads({
workflow_id: 'invalid-workflow-id',
});
// Should not error, but return empty list
expect(result).not.toHaveProperty('error');
expect(result.scratchpads).toHaveLength(0);
expect(result.count).toBe(0);
});
it('should handle list with workflow_id', async () => {
const result = await helper.callListScratchpads({
workflow_id: workflowId,
});
expect(result).not.toHaveProperty('error');
expect(result).toHaveProperty('scratchpads');
expect(result).toHaveProperty('count');
expect(result.count).toBe(result.scratchpads.length);
// Should return the scratchpads created in this test
});
});
describe('search-scratchpads tool', () => {
it('should search scratchpads by content', async () => {
const result = await helper.callSearchScratchpads({
query: 'programming',
});
expect(result).not.toHaveProperty('error');
expect(result).toHaveProperty('results');
expect(result).toHaveProperty('count');
expect(result).toHaveProperty('query', 'programming');
expect(result).toHaveProperty('search_method');
expect(['fts5', 'like']).toContain(result.search_method);
// Should find JavaScript Notes
expect(result.results.length).toBeGreaterThan(0);
const titles = result.results.map((r: any) => r.scratchpad.title);
expect(titles).toContain('JavaScript Notes');
});
it('should search with workflow filter', async () => {
const result = await helper.callSearchScratchpads({
query: 'development',
workflow_id: workflowId,
});
expect(result).not.toHaveProperty('error');
expect(result.results.length).toBeGreaterThan(0);
// All results should be from the specified workflow
for (const searchResult of result.results) {
expect(searchResult.workflow.id).toBe(workflowId);
}
});
it('should handle search limit', async () => {
const result = await helper.callSearchScratchpads({
query: 'a', // Broad query to match multiple results
limit: 2,
});
expect(result).not.toHaveProperty('error');
expect(result.results.length).toBeLessThanOrEqual(2);
});
it('should include snippets and ranks in search results', async () => {
const result = await helper.callSearchScratchpads({
query: 'JavaScript',
});
expect(result).not.toHaveProperty('error');
expect(result.results.length).toBeGreaterThan(0);
for (const searchResult of result.results) {
expect(searchResult).toHaveProperty('snippet');
expect(searchResult).toHaveProperty('rank');
expect(searchResult).toHaveProperty('scratchpad');
expect(searchResult).toHaveProperty('workflow');
expect(typeof searchResult.snippet).toBe('string');
expect(typeof searchResult.rank).toBe('number');
}
});
it('should handle missing query parameter', async () => {
const result = await helper.callSearchScratchpads({} as SearchScratchpadsArgs);
// Check if search actually fails or returns empty results
if (result.error) {
expect(result).toHaveProperty('error');
} else {
expect(result).not.toHaveProperty('error');
expect(result).toHaveProperty('results');
expect(result.results).toHaveLength(0);
}
});
it('should handle empty search results', async () => {
const result = await helper.callSearchScratchpads({
query: 'nonexistent-search-term-xyz-123',
});
expect(result).not.toHaveProperty('error');
expect(result.results).toHaveLength(0);
expect(result.count).toBe(0);
});
it('should handle case-insensitive search', async () => {
const result = await helper.callSearchScratchpads({
query: 'JAVASCRIPT', // Uppercase
});
expect(result).not.toHaveProperty('error');
expect(result.results.length).toBeGreaterThan(0);
const titles = result.results.map((r: any) => r.scratchpad.title);
expect(titles).toContain('JavaScript Notes');
});
it('should handle partial word matching', async () => {
const result = await helper.callSearchScratchpads({
query: 'manage', // Should match "management"
});
expect(result).not.toHaveProperty('error');
expect(result.results.length).toBeGreaterThan(0);
});
});
});
describe('Integration and Edge Cases', () => {
it('should handle workflow with many scratchpads', async () => {
// Create workflow
const workflowResult = await helper.callCreateWorkflow({
name: 'High Volume Workflow',
});
expect(workflowResult).not.toHaveProperty('error');
const workflowId = workflowResult.workflow.id;
// Create many scratchpads
const createPromises = [];
for (let i = 0; i < 20; i++) {
createPromises.push(
helper.callCreateScratchpad({
workflow_id: workflowId,
title: `Scratchpad ${i + 1}`,
content: `This is content for scratchpad number ${i + 1}. It contains unique data ${Math.random()}.`,
})
);
}
const results = await Promise.all(createPromises);
// All should succeed
for (const result of results) {
expect(result).not.toHaveProperty('error');
}
// List should return all scratchpads
const listResult = await helper.callListScratchpads({
workflow_id: workflowId,
});
expect(listResult).not.toHaveProperty('error');
expect(listResult.scratchpads).toHaveLength(20);
expect(listResult.count).toBe(20);
});
it('should maintain data consistency across operations', async () => {
// Create workflow
const workflowResult = await helper.callCreateWorkflow({
name: 'Consistency Test Workflow',
});
expect(workflowResult).not.toHaveProperty('error');
const workflowId = workflowResult.workflow.id;
// Create scratchpad
const createResult = await helper.callCreateScratchpad({
workflow_id: workflowId,
title: 'Consistency Test Pad',
content: 'Original content',
});
expect(createResult).not.toHaveProperty('error');
const scratchpadId = createResult.scratchpad.id;
// Verify via get
const getResult1 = await helper.callGetScratchpad({
id: scratchpadId,
});
expect(getResult1).not.toHaveProperty('error');
expect(getResult1.scratchpad.content).toBe('Original content');
// Append content
const appendResult = await helper.callAppendScratchpad({
id: scratchpadId,
content: '\nAdded content',
});
expect(appendResult).not.toHaveProperty('error');
// Verify via get again
const getResult2 = await helper.callGetScratchpad({
id: scratchpadId,
});
expect(getResult2).not.toHaveProperty('error');
expect(getResult2.scratchpad.content).toBe('Original content\n\n---\n<!--- block start --->\n\nAdded content');
// Search should also reflect changes
const searchResult = await helper.callSearchScratchpads({
query: 'Added content',
});
expect(searchResult).not.toHaveProperty('error');
expect(searchResult.results.length).toBeGreaterThan(0);
const foundScratchpad = searchResult.results.find(
(r: any) => r.scratchpad.id === scratchpadId
);
expect(foundScratchpad).toBeDefined();
});
it('should handle concurrent operations safely', async () => {
// Create workflow
const workflowResult = await helper.callCreateWorkflow({
name: 'Concurrent Test Workflow',
});
expect(workflowResult).not.toHaveProperty('error');
const workflowId = workflowResult.workflow.id;
// Create initial scratchpad
const createResult = await helper.callCreateScratchpad({
workflow_id: workflowId,
title: 'Concurrent Test Pad',
content: 'Base content',
});
expect(createResult).not.toHaveProperty('error');
const scratchpadId = createResult.scratchpad.id;
// Perform concurrent appends
const appendPromises = [];
for (let i = 0; i < 5; i++) {
appendPromises.push(
helper.callAppendScratchpad({
id: scratchpadId,
content: `\nConcurrent append ${i + 1}`,
})
);
}
const appendResults = await Promise.all(appendPromises);
// All should succeed
for (const result of appendResults) {
expect(result).not.toHaveProperty('error');
}
// Final state should be consistent
const finalResult = await helper.callGetScratchpad({
id: scratchpadId,
});
expect(finalResult).not.toHaveProperty('error');
expect(finalResult.scratchpad.content).toContain('Base content');
expect(finalResult.scratchpad.content).toContain('Concurrent append');
});
});
});