import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
/**
* Unit tests for pagination fixes in MCP tools
*
* Tests the fixes applied to:
* - list_test_suites (added pagination parameters)
* - get_tcm_test_suites_by_project (fixed token-based pagination)
* - get_root_suites (fixed pagination logic)
* - get_all_tcm_test_cases_by_project (fixed to use token-based pagination)
* - get_root_id_by_suite_id (fixed pagination)
* - get_tcm_suite_by_id (fixed pagination)
*/
describe('Pagination Fixes Unit Tests', () => {
describe('Pagination Parameter Validation', () => {
it('should validate page parameter is non-negative', () => {
const validPage = 0;
const invalidPage = -1;
assert.ok(validPage >= 0, 'Page 0 should be valid');
assert.ok(invalidPage < 0, 'Negative page should be invalid');
});
it('should validate size parameter is positive and within limits', () => {
const validSize = 50;
const invalidSizeZero = 0;
const invalidSizeNegative = -1;
const maxSize = 1000;
const oversizeValue = 1001;
assert.ok(validSize > 0 && validSize <= maxSize, 'Size 50 should be valid');
assert.ok(invalidSizeZero <= 0, 'Size 0 should be invalid');
assert.ok(invalidSizeNegative <= 0, 'Negative size should be invalid');
assert.ok(oversizeValue > maxSize, 'Oversize value should exceed max');
});
it('should handle page token as optional string', () => {
const validToken = 'abc123token';
const emptyToken = '';
const undefinedToken = undefined;
assert.equal(typeof validToken, 'string', 'Valid token should be string');
assert.equal(typeof emptyToken, 'string', 'Empty token should be string');
assert.equal(undefinedToken, undefined, 'Undefined token should be undefined');
});
});
describe('Pagination Response Structure', () => {
it('should structure pagination response correctly', () => {
const mockResponse = {
items: [{ id: 1, title: 'Test Suite 1' }],
_meta: { nextPageToken: 'next123' },
pagination: {
currentPage: 0,
pageSize: 50,
hasNextPage: true,
nextPageToken: 'next123'
}
};
assert.ok(Array.isArray(mockResponse.items), 'Items should be an array');
assert.ok(mockResponse._meta, 'Meta should be present');
assert.ok(mockResponse.pagination, 'Pagination info should be present');
assert.equal(mockResponse.pagination.hasNextPage, !!mockResponse._meta.nextPageToken, 'hasNextPage should match token presence');
});
it('should handle pagination metadata correctly', () => {
const withNextPage = { nextPageToken: 'token123' };
const withoutNextPage = { nextPageToken: undefined };
assert.equal(!!withNextPage.nextPageToken, true, 'Should have next page when token exists');
assert.equal(!!withoutNextPage.nextPageToken, false, 'Should not have next page when token is undefined');
});
});
describe('Token-based vs Page-based Pagination', () => {
it('should prefer token-based pagination over page-based', () => {
// Token-based pagination (correct approach)
const tokenBasedParams = {
size: 50,
pageToken: 'abc123'
};
// Page-based pagination (old, incorrect approach)
const pageBasedParams = {
size: 50,
page: 1
};
assert.ok('pageToken' in tokenBasedParams, 'Token-based should use pageToken');
assert.ok('page' in pageBasedParams, 'Page-based uses page number');
// Verify we're using the correct approach
assert.ok(tokenBasedParams.pageToken, 'pageToken should be preferred');
});
it('should handle pagination loop termination correctly', () => {
// Simulate pagination loop conditions
let pageCount = 0;
const maxPages = 50;
let pageToken: string | undefined = 'initial-token';
const shouldContinue = () => !!(pageToken && pageCount < maxPages);
// Test normal termination (no more pages)
pageToken = undefined;
assert.equal(shouldContinue(), false, 'Should stop when no more pages');
// Test safety limit termination
pageToken = 'still-has-token';
pageCount = 50;
assert.equal(shouldContinue(), false, 'Should stop at safety limit');
// Test normal continuation
pageToken = 'has-token';
pageCount = 10;
assert.equal(shouldContinue(), true, 'Should continue when conditions are met');
});
});
describe('Error Handling in Pagination', () => {
it('should handle MAX_PAGE_SIZE validation', () => {
const MAX_PAGE_SIZE = 100; // Simulated config value
const requestedSize = 150;
const isValidSize = requestedSize <= MAX_PAGE_SIZE;
assert.equal(isValidSize, false, 'Should reject size exceeding MAX_PAGE_SIZE');
const validSize = 50;
const isValidSizeOk = validSize <= MAX_PAGE_SIZE;
assert.equal(isValidSizeOk, true, 'Should accept size within MAX_PAGE_SIZE');
});
it('should provide helpful error messages for pagination issues', () => {
const MAX_PAGE_SIZE = 100;
const requestedSize = 200;
const errorMessage = `โ Error: Requested page size (${requestedSize}) exceeds configured maximum (${MAX_PAGE_SIZE}). Set MAX_PAGE_SIZE environment variable to increase the limit.`;
assert.ok(errorMessage.includes('exceeds configured maximum'), 'Error should explain the limit');
assert.ok(errorMessage.includes('MAX_PAGE_SIZE environment variable'), 'Error should mention configuration');
});
});
describe('Hierarchy Processing with Pagination', () => {
it('should maintain hierarchy information across paginated results', () => {
// Simulate paginated suite data
const page1Suites = [
{ id: 1, title: 'Root Suite', parentSuiteId: null },
{ id: 2, title: 'Child Suite', parentSuiteId: 1 }
];
const page2Suites = [
{ id: 3, title: 'Grandchild Suite', parentSuiteId: 2 }
];
const allSuites = [...page1Suites, ...page2Suites];
// Verify hierarchy can be built from combined results
const rootSuites = allSuites.filter(suite => suite.parentSuiteId === null);
const childSuites = allSuites.filter(suite => suite.parentSuiteId !== null);
assert.equal(rootSuites.length, 1, 'Should find one root suite');
assert.equal(childSuites.length, 2, 'Should find two child suites');
// Verify parent-child relationships
const childOfRoot = allSuites.find(s => s.parentSuiteId === 1);
const grandchild = allSuites.find(s => s.parentSuiteId === 2);
assert.ok(childOfRoot, 'Should find child of root');
assert.ok(grandchild, 'Should find grandchild');
});
});
});