Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2f45437
fix: preserve original key in merge dialog import column
0xRozier Mar 24, 2026
1260fc0
Refactor importCleanedEntries to handle key generation
0xRozier Mar 24, 2026
88519aa
Refactor import handler for better readability
0xRozier Mar 24, 2026
8587087
Implement setMergedFieldValue method
0xRozier Mar 24, 2026
a729561
Add method to set merged field value in ThreeWayMergeView
0xRozier Mar 24, 2026
a3bc76d
Add test for citation key preservation on import
0xRozier Mar 24, 2026
453708b
Fix import handler test by reordering imports
0xRozier Mar 24, 2026
9e91dd5
Update CHANGELOG with recent bug fixes and changes
0xRozier Mar 24, 2026
3ba665b
Remove redundant citation key setting in ImportHandler
0xRozier Mar 24, 2026
0b4eeb8
Set citation key for original entry if empty
0xRozier Mar 24, 2026
cabdce5
Refactor duplicate handling methods in ImportHandler
0xRozier Mar 25, 2026
8c34e61
Refactor citation key preservation assertion in tests
0xRozier Mar 25, 2026
cbaedd0
Remove duplicate entry in CHANGELOG
0xRozier Mar 25, 2026
644fe5e
Refactor getDuplicateDecision method calls in tests
0xRozier Mar 25, 2026
aac7510
Clean up CHANGELOG formatting
0xRozier Mar 25, 2026
f2452bf
Fix formatting in ImportHandlerTest.java
0xRozier Mar 25, 2026
64fe4fd
Fix indentation in ImportHandler.java
0xRozier Mar 25, 2026
83eb174
Remove unnecessary blank lines in ImportHandler.java
0xRozier Mar 25, 2026
302acd4
Fix Javadoc continuation line alignment in importCleanedEntries
0xRozier Mar 25, 2026
e5007dd
Fix Javadoc continuation line alignment in ImportHandler
0xRozier Mar 25, 2026
a01bb5b
Fix Javadoc formatting: merge continuation line to avoid non-idempote…
0xRozier Mar 25, 2026
81e8196
Merge branch 'main' into fix/issue-8644-citationkey-merge-dialog-over…
0xRozier Mar 27, 2026
13a092e
Update duplicate decision mocks in ImportHandlerTest
0xRozier Mar 30, 2026
576da74
Refactor citation key handling in ImportHandler
0xRozier Mar 30, 2026
d0aaef1
Add test for handling duplicates with generated keys
0xRozier Mar 30, 2026
2d0d757
Refactor duplicate handling in ImportHandlerTest
0xRozier Mar 30, 2026
e0b521e
Fix formatting of getDuplicateDecision call
0xRozier Mar 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We fixed an issue where a tab or the tab bar would not show, while the setting "hide tab bar when a single library is present" was toggled off [#12680](https://github.com/JabRef/jabref/issues/12680)
- We improved CSL support with JabRef LibreOffice converter extension. [#14387](https://github.com/JabRef/jabref/issues/14387)
- We fixed exceptions occuring when generating citation keys or using certain cleanup operations on macOS [#15366](https://github.com/JabRef/jabref/issues/15366)
- We fixed the merge dialog overwriting the citation key chosen by the user during duplicate import. [#8644](https://github.com/JabRef/jabref/issues/8644)

### Removed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.jabref.logic.help.HelpFile;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.field.Field;

public class DuplicateResolverDialog extends BaseDialog<DuplicateResolverResult> {

Expand Down Expand Up @@ -162,6 +163,10 @@ private void init(BibEntry one, BibEntry two, DuplicateResolverType type) {
getDialogPane().setContent(borderPane);
}

public void setMergedFieldValue(Field field, String value) {
threeWayMerge.setMergedFieldValue(field, value);
}

public BibEntry getMergedEntry() {
return threeWayMerge.getMergedEntry();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import org.jabref.model.entry.BibEntryTypesManager;
import org.jabref.model.entry.BibtexString;
import org.jabref.model.entry.LinkedFile;
import org.jabref.model.entry.field.InternalField;
import org.jabref.model.entry.field.StandardField;
import org.jabref.model.groups.ExplicitGroup;
import org.jabref.model.groups.GroupEntryChanger;
Expand Down Expand Up @@ -240,8 +241,20 @@ public void importEntries(List<BibEntry> entries) {
}

public void importCleanedEntries(@Nullable TransferInformation transferInformation, List<BibEntry> entries) {
importCleanedEntries(transferInformation, entries, false);
}

/// Imports cleaned entries into the database.
///
/// @param transferInformation optional transfer information for adjusting linked files
/// @param entries the entries to import
/// @param skipKeyGeneration if true, skip citation key generation (e.g., when the key was already generated before the merge dialog so the user could choose/edit it)

private void importCleanedEntries(@Nullable TransferInformation transferInformation, List<BibEntry> entries, boolean skipKeyGeneration) {
targetBibDatabaseContext.getDatabase().insertEntries(entries);
generateKeys(entries);
if (!skipKeyGeneration) {
generateKeys(entries);
}
setAutomaticFields(entries);
addToGroups(entries, stateManager.getSelectedGroups(targetBibDatabaseContext));
addToImportEntriesGroup(entries);
Expand Down Expand Up @@ -279,17 +292,25 @@ private void importEntryWithDuplicateCheck(@Nullable TransferInformation transfe
LOGGER.error("Error in duplicate search", e);
})
.onSuccess(existingDuplicateInLibrary -> {
// Generate citation key string WITHOUT modifying the entry,
// so the "From import" column shows the original key
Optional<String> generatedKey = generateKeyString(entryToInsert);

BibEntry finalEntry = entryToInsert;
if (existingDuplicateInLibrary.isPresent()) {
Optional<BibEntry> duplicateHandledEntry = handleDuplicates(entryToInsert, existingDuplicateInLibrary.get(), decision);
Optional<BibEntry> duplicateHandledEntry = handleDuplicates(entryToInsert, existingDuplicateInLibrary.get(), decision, generatedKey);
if (duplicateHandledEntry.isEmpty()) {
tracker.markSkipped();
return;
}
finalEntry = duplicateHandledEntry.get();
} else {
// No duplicate: apply the generated key directly
generatedKey.ifPresent(key -> entryToInsert.setCitationKey(key));
}

importCleanedEntries(transferInformation, List.of(finalEntry));
// Skip key generation since it was already handled
importCleanedEntries(transferInformation, List.of(finalEntry), true);

Comment on lines +295 to 314
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Duplicate key from pregen 🐞 Bug ≡ Correctness

ImportHandler generates the citation key before inserting the entry and then skips generateKeys(),
but CitationKeyGenerator.generateKey() can suppress suffixing when the entry already has the same
key as the generated key. This can insert entries with citation keys that already exist in the
database (no suffix added), creating duplicate citation keys after import.
Agent Prompt
### Issue description
`generateKeyString(entry)` calls `CitationKeyGenerator.generateKey(entry)` on an entry that may already have a citation key. `CitationKeyGenerator` uses that existing key (`oldKey`) to suppress suffixing, which is correct for *updating* existing entries but incorrect for *pre-insert* generation; combined with skipping `generateKeys(...)`, it can leave duplicate keys in the DB.

### Issue Context
Key generation is now done before insertion (to avoid overwriting the "From import" column) and post-insert key generation is skipped. That makes the pre-insert generated key authoritative and it must be collision-safe.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[284-316]
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[432-446]

### Suggested fix
In `generateKeyString`, generate based on a clone with the citation key cleared so `oldKey` is `null`:
- `BibEntry tmp = new BibEntry(entry); tmp.clearCiteKey(); return Optional.of(keyGenerator.generateKey(tmp));`

Optionally, add a safety net: only pass `skipKeyGeneration=true` if `finalEntry` already has a citation key (or the user explicitly chose one), otherwise allow the normal `generateKeys(...)` path.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

tracker.markImported(finalEntry);
}).executeWith(taskExecutor);
Expand All @@ -308,12 +329,28 @@ public Optional<BibEntry> findDuplicate(BibEntry entryToCheck) {
}

public Optional<BibEntry> handleDuplicates(BibEntry originalEntry, BibEntry duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult decision) {
DuplicateDecisionResult decisionResult = getDuplicateDecision(originalEntry, duplicateEntry, decision);
return handleDuplicates(originalEntry, duplicateEntry, decision, Optional.empty());
}

public DuplicateDecisionResult getDuplicateDecision(BibEntry originalEntry, BibEntry duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult decision) {
return getDuplicateDecision(originalEntry, duplicateEntry, decision, Optional.empty());
}

public Optional<BibEntry> handleDuplicates(BibEntry originalEntry, BibEntry duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult decision, Optional<String> generatedKey) {
DuplicateDecisionResult decisionResult = getDuplicateDecision(originalEntry, duplicateEntry, decision, generatedKey);
switch (decisionResult.decision()) {
case KEEP_RIGHT:
targetBibDatabaseContext.getDatabase().removeEntry(duplicateEntry);
if (originalEntry.getCitationKey().isEmpty()) {
generatedKey.ifPresent(key -> originalEntry.setCitationKey(key));
}
break;
case KEEP_BOTH:
Optional<String> originalKey = originalEntry.getCitationKey();
Optional<String> duplicateKey = duplicateEntry.getCitationKey();
if (originalKey.isEmpty() || originalKey.equals(duplicateKey)) {
generatedKey.ifPresent(key -> originalEntry.setCitationKey(key));
}
break;
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
case KEEP_MERGE:
targetBibDatabaseContext.getDatabase().removeEntry(duplicateEntry);
Expand All @@ -327,8 +364,9 @@ public Optional<BibEntry> handleDuplicates(BibEntry originalEntry, BibEntry dupl
return Optional.of(originalEntry);
}

public DuplicateDecisionResult getDuplicateDecision(BibEntry originalEntry, BibEntry duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult decision) {
public DuplicateDecisionResult getDuplicateDecision(BibEntry originalEntry, BibEntry duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult decision, Optional<String> generatedKey) {
DuplicateResolverDialog dialog = new DuplicateResolverDialog(duplicateEntry, originalEntry, DuplicateResolverDialog.DuplicateResolverType.IMPORT_CHECK, stateManager, dialogService, preferences);
generatedKey.ifPresent(key -> dialog.setMergedFieldValue(InternalField.KEY_FIELD, key));
if (decision == BREAK) {
Comment on lines +367 to +370
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Merged key not displayed 🐞 Bug ✓ Correctness

The generated citation key is injected into the merge dialog via an internal field row that is
filtered out of the three-way merge view, so the “Merged entry” column will not show the suggested
key and the user cannot edit it there.
Agent Prompt
### Issue description
The merge dialog key suggestion is injected via `InternalField.KEY_FIELD`, but `ThreeWayMergeViewModel` removes internal fields from `visibleFields`, so the citation key row is never rendered. As a result, `setMergedFieldValue(InternalField.KEY_FIELD, key)` does nothing and the suggested/generated key is not shown in the “Merged entry” column.

### Issue Context
- `ImportHandler.getDuplicateDecision(..., generatedKey)` calls `dialog.setMergedFieldValue(InternalField.KEY_FIELD, key)`.
- `ThreeWayMergeViewModel.setVisibleFields(...)` removes internal fields (`FieldFactory::isInternalField`).
- Citation key is stored as `InternalField.KEY_FIELD` inside `BibEntry`.

### Fix Focus Areas
- Ensure the citation key field is visible in the three-way merge dialog for IMPORT_CHECK.
  - Options:
    - Special-case `InternalField.KEY_FIELD` to NOT be removed from visible fields in `ThreeWayMergeViewModel`.
    - Or explicitly add `InternalField.KEY_FIELD` to `visibleFields` when constructing the model/view for merge dialogs.
    - Or implement a dedicated UI row for citation key outside the internal field filtering.
- Add/adjust tests to cover the case where the merge dialog is populated with a generated key and that the merged entry returns the edited key.

- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[388-397]
- jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeViewModel.java[102-112]
- jabgui/src/main/java/org/jabref/gui/mergeentries/threewaymerge/ThreeWayMergeView.java[207-214]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

decision = dialogService.showCustomDialogAndWait(dialog).orElse(BREAK);
}
Expand Down Expand Up @@ -391,6 +429,22 @@ private void generateKeys(List<BibEntry> entries) {
entries.forEach(keyGenerator::generateAndSetKey);
}

/// Generates a citation key string for the given entry WITHOUT modifying it.
///
/// @param entry the entry to generate a key for
/// @return the generated key, or empty if key generation is disabled
private Optional<String> generateKeyString(BibEntry entry) {
if (!preferences.getImporterPreferences().shouldGenerateNewKeyOnImport()) {
return Optional.empty();
}
CitationKeyGenerator keyGenerator = new CitationKeyGenerator(
targetBibDatabaseContext.getMetaData().getCiteKeyPatterns(preferences.getCitationKeyPatternPreferences()
.getKeyPatterns()),
targetBibDatabaseContext.getDatabase(),
preferences.getCitationKeyPatternPreferences());
return Optional.of(keyGenerator.generateKey(entry));
}

public @NonNull List<@NonNull BibEntry> handleBibTeXData(@NonNull String entries) {
if (!entries.contains("@")) {
LOGGER.debug("Seems not to be BibTeX data: {}", entries);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,15 @@ public BibEntry getRightEntry() {
return viewModel.getRightEntry();
}

public void setMergedFieldValue(Field field, String value) {
for (FieldRowView row : fieldRows) {
if (row.viewModel.getField().equals(field)) {
row.viewModel.setMergedFieldValue(value);
break;
}
}
}

public void saveConfiguration() {
toolbar.saveToolbarConfiguration();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.jabref.gui.externalfiles;

import java.util.List;
import java.util.Optional;

import javax.swing.undo.UndoManager;

Expand All @@ -11,16 +12,23 @@
import org.jabref.gui.duplicationFinder.DuplicateResolverDialog;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.logic.FilePreferences;
import org.jabref.logic.LibraryPreferences;
import org.jabref.logic.bibtex.FieldPreferences;
import org.jabref.logic.citationkeypattern.CitationKeyPatternPreferences;
import org.jabref.logic.citationkeypattern.GlobalCitationKeyPatterns;
import org.jabref.logic.database.DuplicateCheck;
import org.jabref.logic.importer.ImportFormatPreferences;
import org.jabref.logic.importer.ImporterPreferences;
import org.jabref.logic.preferences.OwnerPreferences;
import org.jabref.logic.preferences.TimestampPreferences;
import org.jabref.logic.util.CurrentThreadTaskExecutor;
import org.jabref.model.database.BibDatabase;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.database.BibDatabaseMode;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.field.StandardField;
import org.jabref.model.entry.types.StandardEntryType;
import org.jabref.model.metadata.MetaData;
import org.jabref.model.util.DummyFileUpdateMonitor;

import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -142,7 +150,7 @@ void handleDuplicatesKeepRightTest() {
mock(DialogService.class),
new CurrentThreadTaskExecutor()));
// Mock the behavior of getDuplicateDecision to return KEEP_RIGHT
Mockito.doReturn(decisionResult).when(importHandler).getDuplicateDecision(testEntry, duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult.BREAK);
Mockito.doReturn(decisionResult).when(importHandler).getDuplicateDecision(testEntry, duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult.BREAK, Optional.empty());

// Act
BibEntry result = importHandler.handleDuplicates(testEntry, duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult.BREAK).get();
Expand Down Expand Up @@ -173,7 +181,7 @@ void handleDuplicatesKeepBothTest() {
mock(DialogService.class),
new CurrentThreadTaskExecutor()));
// Mock the behavior of getDuplicateDecision to return KEEP_BOTH
Mockito.doReturn(decisionResult).when(importHandler).getDuplicateDecision(testEntry, duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult.BREAK);
Mockito.doReturn(decisionResult).when(importHandler).getDuplicateDecision(testEntry, duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult.BREAK, Optional.empty());

// Act
BibEntry result = importHandler.handleDuplicates(testEntry, duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult.BREAK).get();
Expand Down Expand Up @@ -207,7 +215,7 @@ void handleDuplicatesKeepMergeTest() {
mock(DialogService.class),
new CurrentThreadTaskExecutor()));
// Mock the behavior of getDuplicateDecision to return KEEP_MERGE
Mockito.doReturn(decisionResult).when(importHandler).getDuplicateDecision(testEntry, duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult.BREAK);
Mockito.doReturn(decisionResult).when(importHandler).getDuplicateDecision(testEntry, duplicateEntry, DuplicateResolverDialog.DuplicateResolverResult.BREAK, Optional.empty());

// Act
// create and return a default BibEntry or do other computations
Expand All @@ -218,4 +226,125 @@ void handleDuplicatesKeepMergeTest() {
assertFalse(bibDatabase.getEntries().contains(duplicateEntry)); // Assert that the duplicate entry was removed from the database
assertEquals(mergedEntry, result); // Assert that the merged entry is returned
}

@Test
void handleDuplicatesKeepBothWithCollidingKeysAppliesGeneratedKey() {
// Arrange
BibEntry duplicateEntry = new BibEntry(StandardEntryType.Article)
.withCitationKey("SameKey2023")
.withField(StandardField.AUTHOR, "Duplicate Author");

BibEntry originalEntry = new BibEntry(StandardEntryType.Article)
.withCitationKey("SameKey2023")
.withField(StandardField.AUTHOR, "Original Author");

BibDatabase bibDatabase = bibDatabaseContext.getDatabase();
bibDatabase.insertEntry(duplicateEntry);

DuplicateDecisionResult decisionResult = new DuplicateDecisionResult(
DuplicateResolverDialog.DuplicateResolverResult.KEEP_BOTH, null);
Comment on lines +244 to +245
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. duplicatedecisionresult constructed with null 📘 Rule violation ≡ Correctness

The new test passes null for DuplicateDecisionResult's mergedEntry, which violates the
no-null-passing rule and can lead to NPEs if the value is accessed. Use a real BibEntry instance
(or refactor the type to model absence explicitly) instead of passing null.
Agent Prompt
## Issue description
A new test constructs `DuplicateDecisionResult` with a `null` `mergedEntry`, violating the project rule to not pass null unless explicitly intended and risking NPEs.

## Issue Context
`DuplicateDecisionResult` is a `record` with a `BibEntry mergedEntry` component (no `@Nullable`), so callers should not pass `null`.

## Fix Focus Areas
- jabgui/src/test/java/org/jabref/gui/externalfiles/ImportHandlerTest.java[244-245]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

importHandler = Mockito.spy(new ImportHandler(
bibDatabaseContext,
preferences,
new DummyFileUpdateMonitor(),
mock(UndoManager.class),
mock(StateManager.class),
mock(DialogService.class),
new CurrentThreadTaskExecutor()));

String generatedKey = "UniqueKey2023";
Mockito.doReturn(decisionResult).when(importHandler)
.getDuplicateDecision(originalEntry, duplicateEntry,
DuplicateResolverDialog.DuplicateResolverResult.BREAK,
Optional.of(generatedKey));

// Act
BibEntry result = importHandler.handleDuplicates(originalEntry, duplicateEntry,
DuplicateResolverDialog.DuplicateResolverResult.BREAK,
Optional.of(generatedKey)).get();

// Assert: the colliding key should be replaced by the generated one
assertEquals(generatedKey, result.getCitationKey().orElse(""),
"When keeping both entries with the same key, the imported entry should get the generated key");
assertEquals("SameKey2023", duplicateEntry.getCitationKey().orElse(""),
"The existing entry should keep its original key");
}

@Test
void importWithDuplicateCheckPreservesMergedCitationKey() {
// Arrange: set up key generation with pattern [auth][year]
ImporterPreferences importerPreferences = mock(ImporterPreferences.class);
when(importerPreferences.shouldGenerateNewKeyOnImport()).thenReturn(true);
when(preferences.getImporterPreferences()).thenReturn(importerPreferences);

GlobalCitationKeyPatterns patterns = GlobalCitationKeyPatterns.fromPattern("[auth][year]");
CitationKeyPatternPreferences citationKeyPatternPreferences = mock(CitationKeyPatternPreferences.class);
when(citationKeyPatternPreferences.getKeyPatterns()).thenReturn(patterns);
when(citationKeyPatternPreferences.getUnwantedCharacters()).thenReturn("");
when(citationKeyPatternPreferences.getKeywordDelimiter()).thenReturn(',');
when(citationKeyPatternPreferences.getKeySuffix()).thenReturn(CitationKeyPatternPreferences.KeySuffix.SECOND_WITH_B);
when(preferences.getCitationKeyPatternPreferences()).thenReturn(citationKeyPatternPreferences);

MetaData metaData = mock(MetaData.class);
when(metaData.getCiteKeyPatterns(any())).thenReturn(patterns);
when(bibDatabaseContext.getMetaData()).thenReturn(metaData);

StateManager stateManager = mock(StateManager.class);
when(stateManager.getSelectedGroups(any())).thenReturn(FXCollections.observableArrayList());

LibraryPreferences libraryPreferences = mock(LibraryPreferences.class);
when(libraryPreferences.isAddImportedEntriesEnabled()).thenReturn(false);
when(preferences.getLibraryPreferences()).thenReturn(libraryPreferences);

when(preferences.getOwnerPreferences()).thenReturn(mock(OwnerPreferences.class));
when(preferences.getTimestampPreferences()).thenReturn(mock(TimestampPreferences.class));

FilePreferences filePreferences = mock(FilePreferences.class);
when(filePreferences.shouldDownloadLinkedFiles()).thenReturn(false);
when(preferences.getFilePreferences()).thenReturn(filePreferences);

importHandler = Mockito.spy(new ImportHandler(
bibDatabaseContext,
preferences,
new DummyFileUpdateMonitor(),
mock(UndoManager.class),
stateManager,
mock(DialogService.class),
new CurrentThreadTaskExecutor()));

// Existing entry in the database
BibEntry existingEntry = new BibEntry(StandardEntryType.Article)
.withCitationKey("ExistingKey")
.withField(StandardField.AUTHOR, "Smith")
.withField(StandardField.YEAR, "2022")
.withField(StandardField.TITLE, "Some Title");
BibDatabase bibDatabase = bibDatabaseContext.getDatabase();
bibDatabase.insertEntry(existingEntry);

// User-chosen merged entry with a custom citation key
String userChosenKey = "MyCustomKey";
BibEntry mergedEntry = new BibEntry(StandardEntryType.Article)
.withCitationKey(userChosenKey)
.withField(StandardField.AUTHOR, "Smith")
.withField(StandardField.YEAR, "2022")
.withField(StandardField.TITLE, "Some Title");

// Entry to import
BibEntry importEntry = new BibEntry(StandardEntryType.Article)
.withField(StandardField.AUTHOR, "Smith")
.withField(StandardField.YEAR, "2022")
.withField(StandardField.TITLE, "Some Title");
// Mock findDuplicate to return the existing entry as a duplicate
Mockito.doReturn(Optional.of(existingEntry)).when(importHandler).findDuplicate(importEntry);

// Mock handleDuplicates(4 params) to simulate user choosing KEEP_MERGE with their custom key
Mockito.doReturn(Optional.of(mergedEntry)).when(importHandler).handleDuplicates(importEntry, existingEntry, DuplicateResolverDialog.DuplicateResolverResult.BREAK, Optional.of("Smith2022"));

// Act
importHandler.importEntriesWithDuplicateCheck(null, List.of(importEntry));

// Assert: the user's chosen citation key from the merge dialog must be preserved
assertEquals(userChosenKey, mergedEntry.getCitationKey().orElse(""),
"The citation key chosen by the user in the merge dialog should not be overwritten by key generation");
}
}
Loading