# Instructions - Following Playwright test failed. - Explain why, be concise, respect Playwright best practices. - Provide a snippet of code with the fix, if possible. # Test info - Name: tssc/full_workflow.test.ts >> TSSC Complete Workflow >> Component Creation >> should create a component successfully - Location: tests/tssc/full_workflow.test.ts:45:5 # Error details ``` Error: Failed to cancel pipelines: Request failed with status code 429 ``` # Test source ```ts 418 | 419 | public async authorizePipelineForAgentPool( 420 | pipelineName: string, 421 | poolName: string 422 | ): Promise { 423 | const pipelineId = await this.azureClient.pipelines.getPipelineIdByName(pipelineName); 424 | const agentQueueId = await this.azureClient.agentPools.getAgentQueueByName(poolName); 425 | return await this.azureClient.agentPools.authorizePipelineForAgentPool(pipelineId!, agentQueueId!.id); 426 | } 427 | 428 | public async authorizePipelineForVariableGroup( 429 | pipelineName: string, 430 | varGroupName: string 431 | ): Promise { 432 | const pipelineId = await this.azureClient.pipelines.getPipelineIdByName(pipelineName); 433 | const variableGroup = await this.azureClient.variableGroups.getVariableGroupByName(varGroupName); 434 | return await this.azureClient.variableGroups.authorizePipelineForVariableGroup(pipelineId!, variableGroup!.id); 435 | } 436 | 437 | public async deleteServiceEndpoint(endpointName: string): Promise { 438 | const endpoint = await this.azureClient.serviceEndpoints.getServiceEndpointByName(endpointName); 439 | const projectId = await this.azureClient.projects.getProjectIdByName(this.projectName); 440 | if (!endpoint) { 441 | this.logger.warn(`Service endpoint with name '${endpointName}' not found. Skipping deletion.`); 442 | return; 443 | } 444 | await this.azureClient.serviceEndpoints.deleteServiceEndpoint(endpoint.id, projectId); 445 | } 446 | 447 | 448 | 449 | /** 450 | * Cancel all pipelines for this component with optional filtering 451 | */ 452 | public override async cancelAllPipelines( 453 | options?: CancelPipelineOptions 454 | ): Promise { 455 | // 1. Normalize options with defaults 456 | const opts = this.normalizeOptions(options); 457 | 458 | // 2. Initialize result object 459 | const result: MutableCancelResult = { 460 | total: 0, 461 | cancelled: 0, 462 | failed: 0, 463 | skipped: 0, 464 | details: [], 465 | errors: [], 466 | }; 467 | 468 | this.logger.info(`[Azure] Starting build cancellation for ${this.componentName}`); 469 | 470 | try { 471 | // 3. Fetch all builds from Azure API 472 | const allBuilds = await this.fetchAllBuilds(); 473 | result.total = allBuilds.length; 474 | 475 | if (allBuilds.length === 0) { 476 | this.logger.info(`[Azure] No builds found for ${this.componentName}`); 477 | return result; 478 | } 479 | 480 | this.logger.info(`[Azure] Found ${allBuilds.length} total builds`); 481 | 482 | // 4. Apply filters 483 | const buildsToCancel = this.filterBuilds(allBuilds, opts); 484 | 485 | this.logger.info(`[Azure] ${buildsToCancel.length} builds match filters`); 486 | this.logger.info(`[Azure] ${allBuilds.length - buildsToCancel.length} builds filtered out`); 487 | 488 | // 5. Cancel builds in batches 489 | await this.cancelBuildsInBatches(buildsToCancel, opts, result); 490 | 491 | // 6. Validate result counts (accounting invariant) 492 | const accounted = result.cancelled + result.failed + result.skipped; 493 | if (accounted !== result.total) { 494 | const missing = result.total - accounted; 495 | this.logger.error( 496 | `❌ [Azure] ACCOUNTING ERROR: ${missing} builds unaccounted for ` + 497 | `(total: ${result.total}, accounted: ${accounted})` 498 | ); 499 | 500 | // Add accounting error to errors array 501 | result.errors.push({ 502 | pipelineId: 'ACCOUNTING_ERROR', 503 | message: `${missing} builds lost in processing`, 504 | error: new Error('Result count mismatch - this indicates a bug in the cancellation logic'), 505 | }); 506 | } 507 | 508 | // 7. Log summary 509 | this.logger.info(`[Azure] Cancellation complete:`, { 510 | total: result.total, 511 | cancelled: result.cancelled, 512 | failed: result.failed, 513 | skipped: result.skipped, 514 | }); 515 | 516 | } catch (error: any) { 517 | this.logger.error(`[Azure] Error in cancelAllPipelines: ${error.message}`); > 518 | throw new Error(`Failed to cancel pipelines: ${error.message}`); | ^ Error: Failed to cancel pipelines: Request failed with status code 429 519 | } 520 | 521 | return result; 522 | } 523 | 524 | 525 | 526 | /** 527 | * Fetch all builds from Azure API 528 | */ 529 | private async fetchAllBuilds(): Promise { 530 | try { 531 | // Get pipeline definitions for both source and gitops repos 532 | const pipelineDefSource = await this.azureClient.pipelines.getPipelineDefinition(this.componentName); 533 | const pipelineDefGitops = await this.azureClient.pipelines.getPipelineDefinition( 534 | this.componentName + '-gitops' 535 | ); 536 | 537 | const builds: AzureBuild[] = []; 538 | 539 | // Fetch builds from source pipeline if it exists 540 | if (pipelineDefSource) { 541 | const runsSource = await this.azureClient.pipelines.listPipelineRuns(pipelineDefSource.id); 542 | const buildsSource = await Promise.all( 543 | runsSource.map(run => this.azureClient.pipelines.getBuild(run.id)) 544 | ); 545 | 546 | // Tag builds with their pipeline name for later cancellation logging 547 | const taggedSourceBuilds = buildsSource.map(build => ({ 548 | ...build, 549 | _pipelineName: this.componentName 550 | })); 551 | builds.push(...taggedSourceBuilds); 552 | } 553 | 554 | // Fetch builds from gitops pipeline if it exists 555 | if (pipelineDefGitops) { 556 | const runsGitops = await this.azureClient.pipelines.listPipelineRuns(pipelineDefGitops.id); 557 | const buildsGitops = await Promise.all( 558 | runsGitops.map(run => this.azureClient.pipelines.getBuild(run.id)) 559 | ); 560 | 561 | // Tag builds with their pipeline name for later cancellation logging 562 | const taggedGitopsBuilds = buildsGitops.map(build => ({ 563 | ...build, 564 | _pipelineName: `${this.componentName}-gitops` 565 | })); 566 | builds.push(...taggedGitopsBuilds); 567 | } 568 | 569 | return builds; 570 | 571 | } catch (error: any) { 572 | this.logger.error(`[Azure] Failed to fetch builds: ${error}`); 573 | throw error; 574 | } 575 | } 576 | 577 | /** 578 | * Filter builds based on cancellation options 579 | */ 580 | private filterBuilds( 581 | builds: AzureBuild[], 582 | options: Required> & Pick 583 | ): AzureBuild[] { 584 | return builds.filter(build => { 585 | // Filter 1: Skip completed builds unless includeCompleted is true 586 | if (!options.includeCompleted && this.isCompletedStatus(build)) { 587 | this.logger.info(`[Filter] Skipping completed build ${build.id} (${build.status})`); 588 | return false; 589 | } 590 | 591 | // Filter 2: Check exclusion patterns 592 | if (this.matchesExclusionPattern(build, options.excludePatterns)) { 593 | this.logger.info(`[Filter] Excluding build ${build.id} by pattern`); 594 | return false; 595 | } 596 | 597 | // Filter 3: Filter by event type if specified 598 | if (options.eventType && !this.matchesEventType(build, options.eventType)) { 599 | this.logger.info(`[Filter] Skipping build ${build.id} (event type mismatch)`); 600 | return false; 601 | } 602 | 603 | // Note: Azure builds don't have branch information directly, 604 | // so we skip branch filtering for Azure 605 | if (options.branch) { 606 | this.logger.info(`[Filter] Branch filtering not supported for Azure DevOps, ignoring branch filter`); 607 | } 608 | 609 | return true; // Include this build for cancellation 610 | }); 611 | } 612 | 613 | /** 614 | * Check if build status is completed 615 | */ 616 | private isCompletedStatus(build: AzureBuild): boolean { 617 | const completedStatuses = ['succeeded', 'failed', 'stopped']; 618 | return completedStatuses.includes(build.status); ```