Skip to main content
Glama
parameters-and-steps-triggers.test.ts16.6 kB
jest.mock('@/config', () => ({ getTeamCityUrl: () => 'https://example.test', getTeamCityToken: () => 'token', getMCPMode: () => 'full', })); describe('tools: parameters, steps, triggers', () => { it('add/update/delete parameter call respective endpoints', async () => { jest.resetModules(); await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { const createBuildParameterOfBuildType = jest.fn(async () => ({})); const updateBuildParameterOfBuildType = jest.fn(async () => ({})); const deleteBuildParameterOfBuildType = jest.fn(async () => ({})); const deleteBuildParameterOfBuildType2 = jest.fn(async () => ({})); jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: { createBuildParameterOfBuildType, updateBuildParameterOfBuildType, deleteBuildParameterOfBuildType, deleteBuildParameterOfBuildType_2: deleteBuildParameterOfBuildType2, }, }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); let res = await getRequiredTool('add_parameter').handler({ buildTypeId: 'bt', name: 'k', value: 'v', }); let payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'add_parameter', buildTypeId: 'bt', name: 'k', }); res = await getRequiredTool('update_parameter').handler({ buildTypeId: 'bt', name: 'k', value: 'v2', }); payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'update_parameter', buildTypeId: 'bt', name: 'k', }); res = await getRequiredTool('delete_parameter').handler({ buildTypeId: 'bt', name: 'k' }); payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'delete_parameter', buildTypeId: 'bt', name: 'k', }); resolve(); })().catch(reject); }); }); }); it('manage_build_steps add/update/delete', async () => { jest.resetModules(); await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { const addBuildStepToBuildType = jest.fn(async () => ({})); const replaceBuildStep = jest.fn(async () => ({})); const deleteBuildStep = jest.fn(async () => ({})); const getBuildStep = jest.fn(async () => ({ data: { id: 'S1', name: 'Existing step', type: 'simpleRunner', disabled: false, properties: { property: [{ name: 'some.setting', value: 'keep' }], }, }, })); jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: { addBuildStepToBuildType, replaceBuildStep, deleteBuildStep, getBuildStep, }, }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); let res = await getRequiredTool('manage_build_steps').handler({ buildTypeId: 'bt', action: 'add', name: 'Run', type: 'simpleRunner', properties: { 'script.content': 'echo hi' }, }); let payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'add_build_step', buildTypeId: 'bt', }); res = await getRequiredTool('manage_build_steps').handler({ buildTypeId: 'bt', action: 'update', stepId: 'S1', properties: { a: 1, b: '2' }, }); payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'update_build_step', buildTypeId: 'bt', stepId: 'S1', }); expect(getBuildStep).toHaveBeenCalled(); expect(replaceBuildStep).toHaveBeenCalledWith( 'bt', 'S1', undefined, expect.objectContaining({ name: 'Existing step', type: 'simpleRunner', properties: { property: expect.arrayContaining([ { name: 'a', value: '1' }, { name: 'b', value: '2' }, { name: 'some.setting', value: 'keep' }, ]), }, }), expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/json', Accept: 'application/json', }), }) ); replaceBuildStep.mockClear(); await getRequiredTool('manage_build_steps').handler({ buildTypeId: 'bt', action: 'update', stepId: 'S2', properties: { 'script.content': 'echo hi' }, }); expect(replaceBuildStep).toHaveBeenCalledWith( 'bt', 'S2', undefined, expect.objectContaining({ properties: { property: expect.arrayContaining([ { name: 'script.content', value: 'echo hi' }, { name: 'use.custom.script', value: 'true' }, { name: 'script.type', value: 'customScript' }, { name: 'some.setting', value: 'keep' }, ]), }, }), expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/json', Accept: 'application/json', }), }) ); res = await getRequiredTool('manage_build_steps').handler({ buildTypeId: 'bt', action: 'delete', stepId: 'S1', }); payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'delete_build_step', buildTypeId: 'bt', stepId: 'S1', }); resolve(); })().catch(reject); }); }); }); it('manage_build_steps update merges existing step defaults when only script changes', async () => { jest.resetModules(); await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { const getBuildStep = jest.fn(async () => ({ data: { id: 'S9', name: 'Run script', type: 'simpleRunner', disabled: false, properties: { property: [ { name: 'script.content', value: 'echo old' }, { name: 'some.setting', value: 'keep' }, ], }, }, })); const replaceBuildStep = jest.fn(async () => ({})); jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: { getBuildStep, replaceBuildStep, }, }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); await getRequiredTool('manage_build_steps').handler({ buildTypeId: 'bt', action: 'update', stepId: 'S9', properties: { 'script.content': 'echo 42', }, }); expect(getBuildStep).toHaveBeenCalledWith( 'bt', 'S9', 'id,name,type,disabled,properties(property(name,value))', expect.objectContaining({ headers: expect.objectContaining({ Accept: 'application/json' }), }) ); expect(replaceBuildStep).toHaveBeenCalledWith( 'bt', 'S9', undefined, expect.objectContaining({ name: 'Run script', type: 'simpleRunner', disabled: false, properties: { property: expect.arrayContaining([ { name: 'script.content', value: 'echo 42' }, { name: 'some.setting', value: 'keep' }, { name: 'use.custom.script', value: 'true' }, { name: 'script.type', value: 'customScript' }, ]), }, }), expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/json', Accept: 'application/json', }), }) ); resolve(); })().catch(reject); }); }); }); it('manage_build_steps update decodes escaped newline sequences in script.content', async () => { jest.resetModules(); await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { const replaceBuildStep = jest.fn(async () => ({})) as jest.Mock; const getBuildStep = jest.fn(async () => ({ data: { id: 'S1', name: 'Existing step', type: 'simpleRunner', disabled: false, properties: { property: [ { name: 'script.type', value: 'customScript' }, { name: 'use.custom.script', value: 'true' }, ], }, }, })); jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: { replaceBuildStep, getBuildStep, }, }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); const scriptWithEscapedNewlines = '#!/bin/bash\\nset -euo pipefail\\necho done'; await getRequiredTool('manage_build_steps').handler({ buildTypeId: 'bt', action: 'update', stepId: 'S1', properties: { 'script.content': scriptWithEscapedNewlines }, }); const replacement = replaceBuildStep.mock.calls[0]?.[3] as | { properties?: { property?: Array<{ name?: string; value?: string }> }; } | undefined; const scriptEntry = replacement?.properties?.property?.find( (prop) => prop?.name === 'script.content' ); expect(scriptEntry).toBeDefined(); expect(scriptEntry?.value).toBe('#!/bin/bash\nset -euo pipefail\necho done'); expect(scriptEntry?.value?.includes('\n')).toBe(true); expect(scriptEntry?.value?.includes('\\n')).toBe(false); resolve(); })().catch(reject); }); }); }); it('manage_build_steps surfaces TeamCity errors with context', async () => { jest.resetModules(); await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { const { TeamCityAPIError } = await import('@/teamcity/errors'); const replaceBuildStep = jest .fn() .mockRejectedValue(new TeamCityAPIError('Bad payload', 'VALIDATION_ERROR', 400)); const getBuildStep = jest.fn(async () => ({ data: { id: 'S9', name: 'Existing step', type: 'simpleRunner', disabled: false, properties: { property: [{ name: 'script.content', value: 'echo old' }], }, }, })); jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: { addBuildStepToBuildType: jest.fn(), replaceBuildStep, deleteBuildStep: jest.fn(), getBuildStep, }, }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); const res = await getRequiredTool('manage_build_steps').handler({ buildTypeId: 'bt', action: 'update', stepId: 'S9', properties: { 'script.content': 'echo hi' }, }); const payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: false, error: { code: 'TEAMCITY_ERROR', message: 'Bad payload', }, }); resolve(); })().catch(reject); }); }); }); it('manage_build_steps validates required identifiers', async () => { jest.resetModules(); await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: { addBuildStepToBuildType: jest.fn(), replaceBuildStep: jest.fn(), deleteBuildStep: jest.fn(), }, }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); const res = await getRequiredTool('manage_build_steps').handler({ buildTypeId: 'bt', action: 'update', }); const payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: false, error: { code: 'VALIDATION_ERROR', }, }); resolve(); })().catch(reject); }); }); }); it('manage_build_triggers add/delete', async () => { jest.resetModules(); await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { const addTriggerToBuildType = jest.fn(async () => ({})); const deleteTrigger = jest.fn(async () => ({})); jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: { addTriggerToBuildType, deleteTrigger } }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); let res = await getRequiredTool('manage_build_triggers').handler({ buildTypeId: 'bt', action: 'add', type: 'vcsTrigger', properties: { branchFilter: '+:*' }, }); let payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'add_build_trigger', buildTypeId: 'bt', }); res = await getRequiredTool('manage_build_triggers').handler({ buildTypeId: 'bt', action: 'delete', triggerId: 'T1', }); payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'delete_build_trigger', buildTypeId: 'bt', triggerId: 'T1', }); 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