diff --git a/src/domain/browserScenarioDiff.test.ts b/src/domain/browserScenarioDiff.test.ts index 51041fd40b..bdac324b4f 100644 --- a/src/domain/browserScenarioDiff.test.ts +++ b/src/domain/browserScenarioDiff.test.ts @@ -1077,6 +1077,61 @@ describe("browser scenario comparison validation", () => { }); }); + const duplicateLastStepCapture = () => { + const base = scenarioCapture({}); + return { + ...base, + scenario: { ...base.scenario, action_count: 2 }, + steps: [...base.steps, { ...base.steps[1]!, step_index: 2 }], + }; + }; + + it("keeps a duplicated step id unaligned without classifying it as absent", () => { + const compared = compareBrowserScenarios( + compareBrowserScenariosInputSchema.parse({ + before_scenario: duplicateLastStepCapture(), + after_scenario: scenarioCapture({}), + }), + ); + + expect(compared.overall_status).toBe("unknown"); + expect(compared.alignment).toMatchObject({ + status: "partial", + aligned_steps: 1, + before_only: [], + after_only: [], + failures: [ + expect.objectContaining({ + code: "duplicate_before_step_id", + step_id: "open-settings", + }), + ], + }); + }); + + it("mirrors the duplicate classification when the later capture repeats the id", () => { + const compared = compareBrowserScenarios( + compareBrowserScenariosInputSchema.parse({ + before_scenario: scenarioCapture({}), + after_scenario: duplicateLastStepCapture(), + }), + ); + + expect(compared.overall_status).toBe("unknown"); + expect(compared.alignment).toMatchObject({ + status: "partial", + aligned_steps: 1, + before_only: [], + after_only: [], + failures: [ + expect.objectContaining({ + code: "duplicate_after_step_id", + step_id: "open-settings", + }), + ], + }); + }); + it("rejects duplicate normalization rule IDs", () => { const capture = scenarioCapture({}); expect( diff --git a/src/domain/browserScenarioDiff.ts b/src/domain/browserScenarioDiff.ts index e74255567f..c53184c150 100644 --- a/src/domain/browserScenarioDiff.ts +++ b/src/domain/browserScenarioDiff.ts @@ -151,11 +151,14 @@ const alignScenarioSteps = ( const afterDuplicates = duplicateStepIds(afterCapture.steps); const before = uniqueSteps(beforeCapture.steps, beforeDuplicates); const after = uniqueSteps(afterCapture.steps, afterDuplicates); + // A duplicated step_id keeps its ambiguous presence on that side: it stays + // ineligible for alignment, but its counterpart capture must not classify + // the id as absent when the duplicate side still retains it. const beforeOnly = [...before.keys()] - .filter((stepId) => !after.has(stepId)) + .filter((stepId) => !after.has(stepId) && !afterDuplicates.has(stepId)) .sort(); const afterOnly = [...after.keys()] - .filter((stepId) => !before.has(stepId)) + .filter((stepId) => !before.has(stepId) && !beforeDuplicates.has(stepId)) .sort(); const failures: BrowserScenarioAlignmentFailure[] = [ ...duplicateFailures("before", beforeDuplicates),