Skip to main content
Glama
manage-build-config-extensions.test.ts27 kB
import { describe, expect, it, jest } from '@jest/globals'; const originalMode = process.env['MCP_MODE']; beforeAll(() => { process.env['MCP_MODE'] = 'full'; }); afterAll(() => { if (typeof originalMode === 'undefined') { delete process.env['MCP_MODE']; } else { process.env['MCP_MODE'] = originalMode; } }); describe('build configuration extended management tools', () => { it('manage_build_dependencies add/update/delete artifact and snapshot dependencies', async () => { jest.resetModules(); await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { const addArtifactDependencyToBuildType = jest.fn(async (..._args: unknown[]) => ({ data: { id: 'artifactDep-1' }, })); const replaceArtifactDependency = jest.fn(async (..._args: unknown[]) => ({ data: { id: 'artifactDep-1' }, })); const deleteArtifactDependency = jest.fn(async (..._args: unknown[]) => ({})); const getArtifactDependency = jest.fn(async (..._args: unknown[]) => ({ data: { id: 'artifactDep-1', type: 'artifactDependency', properties: { property: [{ name: 'cleanDestinationDirectory', value: 'false' }], }, 'source-buildType': { id: 'Upstream_Config' }, }, })); const addSnapshotDependencyToBuildType = jest.fn(async (..._args: unknown[]) => ({ data: { id: 'snapshotDep-2' }, })); const replaceSnapshotDependency = jest.fn(async (..._args: unknown[]) => ({ data: { id: 'snapshotDep-2' }, })); const deleteSnapshotDependency = jest.fn(async (..._args: unknown[]) => ({})); const getSnapshotDependency = jest.fn(async (..._args: unknown[]) => ({ data: { id: 'snapshotDep-2', type: 'snapshotDependency', properties: { property: [{ name: 'run-build-if-dependency-failed', value: 'false' }], }, options: { option: [{ name: 'run-build-on-the-same-agent', value: 'false' }], }, 'source-buildType': { id: 'Base_Config' }, }, })); jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: { addArtifactDependencyToBuildType, replaceArtifactDependency, deleteArtifactDependency, getArtifactDependency, addSnapshotDependencyToBuildType, replaceSnapshotDependency, deleteSnapshotDependency, getSnapshotDependency, }, }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); let res = await getRequiredTool('manage_build_dependencies').handler({ buildTypeId: 'Config_A', dependencyType: 'artifact', action: 'add', dependsOn: 'Upstream_Config', properties: { cleanDestinationDirectory: true, pathRules: 'artifacts.zip=>deploy', }, }); let payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'manage_build_dependencies', operation: 'add', dependencyType: 'artifact', dependencyId: 'artifactDep-1', }); // Verify XML request was sent const call = addArtifactDependencyToBuildType.mock.calls[0]; expect(call).toBeDefined(); const [buildTypeId, fields, xmlBody, headers] = call as [ string, unknown, string, unknown, ]; expect(buildTypeId).toBe('Config_A'); expect(fields).toBeUndefined(); expect(xmlBody).toContain('<artifact-dependency'); expect(xmlBody).toContain('<source-buildType id="Upstream_Config"'); expect(xmlBody).toContain('<properties>'); expect(xmlBody).toContain('name="cleanDestinationDirectory" value="true"'); expect(xmlBody).toContain('name="pathRules" value="artifacts.zip=&gt;deploy"'); expect(headers).toMatchObject({ headers: expect.objectContaining({ 'Content-Type': 'application/xml', Accept: 'application/json', }), }); res = await getRequiredTool('manage_build_dependencies').handler({ buildTypeId: 'Config_A', dependencyType: 'snapshot', action: 'add', dependsOn: 'Base_Config', }); payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'manage_build_dependencies', operation: 'add', dependencyType: 'snapshot', dependencyId: 'snapshotDep-2', }); expect(addSnapshotDependencyToBuildType).toHaveBeenCalledWith( 'Config_A', undefined, expect.stringMatching(/^<snapshot-dependency\b[\s\S]*<\/snapshot-dependency>$/), expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/xml', Accept: 'application/json', }), }) ); expect(addSnapshotDependencyToBuildType.mock.calls[0]?.[2]).toContain( '<source-buildType id="Base_Config"/>' ); res = await getRequiredTool('manage_build_dependencies').handler({ buildTypeId: 'Config_A', dependencyType: 'artifact', action: 'update', dependencyId: 'artifactDep-1', properties: { cleanDestinationDirectory: false, revisionName: 'lastSuccessful', }, }); payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'manage_build_dependencies', operation: 'update', dependencyType: 'artifact', dependencyId: 'artifactDep-1', }); expect(getArtifactDependency).toHaveBeenCalledWith( 'Config_A', 'artifactDep-1', expect.anything(), expect.objectContaining({ headers: expect.objectContaining({ Accept: 'application/json' }), }) ); // Verify XML update request was sent const updateCall = replaceArtifactDependency.mock.calls[0]; expect(updateCall).toBeDefined(); const [ updateBuildTypeId, updateDependencyId, updateFields, updateXmlBody, updateHeaders, ] = updateCall as [string, string, unknown, string, unknown]; expect(updateBuildTypeId).toBe('Config_A'); expect(updateDependencyId).toBe('artifactDep-1'); expect(updateFields).toBeUndefined(); expect(updateXmlBody).toContain('<artifact-dependency'); expect(updateXmlBody).toContain('name="cleanDestinationDirectory" value="false"'); expect(updateXmlBody).toContain('name="revisionName" value="lastSuccessful"'); expect(updateHeaders).toMatchObject({ headers: expect.objectContaining({ 'Content-Type': 'application/xml', Accept: 'application/json', }), }); res = await getRequiredTool('manage_build_dependencies').handler({ buildTypeId: 'Config_A', dependencyType: 'snapshot', action: 'update', dependencyId: 'snapshotDep-2', dependsOn: 'New_Base_Config', properties: { 'run-build-if-dependency-failed': 'true', 'run-build-on-the-same-agent': true, }, }); payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'manage_build_dependencies', operation: 'update', dependencyType: 'snapshot', dependencyId: 'snapshotDep-2', }); expect(getSnapshotDependency).toHaveBeenCalledWith( 'Config_A', 'snapshotDep-2', expect.anything(), expect.objectContaining({ headers: expect.objectContaining({ Accept: 'application/json' }), }) ); expect(replaceSnapshotDependency).toHaveBeenCalledWith( 'Config_A', 'snapshotDep-2', undefined, expect.stringMatching(/^<snapshot-dependency\b[\s\S]*<\/snapshot-dependency>$/), expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/xml', Accept: 'application/json', }), }) ); expect(replaceSnapshotDependency.mock.calls[0]?.[3]).toContain( '<source-buildType id="New_Base_Config"/>' ); const updateSnapshotXml = replaceSnapshotDependency.mock.calls[0]?.[3] as string; expect(updateSnapshotXml).toContain( '<properties><property name="run-build-if-dependency-failed" value="true"/></properties>' ); expect(updateSnapshotXml).toContain( '<options><option name="run-build-on-the-same-agent" value="true"/></options>' ); res = await getRequiredTool('manage_build_dependencies').handler({ buildTypeId: 'Config_A', dependencyType: 'snapshot', action: 'delete', dependencyId: 'snapshotDep-2', }); payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'manage_build_dependencies', operation: 'delete', dependencyType: 'snapshot', dependencyId: 'snapshotDep-2', }); expect(deleteSnapshotDependency).toHaveBeenCalledWith( 'Config_A', 'snapshotDep-2', expect.anything() ); res = await getRequiredTool('manage_build_dependencies').handler({ buildTypeId: 'Config_A', dependencyType: 'artifact', action: 'delete', dependencyId: 'artifactDep-1', }); payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'manage_build_dependencies', operation: 'delete', dependencyType: 'artifact', dependencyId: 'artifactDep-1', }); expect(deleteArtifactDependency).toHaveBeenCalledWith( 'Config_A', 'artifactDep-1', expect.anything() ); resolve(); })().catch(reject); }); }); }); it('manage_build_dependencies surfaces validation error when missing identifiers', async () => { jest.resetModules(); await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: {} }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); const res = await getRequiredTool('manage_build_dependencies').handler({ buildTypeId: 'Cfg', dependencyType: 'artifact', action: 'update', properties: { cleanDestinationDirectory: true }, }); const payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(JSON.stringify(payload)).toContain('dependencyId is required for update/delete'); resolve(); })().catch(reject); }); }); }); it('manage_build_features add/update/delete build features', async () => { jest.resetModules(); await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { const addBuildFeatureToBuildType = jest.fn(async (..._args: unknown[]) => ({ data: { id: 'FEATURE_1' }, })); const replaceBuildFeature = jest.fn(async (..._args: unknown[]) => ({ data: { id: 'FEATURE_1' }, })); const deleteFeatureOfBuildType = jest.fn(async (..._args: unknown[]) => ({})); const getBuildFeature = jest.fn(async (..._args: unknown[]) => ({ data: { id: 'FEATURE_1', type: 'ssh-agent', properties: { property: [{ name: 'teamcity.ssh.agent.key', value: 'id_rsa' }], }, }, })); jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: { addBuildFeatureToBuildType, replaceBuildFeature, deleteFeatureOfBuildType, getBuildFeature, }, }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); let res = await getRequiredTool('manage_build_features').handler({ buildTypeId: 'Cfg_B', action: 'add', type: 'ssh-agent', properties: { 'teamcity.ssh.agent.key': 'id_rsa', 'teamcity.ssh.agent.key.passphrase': '***', }, }); let payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'manage_build_features', operation: 'add', featureId: 'FEATURE_1', }); expect(addBuildFeatureToBuildType).toHaveBeenCalledWith( 'Cfg_B', undefined, expect.objectContaining({ type: 'ssh-agent', properties: { property: expect.arrayContaining([ { name: 'teamcity.ssh.agent.key', value: 'id_rsa' }, { name: 'teamcity.ssh.agent.key.passphrase', value: '***' }, ]), }, }), expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/json', Accept: 'application/json', }), }) ); res = await getRequiredTool('manage_build_features').handler({ buildTypeId: 'Cfg_B', action: 'update', featureId: 'FEATURE_1', properties: { 'teamcity.ssh.agent.key.passphrase': 'updated', }, }); payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'manage_build_features', operation: 'update', featureId: 'FEATURE_1', }); expect(getBuildFeature).toHaveBeenCalledWith( 'Cfg_B', 'FEATURE_1', expect.anything(), expect.objectContaining({ headers: expect.objectContaining({ Accept: 'application/json' }), }) ); expect(replaceBuildFeature).toHaveBeenCalledWith( 'Cfg_B', 'FEATURE_1', undefined, expect.objectContaining({ type: 'ssh-agent', properties: { property: expect.arrayContaining([ { name: 'teamcity.ssh.agent.key', value: 'id_rsa' }, { name: 'teamcity.ssh.agent.key.passphrase', value: 'updated' }, ]), }, }), expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/json', Accept: 'application/json', }), }) ); res = await getRequiredTool('manage_build_features').handler({ buildTypeId: 'Cfg_B', action: 'delete', featureId: 'FEATURE_1', }); payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'manage_build_features', operation: 'delete', featureId: 'FEATURE_1', }); expect(deleteFeatureOfBuildType).toHaveBeenCalledWith( 'Cfg_B', 'FEATURE_1', expect.anything() ); resolve(); })().catch(reject); }); }); }); it('manage_build_features validates identifier requirements', async () => { jest.resetModules(); await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: {} }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); const res = await getRequiredTool('manage_build_features').handler({ buildTypeId: 'Cfg_B', action: 'update', properties: { example: 'value' }, }); const payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(JSON.stringify(payload)).toContain('featureId is required for update/delete'); resolve(); })().catch(reject); }); }); }); it('manage_agent_requirements add/update/delete requirements', async () => { jest.resetModules(); await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { const addAgentRequirementToBuildType = jest.fn(async (..._args: unknown[]) => ({ data: { id: 'REQ_5' }, })); const getAgentRequirement = jest.fn(async (..._args: unknown[]) => ({ data: { id: 'REQ_5', type: 'exists', properties: { property: [ { name: 'property-name', value: 'env.ANSIBLE' }, { name: 'condition', value: 'exists' }, ], }, }, })); const replaceAgentRequirement = jest.fn(async (..._args: unknown[]) => ({ data: { id: 'REQ_5' }, })); const deleteAgentRequirement = jest.fn(async (..._args: unknown[]) => ({})); jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: { addAgentRequirementToBuildType, getAgentRequirement, replaceAgentRequirement, deleteAgentRequirement, }, }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); let res = await getRequiredTool('manage_agent_requirements').handler({ buildTypeId: 'Cfg_C', action: 'add', properties: { 'property-name': 'env.ANSIBLE', condition: 'exists', }, }); let payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'manage_agent_requirements', operation: 'add', requirementId: 'REQ_5', }); // Verify XML request was sent const call = addAgentRequirementToBuildType.mock.calls[0]; expect(call).toBeDefined(); const [buildTypeId, fields, xmlBody, headers] = call as [ string, unknown, string, unknown, ]; expect(buildTypeId).toBe('Cfg_C'); expect(fields).toBeUndefined(); expect(xmlBody).toContain('<agent-requirement'); expect(xmlBody).toContain('<properties>'); expect(xmlBody).toContain('name="property-name" value="env.ANSIBLE"'); expect(xmlBody).toContain('name="condition" value="exists"'); expect(headers).toMatchObject({ headers: expect.objectContaining({ 'Content-Type': 'application/xml', Accept: 'application/json', }), }); res = await getRequiredTool('manage_agent_requirements').handler({ buildTypeId: 'Cfg_C', action: 'update', requirementId: 'REQ_5', properties: { 'property-name': 'env.TERRAFORM', }, }); payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'manage_agent_requirements', operation: 'update', requirementId: 'REQ_5', }); expect(getAgentRequirement).toHaveBeenCalledWith( 'Cfg_C', 'REQ_5', expect.anything(), expect.objectContaining({ headers: expect.objectContaining({ Accept: 'application/json' }), }) ); // Verify XML update request was sent const updateCall = replaceAgentRequirement.mock.calls[0]; expect(updateCall).toBeDefined(); const [ updateBuildTypeId, updateRequirementId, updateFields, updateXmlBody, updateHeaders, ] = updateCall as [string, string, unknown, string, unknown]; expect(updateBuildTypeId).toBe('Cfg_C'); expect(updateRequirementId).toBe('REQ_5'); expect(updateFields).toBeUndefined(); expect(updateXmlBody).toContain('<agent-requirement'); expect(updateXmlBody).toContain('name="property-name" value="env.TERRAFORM"'); expect(updateXmlBody).toContain('name="condition" value="exists"'); expect(updateHeaders).toMatchObject({ headers: expect.objectContaining({ 'Content-Type': 'application/xml', Accept: 'application/json', }), }); res = await getRequiredTool('manage_agent_requirements').handler({ buildTypeId: 'Cfg_C', action: 'delete', requirementId: 'REQ_5', }); payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'manage_agent_requirements', operation: 'delete', requirementId: 'REQ_5', }); expect(deleteAgentRequirement).toHaveBeenCalledWith('Cfg_C', 'REQ_5', expect.anything()); resolve(); })().catch(reject); }); }); }); it('manage_agent_requirements validates requirement identifiers', async () => { jest.resetModules(); await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: {} }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); const res = await getRequiredTool('manage_agent_requirements').handler({ buildTypeId: 'Cfg_C', action: 'delete', }); const payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(JSON.stringify(payload)).toContain('requirementId is required for update/delete'); resolve(); })().catch(reject); }); }); }); it('set_build_config_state toggles paused flag', async () => { jest.resetModules(); await new Promise<void>((resolve, reject) => { jest.isolateModules(() => { (async () => { const setBuildTypeField = jest.fn(async (..._args: unknown[]) => ({})); jest.doMock('@/api-client', () => ({ TeamCityAPI: { getInstance: () => ({ buildTypes: { setBuildTypeField, }, }), }, })); // eslint-disable-next-line @typescript-eslint/no-var-requires const { getRequiredTool } = require('@/tools'); let res = await getRequiredTool('set_build_config_state').handler({ buildTypeId: 'Cfg_D', paused: true, }); let payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'set_build_config_state', buildTypeId: 'Cfg_D', paused: true, }); expect(setBuildTypeField).toHaveBeenCalledWith( 'Cfg_D', 'paused', 'true', expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'text/plain', Accept: 'application/json', }), }) ); res = await getRequiredTool('set_build_config_state').handler({ buildTypeId: 'Cfg_D', paused: false, }); payload = JSON.parse((res.content?.[0]?.text as string) ?? '{}'); expect(payload).toMatchObject({ success: true, action: 'set_build_config_state', buildTypeId: 'Cfg_D', paused: false, }); expect(setBuildTypeField).toHaveBeenLastCalledWith( 'Cfg_D', 'paused', 'false', expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'text/plain', Accept: 'application/json', }), }) ); 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