diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts index 70649ff0fc..6566297acf 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/atxTransformHandler.ts @@ -841,11 +841,6 @@ export class ATXTransformHandler { } } - /** - * Normalize a beam-map repo item into the PascalCase shape the C# IDE consumes, - * tolerant of key-name variants from the web writer (repoName/repo/name, - * artifactId/beamArtifactId, stepId/planStepId, targetFramework/tfm). - */ /** * The wire contract with the web orchestrator / FES is not yet pinned to a single * spelling: a plan-step / HITL id arrives as stepId, planStepId, or parentStepId @@ -855,7 +850,9 @@ export class ATXTransformHandler { */ private getStepId(obj: any): string | undefined { if (obj == null || typeof obj !== 'object') return undefined - return obj.stepId ?? obj.planStepId ?? obj.parentStepId ?? undefined + // Use || (not ??) so an empty-string id falls through to the next spelling; an empty + // stepId must not short-circuit coalescing and then fail every scope match. + return obj.stepId || obj.planStepId || obj.parentStepId || undefined } /** @@ -879,6 +876,11 @@ export class ATXTransformHandler { return { scopedLbv, allOutOfScopeLbv } } + /** + * Normalize a beam-map repo item into the PascalCase shape the C# IDE consumes, + * tolerant of key-name variants from the web writer (repoName/repo/name, + * artifactId/beamArtifactId, stepId/planStepId, targetFramework/tfm). + */ private normalizeBeamRepo(r: BeamMapRepo | null | undefined): Omit { const o: BeamMapRepo = r || {} return { @@ -2001,13 +2003,46 @@ export class ATXTransformHandler { // Plan not available yet } - // Check for pending HITL tasks (e.g. missing packages) - job stays in PLANNING while HITL is pending + // Check for pending HITL tasks (e.g. missing packages) - job stays in PLANNING while HITL is pending. + // Beam multi-repo: several repos can have a pending LBV HITL at once; prefer the loaded repo's + // in-scope one (parity with AWAITING_HUMAN_INPUT). No-op when scope is empty, so non-beam is unchanged. const hitls = await this.listHitls(request.WorkspaceId, request.TransformationJobId) if (hitls && hitls.length > 0) { + const planScopeStepIds = (request.beamScopeStepIds || '') + .split(',') + .map(s => s.trim()) + .filter(s => s.length > 0) + const { scopedLbv: scopedPlanLbv, allOutOfScopeLbv: planAllOutOfScopeLbv } = + this.selectScopedLbvHitl(hitls, planScopeStepIds) + // Beam multi-repo parity with EXECUTING / getHitlAgentArtifact: if scope is set and every + // pending HITL is a sibling repo's LBV (none in the loaded repo's subtree), do NOT surface a + // sibling's LBV — return plan-only. Prevents handing the IDE another repo's build HITL. + if (planScopeStepIds.length > 0 && planAllOutOfScopeLbv) { + this.logging.log( + 'ATX: PLANNING job — all pending HITLs are sibling-repo LBV (none in loaded scope); not surfacing' + ) + return { + TransformationJob: { + WorkspaceId: request.WorkspaceId, + JobId: request.TransformationJobId, + Status: jobStatus, + } as AtxTransformationJob, + TransformationPlan: plan, + } as AtxGetTransformInfoResponse + } + // When scope is set but no in-scope LBV matched, drop out-of-scope LBVs from the fallback + // pool so the plain find() below can't select a sibling's LBV (mixed pending-set case). + const planFallbackPool = + planScopeStepIds.length > 0 && !scopedPlanLbv + ? hitls.filter(h => h.tag !== 'local-build-verification') + : hitls const hitl = - hitls.find(h => h.tag === 'local-build-verification') || - hitls.find(h => h.tag === 'missing-packages' || h.tag === 'handle_missing_packages_hitl') || - hitls[0] + scopedPlanLbv || + planFallbackPool.find(h => h.tag === 'local-build-verification') || + planFallbackPool.find( + h => h.tag === 'missing-packages' || h.tag === 'handle_missing_packages_hitl' + ) || + planFallbackPool[0] this.logging.log(`ATX: Found HITL task - tag: ${hitl.tag}, hasArtifact: ${!!hitl.agentArtifact}`) // For missing packages HITL, try to download artifact if available if (hitl.tag === 'handle_missing_packages_hitl' || hitl.tag === 'missing-packages') { @@ -2036,8 +2071,11 @@ export class ATXTransformHandler { } if (hitl.tag === 'local-build-verification') { this.jobsPastLocalBuild.add(request.TransformationJobId) + // Forward the HITL's plan-step id so a multi-repo beam IDE can confirm this LBV + // belongs to the loaded repo (parity with AWAITING_HUMAN_INPUT). Undefined when absent. + const planLbvStepId = this.getStepId(hitl) this.logging.log( - `ATX: ${jobStatus} job has pending LBV HITL — taskId=${hitl.taskId}; surfacing AWAITING_HUMAN_INPUT to IDE` + `ATX: ${jobStatus} job has pending LBV HITL — taskId=${hitl.taskId} stepId=${planLbvStepId ?? ''}; surfacing AWAITING_HUMAN_INPUT to IDE` ) return { TransformationJob: { @@ -2047,6 +2085,7 @@ export class ATXTransformHandler { } as AtxTransformationJob, HitlTag: hitl.tag, HitlTaskId: hitl.taskId, + StepInformation: planLbvStepId ? { StepId: planLbvStepId } : undefined, TransformationPlan: plan, } as AtxGetTransformInfoResponse } diff --git a/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/atxTransformHandler.test.ts b/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/atxTransformHandler.test.ts index 7f9b4fddbd..b8a0894b5c 100644 --- a/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/atxTransformHandler.test.ts +++ b/server/aws-lsp-codewhisperer/src/language-server/netTransform/tests/atxTransformHandler.test.ts @@ -497,6 +497,30 @@ describe('ATXTransformHandler - getTransformInfo', () => { expect((handler as any).jobsPastLocalBuild.has('job-123')).to.be.true }) + it('should NOT surface a sibling repo LBV when status is PLANNING and all pending LBVs are out of the loaded scope', async () => { + // Beam multi-repo: the IDE loaded one repo (beamScopeStepIds = its subtree). Every pending + // HITL is a local-build-verification for a DIFFERENT (sibling) repo's plan step. Surfacing + // one would make the IDE build the loaded solution against a sibling's HITL → false-green. + // Mirrors the EXECUTING/getHitlAgentArtifact guard: return the plan-only view instead. + getJobStub.resolves({ statusDetails: { status: 'PLANNING' } }) + getTransformationPlanStub.resolves({ Root: { Children: [] } }) + listHitlsStub.resolves([ + { tag: 'local-build-verification', taskId: 'task-sib1', stepId: 'sibling-step-1' }, + { tag: 'local-build-verification', taskId: 'task-sib2', stepId: 'sibling-step-2' }, + ]) + + const result = await handler.getTransformInfo({ + ...baseRequest, + beamScopeStepIds: 'loaded-step-a,loaded-step-b', + }) + + // Plan-only view: original job status preserved, no HITL surfaced to the IDE. + expect(result?.TransformationJob.Status).to.equal('PLANNING') + expect(result?.HitlTag).to.be.undefined + expect(result?.HitlTaskId).to.be.undefined + expect(result?.TransformationPlan).to.deep.equal({ Root: { Children: [] } }) + }) + it('should filter pre-job mode-selection -checkpoint HITL before LBV has run', async () => { getJobStub.resolves({ statusDetails: { status: 'PLANNING' } }) getTransformationPlanStub.resolves({ Root: { Children: [] } }) @@ -4167,6 +4191,15 @@ describe('ATXTransformHandler - Beam to IDE', () => { expect(g('str')).to.be.undefined expect(g({ other: 'x' })).to.be.undefined }) + + it('skips an empty-string stepId and falls through to planStepId / parentStepId (|| not ??)', () => { + const g = (o: any) => (handler as any).getStepId(o) + // Empty stepId must NOT short-circuit coalescing (the ??→|| hardening) — an empty + // string is falsy under ||, so the non-empty planStepId is returned. + expect(g({ stepId: '', planStepId: 'step-1' })).to.equal('step-1') + // Empty stepId + empty planStepId → fall all the way through to parentStepId. + expect(g({ stepId: '', planStepId: '', parentStepId: 'parent-1' })).to.equal('parent-1') + }) }) // --- normalizeBeamRepo: tolerant wire-shape mapping ---