import { getRequiredTool } from '@/tools';
// Mock TeamCity API client to simulate ambiguous and fallback scenarios
jest.mock('@/api-client', () => {
const listBuilds = jest.fn(async (locator?: string) => {
if (locator?.includes('number:77')) {
// Ambiguous: two builds, no buildTypeId filter provided
return { build: [{ id: 1001 }, { id: 1002 }] };
}
if (locator?.includes('buildType:(id:BT_FALLBACK)') && locator.includes('number:56')) {
// Force fallback by returning empty for the direct locator
return { build: [] };
}
if (locator === 'buildType:(id:BT_FALLBACK),branch:default:any,count:100') {
// Fallback recent list contains the matching number
return { build: [{ id: 2002, number: '56' }] };
}
return { build: [] };
});
const getBuildLogChunk = jest.fn(
(buildId?: string, opts?: { startLine?: number; lineCount?: number }) => {
return Promise.resolve({
lines: [`resolved ${buildId}`],
startLine: opts?.startLine ?? 0,
nextStartLine: undefined,
totalLines: 1,
});
}
);
return {
TeamCityAPI: {
getInstance: () => ({
listBuilds,
getBuildLogChunk,
}),
},
};
});
describe('fetch_build_log buildNumber resolution edge cases', () => {
it('errors when multiple builds match and buildTypeId is missing', async () => {
const res = await getRequiredTool('fetch_build_log').handler({
buildNumber: '77',
page: 1,
pageSize: 10,
});
const payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}');
// runTool wraps errors in a { success: false, error: { message } } shape
expect(payload.success).toBe(false);
const errObj = (payload as { error?: { message?: unknown } }).error;
const message =
typeof errObj?.message === 'string' ? errObj.message : String(errObj?.message ?? '');
expect(message).toContain('Multiple builds match number 77');
});
it('falls back to recent list when direct locator returns none', async () => {
const res = await getRequiredTool('fetch_build_log').handler({
buildNumber: 56,
buildTypeId: 'BT_FALLBACK',
page: 1,
pageSize: 10,
});
const payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}');
expect(payload.meta.buildId).toBe('2002');
expect(payload.meta.buildNumber).toBe('56');
expect(payload.lines[0]).toBe('resolved 2002');
});
});
describe('fetch_build_log buildNumber resolution - no build found scenarios', () => {
beforeEach(() => {
jest.resetModules();
});
it('errors when no build found without buildTypeId', async () => {
jest.doMock('@/api-client', () => ({
TeamCityAPI: {
getInstance: () => ({
listBuilds: jest.fn().mockResolvedValue({ build: [] }),
getBuildLogChunk: jest.fn(),
}),
},
}));
jest.isolateModules(() => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { getRequiredTool } = require('@/tools');
return getRequiredTool('fetch_build_log')
.handler({
buildNumber: '999',
page: 1,
pageSize: 10,
})
.then((res: { content?: Array<{ text?: string }> }) => {
const payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}');
expect(payload.success).toBe(false);
const errObj = (payload as { error?: { message?: unknown } }).error;
const message =
typeof errObj?.message === 'string' ? errObj.message : String(errObj?.message ?? '');
expect(message).toContain('No build found with number 999');
});
});
});
it('errors when fallback search also returns no match', async () => {
jest.doMock('@/api-client', () => ({
TeamCityAPI: {
getInstance: () => ({
listBuilds: jest.fn().mockImplementation(async (locator?: string) => {
// Direct search returns empty
if (locator?.includes('number:888')) {
return { build: [] };
}
// Fallback search also returns builds but none match
if (locator?.includes('branch:default:any,count:100')) {
return {
build: [
{ id: 5001, number: '100' },
{ id: 5002, number: '200' },
],
};
}
return { build: [] };
}),
getBuildLogChunk: jest.fn(),
}),
},
}));
jest.isolateModules(() => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { getRequiredTool } = require('@/tools');
return getRequiredTool('fetch_build_log')
.handler({
buildNumber: '888',
buildTypeId: 'BT_NO_MATCH',
page: 1,
pageSize: 10,
})
.then((res: { content?: Array<{ text?: string }> }) => {
const payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}');
expect(payload.success).toBe(false);
const errObj = (payload as { error?: { message?: unknown } }).error;
const message =
typeof errObj?.message === 'string' ? errObj.message : String(errObj?.message ?? '');
expect(message).toContain('No build found with number 888');
expect(message).toContain('BT_NO_MATCH');
});
});
});
});