// ***************************************************************************** // Copyright (C) 2026 EclipseSource GmbH. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { expect } from 'chai'; import { buildDiffLabel, parseUserInteractionArgs, parseUserInteractionInput } from './user-interaction-tool'; describe('parseUserInteractionArgs', () => { it('should return undefined for undefined input', () => { expect(parseUserInteractionArgs(undefined)).to.be.undefined; }); it('should return undefined for invalid JSON', () => { expect(parseUserInteractionArgs('not json')).to.be.undefined; }); it('should return undefined when interactions is missing', () => { const input = JSON.stringify({ foo: 'bar' }); expect(parseUserInteractionArgs(input)).to.be.undefined; }); it('should return undefined when interactions is not an array', () => { const input = JSON.stringify({ interactions: 'nope' }); expect(parseUserInteractionArgs(input)).to.be.undefined; }); it('should return undefined when no step is valid', () => { const input = JSON.stringify({ interactions: [{ foo: 'bar' }, { title: 1 }] }); expect(parseUserInteractionArgs(input)).to.be.undefined; }); it('should drop steps that are missing title or message', () => { const input = JSON.stringify({ interactions: [ { title: 'Valid', message: 'Hello' }, { title: 'No message' }, { message: 'No title' } ] }); const result = parseUserInteractionArgs(input); expect(result).to.not.be.undefined; expect(result!.interactions).to.have.length(1); expect(result!.interactions[0].title).to.equal('Valid'); }); it('should accept a step without options (informational)', () => { const input = JSON.stringify({ interactions: [{ title: 'Info', message: 'Just so you know' }] }); const result = parseUserInteractionArgs(input); expect(result).to.not.be.undefined; expect(result!.interactions[0].options).to.be.undefined; }); it('should filter out invalid options within a step', () => { const input = JSON.stringify({ interactions: [{ title: 'T', message: 'M', options: [{ text: 'A', value: 'a' }, { bad: true }, { text: 'B', value: 'b' }] }] }); const result = parseUserInteractionArgs(input); expect(result!.interactions[0].options).to.have.length(2); }); it('should drop options array if no options are valid', () => { const input = JSON.stringify({ interactions: [{ title: 'T', message: 'M', options: [{ bad: true }] }] }); const result = parseUserInteractionArgs(input); expect(result!.interactions[0].options).to.be.undefined; }); it('should normalize singular link into a links array on a step', () => { const input = JSON.stringify({ interactions: [{ title: 'T', message: 'M', options: [{ text: 'A', value: 'a' }], link: { ref: 'src/index.ts' } }] }); const result = parseUserInteractionArgs(input); expect(result!.interactions[0].links).to.deep.equal([{ ref: 'src/index.ts' }]); }); it('should accept a links array with multiple entries', () => { const input = JSON.stringify({ interactions: [{ title: 'T', message: 'M', options: [{ text: 'A', value: 'a' }], links: [ { ref: 'src/a.ts' }, { ref: 'src/old.ts', rightRef: 'src/new.ts' } ] }] }); const result = parseUserInteractionArgs(input); expect(result!.interactions[0].links).to.have.length(2); }); it('should filter out invalid links from a step', () => { const input = JSON.stringify({ interactions: [{ title: 'T', message: 'M', options: [{ text: 'A', value: 'a' }], links: [ { ref: 'src/a.ts' }, { noRef: true }, { ref: '' } ] }] }); const result = parseUserInteractionArgs(input); expect(result!.interactions[0].links).to.have.length(1); }); it('should ignore invalid rightRef placeholders and keep multi-step file links', () => { const emptyRightRefPlaceholder = { path: '', gitRef: '', line: 0, empty: false, label: '' }; const input = JSON.stringify({ interactions: ['README.md', 'package.json', 'CONTRIBUTING.md'].map((path, index) => ({ title: `File link: ${path}`, message: 'Open the file link.', links: [{ ref: { path, gitRef: '', line: 1, empty: false, label: '' }, rightRef: emptyRightRefPlaceholder, label: `Open ${path}`, autoOpen: index === 0 }] })) }); const result = parseUserInteractionArgs(input); expect(result!.interactions.map(step => step.links![0])).to.deep.equal([ { ref: { path: 'README.md', line: 1 }, label: 'Open README.md', autoOpen: true }, { ref: { path: 'package.json', line: 1 }, label: 'Open package.json', autoOpen: false }, { ref: { path: 'CONTRIBUTING.md', line: 1 }, label: 'Open CONTRIBUTING.md', autoOpen: false } ]); }); it('should accept multiple steps in order', () => { const input = JSON.stringify({ interactions: [ { title: 'Overview', message: 'PR summary' }, { title: 'Area 1', message: 'finding', options: [{ text: 'OK', value: 'approve' }] }, { title: 'Area 2', message: 'no findings' } ] }); const result = parseUserInteractionArgs(input); expect(result!.interactions).to.have.length(3); expect(result!.interactions[1].options).to.have.length(1); expect(result!.interactions[2].options).to.be.undefined; }); it('should preserve buttonLabel in options', () => { const input = JSON.stringify({ interactions: [{ title: 'T', message: 'M', options: [{ text: 'Confirm changes', value: 'confirm', buttonLabel: '✅ Confirm' }] }] }); const result = parseUserInteractionArgs(input); expect(result!.interactions[0].options![0].buttonLabel).to.equal('✅ Confirm'); }); it('should reject step links with empty path in object ref', () => { const input = JSON.stringify({ interactions: [{ title: 'T', message: 'M', links: [{ ref: { path: '' } }] }] }); const result = parseUserInteractionArgs(input); expect(result!.interactions[0].links).to.be.undefined; }); it('should accept step links with EmptyContentRef', () => { const input = JSON.stringify({ interactions: [{ title: 'T', message: 'M', links: [{ ref: { empty: true, label: 'New file' } }] }] }); const result = parseUserInteractionArgs(input); expect(result!.interactions[0].links![0].ref).to.deep.equal({ empty: true, label: 'New file' }); }); it('should accept step links with EmptyContentRef as rightRef', () => { const input = JSON.stringify({ interactions: [{ title: 'T', message: 'M', links: [{ ref: 'src/old.ts', rightRef: { empty: true } }] }] }); const result = parseUserInteractionArgs(input); expect(result!.interactions[0].links![0].rightRef).to.deep.equal({ empty: true }); }); }); describe('parseUserInteractionInput', () => { it('should return empty result for undefined input', () => { expect(parseUserInteractionInput(undefined)).to.deep.equal({ title: '', stepCount: 0 }); }); it('should return empty result for empty string', () => { expect(parseUserInteractionInput('')).to.deep.equal({ title: '', stepCount: 0 }); }); it('should extract first step title and step count from valid JSON', () => { const input = JSON.stringify({ interactions: [{ title: 'First' }, { title: 'Second' }] }); expect(parseUserInteractionInput(input)).to.deep.equal({ title: 'First', stepCount: 2 }); }); it('should return empty title and 0 count when interactions array is empty', () => { const input = JSON.stringify({ interactions: [] }); expect(parseUserInteractionInput(input)).to.deep.equal({ title: '', stepCount: 0 }); }); it('should fall back to regex-based title extraction for incomplete JSON', () => { const input = '{"interactions": [{"title": "Streaming Title", "message": "incom'; expect(parseUserInteractionInput(input).title).to.equal('Streaming Title'); }); it('should return empty title from incomplete JSON without title field', () => { const input = '{"interactions": [{"message": "no title here'; expect(parseUserInteractionInput(input).title).to.equal(''); }); }); describe('buildDiffLabel', () => { it('formats two empty refs', () => { expect(buildDiffLabel({ empty: true, label: 'New' }, { empty: true, label: 'Deleted' })) .to.equal('New ⟷ Deleted'); }); it('formats empty left vs path with gitRef', () => { expect(buildDiffLabel({ empty: true, label: 'New' }, { path: 'src/x.ts', gitRef: 'abcdef0123' })) .to.equal('src/x.ts (New ⟷ abcdef0)'); }); it('formats path with gitRef vs working copy of same path', () => { expect(buildDiffLabel({ path: 'src/x.ts', gitRef: 'abcdef0123' }, { path: 'src/x.ts' })) .to.equal('src/x.ts (abcdef0 ⟷ Working Copy)'); }); it('formats two different paths', () => { expect(buildDiffLabel({ path: 'src/old.ts' }, { path: 'src/new.ts' })) .to.equal('src/old.ts ⟷ src/new.ts'); }); });