Skip to main content
Glama
project-build-crud.test.ts9.71 kB
// Full mode for write tools // Use required tool helper to avoid undefined checks jest.mock('@/config', () => ({ getTeamCityUrl: () => 'https://example.test', getTeamCityToken: () => 'token', getMCPMode: () => 'full', })); describe('tools: project/build CRUD & updates', () => { afterEach(() => { jest.resetModules(); jest.clearAllMocks(); }); it('create_project and delete_project return structured JSON', async () => { await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { const addProject = jest.fn(async () => ({ data: { id: 'P1' } })); const deleteProject = jest.fn(async () => ({})); jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ projects: { addProject, deleteProject } }) }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); let res = await getRequiredTool('create_project').handler({ name: 'Proj', id: 'P1' }); let payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'create_project', id: 'P1' }); res = await getRequiredTool('delete_project').handler({ projectId: 'P1' }); payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'delete_project', id: 'P1' }); resolve(); })().catch(reject); }); }); }); it('create_build_config returns structured JSON', async () => { await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { const createBuildType = jest.fn(async () => ({ data: { id: 'BT_NEW' } })); const getBuildType = jest.fn(async () => ({ id: 'BT_SRC', project: { id: 'P1' } })); jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: { createBuildType }, getBuildType }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); const res = await getRequiredTool('create_build_config').handler({ projectId: 'P1', name: 'Build', id: 'BT_NEW', }); const payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'create_build_config', id: 'BT_NEW', }); resolve(); })().catch(reject); }); }); }); it('clone_build_config delegates to BuildConfigurationCloneManager and returns metadata', async () => { await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { const retrieveConfiguration = jest.fn(async () => ({ id: 'BT_SRC', name: 'Source Config', projectId: 'P1', })); const cloneConfiguration = jest.fn(async () => ({ id: 'BT_CLONE', name: 'Clone Config', projectId: 'P1', url: 'https://example.test/viewType.html?buildTypeId=BT_CLONE', })); const managerCtor = jest .fn() .mockImplementation(() => ({ retrieveConfiguration, cloneConfiguration })); jest.doMock('@/teamcity/build-configuration-clone-manager', () => ({ BuildConfigurationCloneManager: managerCtor, })); jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: { createBuildType: jest.fn() }, getBuildType: jest.fn(), }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); const res = await getRequiredTool('clone_build_config').handler({ sourceBuildTypeId: 'BT_SRC', name: 'Clone Config', id: 'BT_CLONE', projectId: 'P1', }); const payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(managerCtor).toHaveBeenCalledTimes(1); expect(retrieveConfiguration).toHaveBeenCalledWith('BT_SRC'); expect(cloneConfiguration).toHaveBeenCalledWith( expect.objectContaining({ id: 'BT_SRC' }), expect.objectContaining({ id: 'BT_CLONE', name: 'Clone Config', targetProjectId: 'P1' }) ); expect(payload).toMatchObject({ success: true, action: 'clone_build_config', id: 'BT_CLONE', name: 'Clone Config', projectId: 'P1', url: 'https://example.test/viewType.html?buildTypeId=BT_CLONE', }); resolve(); })().catch(reject); }); }); }); it('clone_build_config surfaces descriptive error when source configuration is missing', async () => { await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { const retrieveConfiguration = jest.fn(async () => null); const cloneConfiguration = jest.fn(); const managerCtor = jest .fn() .mockImplementation(() => ({ retrieveConfiguration, cloneConfiguration })); jest.doMock('@/teamcity/build-configuration-clone-manager', () => ({ BuildConfigurationCloneManager: managerCtor, })); jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: { createBuildType: jest.fn() }, getBuildType: jest.fn(), }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); const res = await getRequiredTool('clone_build_config').handler({ sourceBuildTypeId: 'BT_SRC', name: 'Clone Config', id: 'BT_CLONE', }); const payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(managerCtor).toHaveBeenCalledTimes(1); expect(retrieveConfiguration).toHaveBeenCalledWith('BT_SRC'); expect(cloneConfiguration).not.toHaveBeenCalled(); expect(payload).toMatchObject({ success: false, action: 'clone_build_config', }); expect(payload.error).toMatch(/Source build configuration.+BT_SRC/i); resolve(); })().catch(reject); }); }); }); it('clone_build_config surfaces manager errors to the caller', async () => { await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { const retrieveConfiguration = jest.fn(async () => ({ id: 'BT_SRC', name: 'Source Config', projectId: 'P1', })); const cloneConfiguration = jest.fn(async () => { throw new Error('Permission denied: project requires edit rights'); }); const managerCtor = jest .fn() .mockImplementation(() => ({ retrieveConfiguration, cloneConfiguration })); jest.doMock('@/teamcity/build-configuration-clone-manager', () => ({ BuildConfigurationCloneManager: managerCtor, })); jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: { createBuildType: jest.fn() }, getBuildType: jest.fn(), }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); const res = await getRequiredTool('clone_build_config').handler({ sourceBuildTypeId: 'BT_SRC', name: 'Clone Config', id: 'BT_CLONE', projectId: 'P1', }); const payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload.success).toBe(false); expect(payload).toMatchObject({ action: 'clone_build_config' }); expect(String(payload.error)).toContain('Permission denied'); resolve(); })().catch(reject); }); }); }); it('update_build_config sets fields and returns structured JSON', async () => { await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { const setBuildTypeField = jest.fn(async () => ({})); jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: { setBuildTypeField } }) }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); const res = await getRequiredTool('update_build_config').handler({ buildTypeId: 'BT1', name: 'New Name', description: 'Desc', paused: true, artifactRules: 'artifacts/*.tgz', }); const payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'update_build_config', id: 'BT1', }); resolve(); })().catch(reject); }); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Daghis/teamcity-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server