Skip to main content
Glama
build-results-and-logs-scenario.test.ts11.8 kB
import { randomUUID } from 'node:crypto'; import { promises as fs } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it } from '@jest/globals'; import type { ActionResult, BuildLogChunk, BuildRef, TriggerBuildResult, } from '../types/tool-results'; import { callTool, callToolsBatch } from './lib/mcp-runner'; const SERIAL_WORKER = process.env['JEST_WORKER_ID'] === '1' || process.env['SERIAL_BUILD_TESTS'] === 'true'; const serialDescribe = SERIAL_WORKER ? describe : describe.skip; const hasTeamCityEnv = Boolean( (process.env['TEAMCITY_URL'] ?? process.env['TEAMCITY_SERVER_URL']) && (process.env['TEAMCITY_TOKEN'] ?? process.env['TEAMCITY_API_TOKEN']) ); const ts = Date.now(); const PROJECT_ID = `E2E_RESULTS_${ts}`; const PROJECT_NAME = `E2E Results ${ts}`; const BT_ID = `E2E_RESULTS_BT_${ts}`; const BT_NAME = `E2E Results BuildType ${ts}`; let buildId: string | undefined; let buildNumber: string | undefined; interface BuildLogStreamResponse { encoding: 'stream'; outputPath: string; bytesWritten: number; meta: { buildId: string; pageSize?: number; startLine?: number; page?: number; buildNumber?: string; buildTypeId?: string; }; } serialDescribe('Build results and logs: full writes + dev reads', () => { afterAll(async () => { try { await callTool('full', 'delete_project', { projectId: PROJECT_ID }); } catch (_e) { expect(true).toBe(true); } }); it('creates project, build config, and step (full)', async () => { if (!hasTeamCityEnv) return expect(true).toBe(true); const batch = await callToolsBatch('full', [ { tool: 'create_project', args: { id: PROJECT_ID, name: PROJECT_NAME, }, }, { tool: 'create_build_config', args: { projectId: PROJECT_ID, id: BT_ID, name: BT_NAME, description: 'Build results/logs scenario', }, }, { tool: 'manage_build_steps', args: { buildTypeId: BT_ID, action: 'add', name: 'log-output', type: 'simpleRunner', properties: { 'script.content': 'echo "line1" && echo "line2" && echo "line3"' }, }, }, ]); expect(batch.results).toHaveLength(3); expect(batch.completed).toBe(true); const [projectStep, configStep, stepStep] = batch.results; const cproj = projectStep?.result as ActionResult | undefined; const cbt = configStep?.result as ActionResult | undefined; const step = stepStep?.result as ActionResult | undefined; expect(projectStep?.ok).toBe(true); expect(cproj).toMatchObject({ success: true, action: 'create_project' }); expect(configStep?.ok).toBe(true); expect(cbt).toMatchObject({ success: true, action: 'create_build_config' }); expect(stepStep?.ok).toBe(true); expect(step).toMatchObject({ success: true, action: 'add_build_step' }); }, 60000); it('triggers a build (dev)', async () => { if (!hasTeamCityEnv) return expect(true).toBe(true); const trig = await callTool<TriggerBuildResult>('dev', 'trigger_build', { buildTypeId: BT_ID, comment: 'e2e-results', }); expect(trig).toMatchObject({ success: true, action: 'trigger_build' }); buildId = trig.buildId; expect(typeof buildId).toBe('string'); }, 90000); it('reads build status and number (dev)', async () => { if (!hasTeamCityEnv) return expect(true).toBe(true); if (!buildId) return expect(true).toBe(true); const b = await callTool<BuildRef>('dev', 'get_build', { buildId }); buildNumber = String(b.number ?? ''); expect(typeof buildNumber).toBe('string'); }, 60000); it('get_build_results with flags (dev)', async () => { if (!hasTeamCityEnv) return expect(true).toBe(true); if (!buildId) return expect(true).toBe(true); const res = await callTool<Record<string, unknown>>('dev', 'get_build_results', { buildId, includeArtifacts: true, includeStatistics: true, includeChanges: true, includeDependencies: true, artifactFilter: '*', maxArtifactSize: 1024, }); expect(res).toBeDefined(); }, 60000); it('get_build_results streaming artifacts returns handles (dev)', async () => { if (!hasTeamCityEnv) return expect(true).toBe(true); if (!buildId) return expect(true).toBe(true); const res = await callTool<Record<string, unknown>>('dev', 'get_build_results', { buildId, includeArtifacts: true, artifactEncoding: 'stream', maxArtifactSize: 1024, }); expect(res).toBeDefined(); const artifacts = res?.['artifacts'] as Array<Record<string, unknown>> | undefined; if (Array.isArray(artifacts) && artifacts.length > 0) { const first = artifacts[0] ?? {}; expect(first).not.toHaveProperty('content'); expect(first).toHaveProperty('downloadHandle'); } }, 60000); it('get_build_results resolves using buildTypeId and buildNumber (dev)', async () => { if (!hasTeamCityEnv) return expect(true).toBe(true); if (!buildNumber) return expect(true).toBe(true); const res = await callTool<Record<string, unknown>>('dev', 'get_build_results', { buildTypeId: BT_ID, buildNumber, includeStatistics: true, }); expect(res).toBeDefined(); const build = (res?.['build'] ?? {}) as { number?: string }; if (build.number) { expect(String(build.number)).toBe(String(buildNumber)); } }, 60000); it('get_build_status resolves using buildTypeId and buildNumber (dev)', async () => { if (!hasTeamCityEnv) return expect(true).toBe(true); if (!buildNumber) return expect(true).toBe(true); let result: Record<string, unknown> | undefined; let lastFailure: Record<string, unknown> | undefined; let attempts = 0; while (attempts < 10) { // eslint-disable-next-line no-await-in-loop const candidate = await callTool<Record<string, unknown>>('dev', 'get_build_status', { buildTypeId: BT_ID, buildNumber, includeTests: true, }); const isFailure = candidate != null && typeof candidate === 'object' && 'success' in candidate && candidate['success'] === false; if (!isFailure) { result = candidate; break; } attempts += 1; lastFailure = candidate; // eslint-disable-next-line no-await-in-loop await new Promise((resolve) => setTimeout(resolve, 1000)); } if (!result && lastFailure) { const failureMessage = typeof lastFailure === 'object' && lastFailure !== null && 'error' in lastFailure ? JSON.stringify(lastFailure['error'], null, 2) : 'Unknown failure'; throw new Error(`get_build_status by buildNumber failed after retries: ${failureMessage}`); } expect(result).toBeDefined(); expect(result?.['success']).not.toBe(false); expect(result?.['buildId']).toBeDefined(); const resolvedNumber = result?.['buildNumber']; if (typeof resolvedNumber === 'string' && resolvedNumber.length > 0) { expect(String(resolvedNumber)).toBe(String(buildNumber)); } }, 60000); it('get_build_results surfaces friendly not-found message for unknown build number (dev)', async () => { if (!hasTeamCityEnv) return expect(true).toBe(true); const bogusNumber = `MISSING-${Date.now()}`; const res = await callTool<Record<string, unknown>>('dev', 'get_build_results', { buildTypeId: BT_ID, buildNumber: bogusNumber, }); expect(res).toBeDefined(); expect(res?.['success']).toBe(false); const error = (res?.['error'] ?? {}) as { message?: string }; expect(error.message ?? '').toMatch(new RegExp(`${BT_ID}[^]*${bogusNumber}`)); }, 60000); it('fetch_build_log with paging and tail (dev)', async () => { if (!hasTeamCityEnv) return expect(true).toBe(true); if (!buildId) return expect(true).toBe(true); type BuildLogResponse = BuildLogChunk | { success: false; error?: { message?: string } }; const page1 = await callTool<BuildLogResponse>('dev', 'fetch_build_log', { buildId, page: 1, pageSize: 200, }); if ('success' in page1 && page1.success === false) { const message = page1.error?.message ?? 'unknown'; if (message.includes('404')) { expect(true).toBe(true); return; } throw new Error(`fetch_build_log page request failed: ${message}`); } expect(page1).toHaveProperty('lines'); const range = await callTool<BuildLogResponse>('dev', 'fetch_build_log', { buildId, startLine: 0, lineCount: 2, }); if ('success' in range && range.success === false) { const message = range.error?.message ?? 'unknown'; if (message.includes('404')) { expect(true).toBe(true); return; } throw new Error(`fetch_build_log range request failed: ${message}`); } expect(range).toHaveProperty('lines'); const tail = await callTool<BuildLogResponse>('dev', 'fetch_build_log', { buildId, tail: true, lineCount: 1, }); if ('success' in tail && tail.success === false) { const message = tail.error?.message ?? 'unknown'; if (message.includes('404')) { expect(true).toBe(true); return; } throw new Error(`fetch_build_log tail request failed: ${message}`); } expect(tail).toHaveProperty('lines'); }, 60000); it('streams a build log segment to disk (dev)', async () => { if (!hasTeamCityEnv) return expect(true).toBe(true); if (!buildId) return expect(true).toBe(true); const targetPath = join(tmpdir(), `build-log-${buildId}-${randomUUID()}.log`); try { const result = await callTool< BuildLogStreamResponse | { success: false; error?: { message?: string } } >('dev', 'fetch_build_log', { buildId, encoding: 'stream', lineCount: 25, outputPath: targetPath, }); if ('success' in result && result.success === false) { const message = result.error?.message ?? 'unknown'; if (message.includes('404') || message.includes('Converting circular structure')) { expect(true).toBe(true); return; } throw new Error(`fetch_build_log streaming request failed: ${message}`); } const streamed = result as BuildLogStreamResponse; expect(streamed.encoding).toBe('stream'); expect(streamed.outputPath).toBe(targetPath); expect(streamed.meta.buildId).toBe(String(buildId)); const written = await fs.readFile(targetPath, 'utf8'); expect(written.length).toBeGreaterThan(0); expect(written).toContain('line1'); expect(streamed.bytesWritten).toBeGreaterThan(0); } finally { await fs.rm(targetPath, { force: true }); } }, 60000); it('fetch_build_log by buildNumber + buildTypeId (dev)', async () => { if (!hasTeamCityEnv) return expect(true).toBe(true); if (!buildNumber) return expect(true).toBe(true); try { const byNumber = await callTool<BuildLogChunk>('dev', 'fetch_build_log', { buildNumber, buildTypeId: BT_ID, page: 1, pageSize: 50, }); expect(byNumber).toHaveProperty('lines'); } catch (e) { // Build number resolution may not be stable across branches; non-fatal expect(true).toBe(true); } }, 60000); it('deletes project (full)', async () => { if (!hasTeamCityEnv) return expect(true).toBe(true); const res = await callTool('full', 'delete_project', { projectId: PROJECT_ID }); expect(res).toMatchObject({ success: true, action: 'delete_project' }); }, 60000); });

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