Repository: OpenRefine/OpenRefine (java)

Issue:
Data parsing bug when the first column has nulls
When opening a file where the first column ends with several rows of null values, OpenRefine 3.8-beta1 [7fd4077] doesn't properly parse the data and ends up grouping the rows starting with null values as if it was a single row. The behavior stops facets from working properly and also hallucinates quotes around some values. 


<!-- Describe the bug - Please add a clear and concise description of the bug above this line. You can delete this line if you want. It will be hidden in the final bug report -->

### To Reproduce
Steps to reproduce the behavior:
1. First, import the dataset from URL (I did not try downloading before importing): the problem https://raw.githubusercontent.com/civicliteracies/EITI_SDT_data_verification_and_validation/main/data/outputs/agencies_4_actual_complete.csv
3. Then create a facet by country then one by Government entity. You can either click exactly as the picture above (AFGHANIISTAN, AGENCE DE L'EAU) or try with any other government entity facet option that is found at the end of the dataset (they generally have only one ocurrence)
4. Finally, compare the outcome by loading from URL the comparison file for which an index was added: https://raw.githubusercontent.com/civicliteracies/EITI_SDT_data_verification_and_validation/main/data/outputs/agencies_4_actual_complete_indexed.csv



### Current Results

The following picture shows a combination of facets which should show only one row, but end up showing a lot of rows bunched together.

<img width="1426" alt="Capture d’écran 2024-04-08 à 13 38 39" src="https://github.com/OpenRefine/OpenRefine/assets/8268300/1774aa03-4ae0-42bb-9ead-36fd1251165a">

We worked around the bug by adding an index to the dataset, so that the first column wouldn't have any nulls. We discovered in that process that not only rows are bunched together, but OpenRefine hallucinates double quotes around some values, as illustrated in the below picture. 

![comparison open refine bug](https://github.com/OpenRefine/OpenRefine/assets/8268300/95589611-7a26-4f52-8195-c5f0cc34f792)

<!-- What results occurred or were shown. -->

### Expected Behavior

OpenRefine should properly facet the file even if the first column has nulls.

<!-- A clear and concise description of what you expected to happen or to show. -->

### Screenshots

See above
<!-- If applicable, add screenshots to help explain your problem. -->

### Versions<!--  (please complete the following information)-->
 - Operating System: MacOS Ventura 13.2
 - Browser Version: Firefox 124.0.2
 - JRE or JDK Version: openjdk version "17.0.9" 2023-10-17
 - OpenRefine: 3.8-beta1 [7fd4077] 

### Datasets
* problem dataset: https://raw.githubusercontent.com/civicliteracies/EITI_SDT_data_verification_and_validation/main/data/outputs/agencies_4_actual_complete.csv
* comparison dataset that solves the issue by adding an index https://raw.githubusercontent.com/civicliteracies/EITI_SDT_data_verification_and_validation/main/data/outputs/agencies_4_actual_complete_indexed.csv

### Additional context
Bug found today.
<!-- Add any other context about the problem here. -->


Pull request description:
Try harder to return separator guess. Fixes #6516
Fixes #6516

* In cases where the Univocity guesser and our guesser disagree, return our separator guess rather than just punting.
* Also remove magic numbers and simplify.



Patch:
diff --git a/.github/workflows/snapshot_release.yml b/.github/workflows/snapshot_release.yml
index 21ce82ff3..cdd5fbf84 100644
--- a/.github/workflows/snapshot_release.yml
+++ b/.github/workflows/snapshot_release.yml
@@ -24,7 +24,7 @@ jobs:
       - name: Setup Node
         uses: actions/setup-node@v4
         with:
-          node-version: '16'
+          node-version: '20'
       - id: set-matrix
         run: npm install --save glob@8.1.0 && node main/tests/cypress/build-test-matrix.js >> $GITHUB_OUTPUT
         env:
@@ -60,7 +60,7 @@ jobs:
       - name: Setup Node
         uses: actions/setup-node@v4
         with:
-          node-version: '16'
+          node-version: '20'
 
       - name: Restore Tests dependency cache
         uses: actions/cache@v4
diff --git a/extensions/phonetic/.gitignore b/extensions/phonetic/.gitignore
deleted file mode 100644
index 17b5c9465..000000000
--- a/extensions/phonetic/.gitignore
+++ /dev/null
@@ -1,1 +0,0 @@
-module/MOD-INF/classes/
diff --git a/extensions/phonetic/module/MOD-INF/controller.js b/extensions/phonetic/module/MOD-INF/controller.js
deleted file mode 100644
index 6d652f685..000000000
--- a/extensions/phonetic/module/MOD-INF/controller.js
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
-
-Copyright 2010, Google Inc.
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
- * Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above
-copyright notice, this list of conditions and the following disclaimer
-in the documentation and/or other materials provided with the
-distribution.
- * Neither the name of Google Inc. nor the names of its
-contributors may be used to endorse or promote products derived from
-this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,           
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY           
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
- */
-
-var html = "text/html";
-var encoding = "UTF-8";
-var ClientSideResourceManager = Packages.com.google.refine.ClientSideResourceManager;
-
-/*
- * Function invoked to initialize the extension.
- */
-function init() {
-  // Register new keyers
-  Packages.com.google.refine.clustering.binning.KeyerFactory.put("daitch-mokotoff", new Packages.org.openrefine.phonetic.keyers.DaitchMokotoffKeyer());
-  Packages.com.google.refine.clustering.binning.KeyerFactory.put("beider-morse", new Packages.org.openrefine.phonetic.keyers.BeiderMorseKeyer());
-
-  // Similarly, we could register new distances like this:
-  // Packages.com.google.refine.clustering.knn.DistanceFactory.put("my-distance", new Packages.org.openrefine.mydistances.MyDistance());
-
-    /*
-     * Resources: just some javascript to load the localized strings
-     */
-    ClientSideResourceManager.addPaths(
-      "project/scripts",
-      module,
-      [
-        "scripts/load-language.js",
-      ]
-    );
-
-}
diff --git a/extensions/phonetic/module/MOD-INF/module.properties b/extensions/phonetic/module/MOD-INF/module.properties
deleted file mode 100644
index e207201d3..000000000
--- a/extensions/phonetic/module/MOD-INF/module.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-name = phonetic
-description = OpenRefine Phonetic Clustering extension
-templating.macros = macros.vm
-requires = core
diff --git a/extensions/phonetic/module/langs/translation-en.json b/extensions/phonetic/module/langs/translation-en.json
deleted file mode 100644
index 5cd9e72e6..000000000
--- a/extensions/phonetic/module/langs/translation-en.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-    "clustering-keyers/beider-morse": "Beider-Morse",
-    "clustering-keyers/daitch-mokotoff": "Daitch-Mokotoff"
-}
diff --git a/extensions/phonetic/module/macros.vm b/extensions/phonetic/module/macros.vm
deleted file mode 100644
index 18a83bfce..000000000
--- a/extensions/phonetic/module/macros.vm
+++ /dev/null
@@ -1,14 +0,0 @@
-#*
-  This file contains common velocity macros used in all .vt files.
-  For Velocity documentation, see:
-  
-  http://velocity.apache.org/engine/releases/velocity-1.5/user-guide.html
-*#
-
-#macro( makeAList $list )
-  <ul>
-    #foreach($item in $list)
-      <li>$item</li>
-    #end
-  </ul>
-#end
\ No newline at end of file
diff --git a/extensions/phonetic/module/scripts/load-language.js b/extensions/phonetic/module/scripts/load-language.js
deleted file mode 100644
index 2a05202b4..000000000
--- a/extensions/phonetic/module/scripts/load-language.js
+++ /dev/null
@@ -1,1 +0,0 @@
-I18NUtil.init("phonetic");
diff --git a/extensions/phonetic/pom.xml b/extensions/phonetic/pom.xml
deleted file mode 100644
index 438072841..000000000
--- a/extensions/phonetic/pom.xml
+++ /dev/null
@@ -1,92 +0,0 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <groupId>org.openrefine</groupId>
-  <artifactId>phonetic</artifactId>
-  <packaging>jar</packaging>
-  <version>3.9-SNAPSHOT</version>
-
-  <name>OpenRefine - Phonetic clustering extension</name>
-  <description>Adds a few advanced phonetic clustering methods</description>
-  <url>http://openrefine.org/</url>
-  <parent>
-    <groupId>org.openrefine</groupId>
-    <artifactId>extensions</artifactId>
-    <version>3.9-SNAPSHOT</version>
-  </parent>
-
-  <build>
-    <finalName>openrefine-sample</finalName>
-    <resources>
-      <resource>
-        <directory>src</directory>
-      </resource>
-    </resources>
-    <sourceDirectory>src</sourceDirectory>
-    <testSourceDirectory>tests/src</testSourceDirectory>
-    <outputDirectory>module/MOD-INF/classes</outputDirectory>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-surefire-plugin</artifactId>
-        <version>${surefire.version}</version>
-        <configuration>
-          <suiteXmlFiles>
-            <suiteXmlFile>tests/conf/tests.xml</suiteXmlFile>
-          </suiteXmlFiles>
-        </configuration>
-      </plugin>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-dependency-plugin</artifactId>
-	<version>${maven-dependency-plugin.version}</version>
-        <executions>
-            <execution>
-            <phase>compile</phase>
-            <goals>
-                <goal>copy-dependencies</goal>
-            </goals>
-            <configuration>
-                <outputDirectory>module/MOD-INF/lib</outputDirectory>
-                <includeScope>runtime</includeScope>
-            </configuration>
-            </execution>
-        </executions>
-      </plugin>
-    </plugins>
-  </build>
-
-  <dependencies>
-     <dependency>
-      <groupId>${project.groupId}</groupId>
-      <artifactId>main</artifactId>
-      <version>${project.version}</version>
-      <scope>provided</scope>
-    </dependency>
-    <dependency>
-      <groupId>javax.servlet</groupId>
-      <artifactId>javax.servlet-api</artifactId>
-      <version>${servlet-api.version}</version>
-      <scope>provided</scope>
-    </dependency>
-
-   <!-- test dependencies -->
-
-    <dependency>
-      <groupId>org.testng</groupId>
-      <artifactId>testng</artifactId>
-      <version>${testng.version}</version>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.logging.log4j</groupId>
-      <artifactId>log4j-slf4j-impl</artifactId>
-      <version>${log4j.version}</version>
-      <scope>test</scope>
-    </dependency>
-
-  </dependencies>
-
-</project>
-
diff --git a/extensions/pom.xml b/extensions/pom.xml
index 4f5cf1566..592a15135 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -23,7 +23,6 @@
         <module>database</module>
         <module>gdata</module>
         <module>pc-axis</module>
-        <module>phonetic</module>
         <!-- Add new extensions here -->
   </modules>
 
diff --git a/extensions/phonetic/src/org/openrefine/phonetic/keyers/BeiderMorseKeyer.java b/main/src/com/google/refine/clustering/binning/BeiderMorseKeyer.java
similarity index 91%
rename from extensions/phonetic/src/org/openrefine/phonetic/keyers/BeiderMorseKeyer.java
rename to main/src/com/google/refine/clustering/binning/BeiderMorseKeyer.java
index af2759231..eb0ea843a 100644
--- a/extensions/phonetic/src/org/openrefine/phonetic/keyers/BeiderMorseKeyer.java
+++ b/main/src/com/google/refine/clustering/binning/BeiderMorseKeyer.java
@@ -1,11 +1,9 @@
 
-package org.openrefine.phonetic.keyers;
+package com.google.refine.clustering.binning;
 
 import org.apache.commons.codec.EncoderException;
 import org.apache.commons.codec.language.bm.BeiderMorseEncoder;
 
-import com.google.refine.clustering.binning.Keyer;
-
 public class BeiderMorseKeyer extends Keyer {
 
     protected BeiderMorseEncoder encoder = new BeiderMorseEncoder();
diff --git a/extensions/phonetic/src/org/openrefine/phonetic/keyers/DaitchMokotoffKeyer.java b/main/src/com/google/refine/clustering/binning/DaitchMokotoffKeyer.java
similarity index 77%
rename from extensions/phonetic/src/org/openrefine/phonetic/keyers/DaitchMokotoffKeyer.java
rename to main/src/com/google/refine/clustering/binning/DaitchMokotoffKeyer.java
index 8d7b1879e..96ba20b6c 100644
--- a/extensions/phonetic/src/org/openrefine/phonetic/keyers/DaitchMokotoffKeyer.java
+++ b/main/src/com/google/refine/clustering/binning/DaitchMokotoffKeyer.java
@@ -1,10 +1,8 @@
 
-package org.openrefine.phonetic.keyers;
+package com.google.refine.clustering.binning;
 
 import org.apache.commons.codec.language.DaitchMokotoffSoundex;
 
-import com.google.refine.clustering.binning.Keyer;
-
 public class DaitchMokotoffKeyer extends Keyer {
 
     protected DaitchMokotoffSoundex encoder = new DaitchMokotoffSoundex();
diff --git a/main/src/com/google/refine/exporters/ExporterRegistry.java b/main/src/com/google/refine/exporters/ExporterRegistry.java
index 00c4e3092..10c777aa2 100644
--- a/main/src/com/google/refine/exporters/ExporterRegistry.java
+++ b/main/src/com/google/refine/exporters/ExporterRegistry.java
@@ -36,29 +36,10 @@ package com.google.refine.exporters;
 import java.util.HashMap;
 import java.util.Map;
 
-import com.google.refine.exporters.sql.SqlExporter;
-
-abstract public class ExporterRegistry {
+public class ExporterRegistry {
 
     static final private Map<String, Exporter> s_formatToExporter = new HashMap<String, Exporter>();
 
-    static {
-        s_formatToExporter.put("csv", new CsvExporter());
-        s_formatToExporter.put("tsv", new CsvExporter('\t'));
-        s_formatToExporter.put("*sv", new CsvExporter());
-
-        s_formatToExporter.put("xls", new XlsExporter(false));
-        s_formatToExporter.put("xlsx", new XlsExporter(true));
-
-        s_formatToExporter.put("ods", new OdsExporter());
-
-        s_formatToExporter.put("html", new HtmlTableExporter());
-
-        s_formatToExporter.put("template", new TemplatingExporter());
-
-        s_formatToExporter.put("sql", new SqlExporter());
-    }
-
     static public void registerExporter(String format, Exporter exporter) {
         s_formatToExporter.put(format.toLowerCase(), exporter);
     }
diff --git a/main/src/com/google/refine/importers/SeparatorBasedImporter.java b/main/src/com/google/refine/importers/SeparatorBasedImporter.java
index e3ccd2acb..b2f6596f6 100644
--- a/main/src/com/google/refine/importers/SeparatorBasedImporter.java
+++ b/main/src/com/google/refine/importers/SeparatorBasedImporter.java
@@ -70,6 +70,7 @@ import com.google.refine.util.JSONUtilities;
 
 public class SeparatorBasedImporter extends TabularImportingParserBase {
 
+    public static final int GUESSER_LINE_COUNT = 100;
     char DEFAULT_QUOTE_CHAR = new CsvParserSettings().getFormat().getQuote();
 
     public SeparatorBasedImporter() {
@@ -207,13 +208,12 @@ public class SeparatorBasedImporter extends TabularImportingParserBase {
                 CsvFormat format = guessFormat(file, encoding);
                 if (format != null) {
                     if (separator != null) {
-                        if (format.getDelimiter() == separator.separator) {
-                            // They both agree - yay!
-                            return StringEscapeUtils.escapeJava(Character.toString(separator.separator));
-                        } else {
-                            logger.warn("Delimiter guesses disagree - uniVocity: {} - internal: {}", format.getDelimiter(),
+                        if (format.getDelimiter() != separator.separator) {
+                            logger.warn("Delimiter guesses disagree - uniVocity: '{}' - internal: '{}'", format.getDelimiter(),
                                     separator.separator);
                         }
+                        // Even if they disagree, use our guess for backward compatibility
+                        return StringEscapeUtils.escapeJava(Character.toString(separator.separator));
                     } else {
                         // We got a guess from CsvParser, but not ours, so let's use that
                         return StringEscapeUtils.escapeJava(format.getDelimiterString());
@@ -224,7 +224,6 @@ public class SeparatorBasedImporter extends TabularImportingParserBase {
                         return StringEscapeUtils.escapeJava(Character.toString(separator.separator));
                     }
                 }
-
             }
         }
         return null;
@@ -246,15 +245,12 @@ public class SeparatorBasedImporter extends TabularImportingParserBase {
                 Reader reader = encoding != null ? new InputStreamReader(is, encoding) : new InputStreamReader(is);
                 LineNumberReader lineNumberReader = new LineNumberReader(reader)) {
             CsvParserSettings settings = new CsvParserSettings();
+            // We could provide a set of delimiters to consider below if we wanted to restrict this
             settings.detectFormatAutomatically();
-            settings.setFormatDetectorRowSampleCount(100);
+            settings.setFormatDetectorRowSampleCount(GUESSER_LINE_COUNT); // default is 20, but let's match our guesser
             CsvParser parser = new CsvParser(settings);
             parser.beginParsing(lineNumberReader);
-            for (int i = 0; i < 20; i++) {
-                if (parser.parseNext() == null) {
-                    break;
-                }
-            }
+            // Format guesser result is available as soon as beginParsing() completes
             return parser.getDetectedFormat();
         } catch (IOException e) {
             e.printStackTrace();
@@ -280,7 +276,7 @@ public class SeparatorBasedImporter extends TabularImportingParserBase {
                 boolean inQuote = false;
                 String s;
                 while (totalChars < 64 * 1024 &&
-                        lineCount < 100 &&
+                        lineCount < GUESSER_LINE_COUNT &&
                         (s = lineNumberReader.readLine()) != null) {
 
                     totalChars += s.length() + 1; // count the new line character
diff --git a/main/webapp/modules/core/MOD-INF/controller.js b/main/webapp/modules/core/MOD-INF/controller.js
index 2bf658289..344b1dfa4 100644
--- a/main/webapp/modules/core/MOD-INF/controller.js
+++ b/main/webapp/modules/core/MOD-INF/controller.js
@@ -319,6 +319,19 @@ function registerImporting() {
   );
 }
 
+function registerExporters() {
+   var ER = Packages.com.google.refine.exporters.ExporterRegistry;
+   ER.registerExporter("csv", new Packages.com.google.refine.exporters.CsvExporter());
+   ER.registerExporter("tsv", new Packages.com.google.refine.exporters.CsvExporter('\t'.charAt(0)));
+   ER.registerExporter("*sv", new Packages.com.google.refine.exporters.CsvExporter());
+   ER.registerExporter("xls", new Packages.com.google.refine.exporters.XlsExporter(false));
+   ER.registerExporter("xlsx", new Packages.com.google.refine.exporters.XlsExporter(true));
+   ER.registerExporter("ods", new Packages.com.google.refine.exporters.OdsExporter());
+   ER.registerExporter("html", new Packages.com.google.refine.exporters.HtmlTableExporter());
+   ER.registerExporter("template", new Packages.com.google.refine.exporters.TemplatingExporter());
+   ER.registerExporter("sql", new Packages.com.google.refine.exporters.sql.SqlExporter());
+}
+
 function registerDistances() {
    var DF = Packages.com.google.refine.clustering.knn.DistanceFactory;
    var VicinoDistance = Packages.com.google.refine.clustering.knn.VicinoDistance;
@@ -332,6 +345,8 @@ function registerKeyers() {
    KF.put("ngram-fingerprint", new Packages.com.google.refine.clustering.binning.NGramFingerprintKeyer());
    KF.put("metaphone3", new Packages.com.google.refine.clustering.binning.Metaphone3Keyer());
    KF.put("cologne-phonetic", new Packages.com.google.refine.clustering.binning.ColognePhoneticKeyer());
+   KF.put("daitch-mokotoff", new Packages.com.google.refine.clustering.binning.DaitchMokotoffKeyer());
+   KF.put("beider-morse", new Packages.com.google.refine.clustering.binning.BeiderMorseKeyer());
 }
 
 /*
@@ -343,6 +358,7 @@ function init() {
   registerCommands();
   registerOperations();
   registerImporting();
+  registerExporters();
   registerDistances();
   registerKeyers();
 
diff --git a/main/webapp/modules/core/langs/translation-en.json b/main/webapp/modules/core/langs/translation-en.json
index fec7180b5..c636782c8 100644
--- a/main/webapp/modules/core/langs/translation-en.json
+++ b/main/webapp/modules/core/langs/translation-en.json
@@ -218,6 +218,8 @@
     "clustering-keyers/ngram-fingerprint": "n-Gram fingerprint",
     "clustering-keyers/metaphone3": "Metaphone3",
     "clustering-keyers/cologne-phonetic": "Cologne phonetic",
+    "clustering-keyers/beider-morse": "Beider-Morse",
+    "clustering-keyers/daitch-mokotoff": "Daitch-Mokotoff",
     "core-dialogs/distance-fun": "Distance function",
     "clustering-distances/levenshtein": "Levenshtein",
     "clustering-distances/ppm": "PPM",
diff --git a/main/webapp/modules/core/scripts/views/data-table/cell-ui.js b/main/webapp/modules/core/scripts/views/data-table/cell-ui.js
index 3d1ac1c74..c1a8d56c1 100644
--- a/main/webapp/modules/core/scripts/views/data-table/cell-ui.js
+++ b/main/webapp/modules/core/scripts/views/data-table/cell-ui.js
@@ -85,7 +85,11 @@ DataTableCellUI.prototype._render = function() {
 
   var renderedCell = undefined;
   for (let record of CellRendererRegistry.renderers) {
-    renderedCell = record.renderer.render(this._rowIndex, this._cellIndex, cell, this);
+    try {
+      renderedCell = record.renderer.render(this._rowIndex, this._cellIndex, cell, this);
+    } catch (e) {
+      continue;
+    }
     if (renderedCell) {
       break;
     }
diff --git a/pom.xml b/pom.xml
index 37270df0c..542db8eb2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -104,7 +104,7 @@
     <guava.version>33.1.0-jre</guava.version>
     <jaxb-api.version>2.3.1</jaxb-api.version>
     <juniversalchardet.version>2.4.0</juniversalchardet.version>
-    <testng.version>7.10.0</testng.version>
+    <testng.version>7.10.1</testng.version>
     <httpmime.version>4.5.14</httpmime.version>
     <owasp-encoder.version>1.2.3</owasp-encoder.version>
     <jasypt.version>1.9.3</jasypt.version>


Test patch:
diff --git a/.github/workflows/pull_request_e2e.yml b/.github/workflows/pull_request_e2e.yml
index dca92d736..fb09561a5 100644
--- a/.github/workflows/pull_request_e2e.yml
+++ b/.github/workflows/pull_request_e2e.yml
@@ -26,7 +26,7 @@ jobs:
       - name: Setup Node
         uses: actions/setup-node@v4
         with:
-          node-version: '16'
+          node-version: '20'
       - id: set-matrix
         run: npm install --save glob@8.1.0 && node main/tests/cypress/build-test-matrix.js >> $GITHUB_OUTPUT
         env:
@@ -58,7 +58,7 @@ jobs:
       - name: Setup Node
         uses: actions/setup-node@v4
         with:
-          node-version: '16'
+          node-version: '20'
 
       - name: Restore Tests dependency cache
         uses: actions/cache@v4
diff --git a/extensions/phonetic/tests/conf/tests.xml b/extensions/phonetic/tests/conf/tests.xml
deleted file mode 100644
index 7ad8efc22..000000000
--- a/extensions/phonetic/tests/conf/tests.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >
- 
-<suite name="Wikidata extension">
-  <test name="tests">
-    <groups>
-      <run>
-        <exclude name="broken" />
-      </run>
-    </groups>
-    <packages>
-      <package name="org.openrefine.phonetic.*" />
-    </packages>
-  </test>
-</suite>
diff --git a/main/tests/cypress/package.json b/main/tests/cypress/package.json
index 47a9901ab..800370113 100644
--- a/main/tests/cypress/package.json
+++ b/main/tests/cypress/package.json
@@ -6,7 +6,7 @@
     "author": "OpenRefine",
     "private": true,
     "engines": {
-        "node": ">=16.0.0",
+        "node": ">=20.0.0",
         "npm":  ">=8.11.0",
         "yarn": ">=1.22.15"
     },
@@ -20,7 +20,7 @@
         "cypress-file-upload": "^5.0.8"
     },
     "devDependencies": {
-        "eslint": "^8.57.0",
+        "eslint": "^9.0.0",
         "eslint-config-google": "^0.14.0",
         "eslint-config-prettier": "^9.1.0",
         "eslint-plugin-cypress": "^2.15.1",
diff --git a/main/tests/cypress/yarn.lock b/main/tests/cypress/yarn.lock
index 436ed0265..17be768c5 100644
--- a/main/tests/cypress/yarn.lock
+++ b/main/tests/cypress/yarn.lock
@@ -51,32 +51,32 @@
   resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8"
   integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==
 
-"@eslint/eslintrc@^2.1.4":
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad"
-  integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==
+"@eslint/eslintrc@^3.0.2":
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.0.2.tgz#36180f8e85bf34d2fe3ccc2261e8e204a411ab4e"
+  integrity sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==
   dependencies:
     ajv "^6.12.4"
     debug "^4.3.2"
-    espree "^9.6.0"
-    globals "^13.19.0"
+    espree "^10.0.1"
+    globals "^14.0.0"
     ignore "^5.2.0"
     import-fresh "^3.2.1"
     js-yaml "^4.1.0"
     minimatch "^3.1.2"
     strip-json-comments "^3.1.1"
 
-"@eslint/js@8.57.0":
-  version "8.57.0"
-  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f"
-  integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==
+"@eslint/js@9.0.0":
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.0.0.tgz#1a9e4b4c96d8c7886e0110ed310a0135144a1691"
+  integrity sha512-RThY/MnKrhubF6+s1JflwUjPEsnCEmYCWwqa/aRISKWNXGZ9epUwft4bUMM35SdKF9xvBrLydAM1RDHd1Z//ZQ==
 
-"@humanwhocodes/config-array@^0.11.14":
-  version "0.11.14"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b"
-  integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==
+"@humanwhocodes/config-array@^0.12.3":
+  version "0.12.3"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.12.3.tgz#a6216d90f81a30bedd1d4b5d799b47241f318072"
+  integrity sha512-jsNnTBlMWuTpDkeE3on7+dWJi0D6fdDfeANj/w7MpS8ztROCoLvIO2nG0CcFj+E4k8j4QrSTh4Oryi3i2G669g==
   dependencies:
-    "@humanwhocodes/object-schema" "^2.0.2"
+    "@humanwhocodes/object-schema" "^2.0.3"
     debug "^4.3.1"
     minimatch "^3.0.5"
 
@@ -85,10 +85,10 @@
   resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
   integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
 
-"@humanwhocodes/object-schema@^2.0.2":
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917"
-  integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==
+"@humanwhocodes/object-schema@^2.0.3":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
+  integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
 
 "@nodelib/fs.scandir@2.1.5":
   version "2.1.5"
@@ -138,20 +138,15 @@
   dependencies:
     "@types/node" "*"
 
-"@ungap/structured-clone@^1.2.0":
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
-  integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
-
 acorn-jsx@^5.3.2:
   version "5.3.2"
   resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
   integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
 
-acorn@^8.9.0:
-  version "8.9.0"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.9.0.tgz#78a16e3b2bcc198c10822786fa6679e245db5b59"
-  integrity sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==
+acorn@^8.11.3:
+  version "8.11.3"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
+  integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
 
 aggregate-error@^3.0.0:
   version "3.1.0"
@@ -520,13 +515,6 @@ delayed-stream@~1.0.0:
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz"
   integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
 
-doctrine@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz"
-  integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
-  dependencies:
-    esutils "^2.0.2"
-
 ecc-jsbn@~0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz"
@@ -581,54 +569,55 @@ eslint-plugin-cypress@^2.15.1:
   dependencies:
     globals "^13.20.0"
 
-eslint-scope@^7.2.2:
-  version "7.2.2"
-  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f"
-  integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
+eslint-scope@^8.0.1:
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.0.1.tgz#a9601e4b81a0b9171657c343fb13111688963cfc"
+  integrity sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==
   dependencies:
     esrecurse "^4.3.0"
     estraverse "^5.2.0"
 
-eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3:
+eslint-visitor-keys@^3.3.0:
   version "3.4.3"
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
   integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
 
-eslint@^8.57.0:
-  version "8.57.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668"
-  integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==
+eslint-visitor-keys@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb"
+  integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==
+
+eslint@^9.0.0:
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.0.0.tgz#6270548758e390343f78c8afd030566d86927d40"
+  integrity sha512-IMryZ5SudxzQvuod6rUdIUz29qFItWx281VhtFVc2Psy/ZhlCeD/5DT6lBIJ4H3G+iamGJoTln1v+QSuPw0p7Q==
   dependencies:
     "@eslint-community/eslint-utils" "^4.2.0"
     "@eslint-community/regexpp" "^4.6.1"
-    "@eslint/eslintrc" "^2.1.4"
-    "@eslint/js" "8.57.0"
-    "@humanwhocodes/config-array" "^0.11.14"
+    "@eslint/eslintrc" "^3.0.2"
+    "@eslint/js" "9.0.0"
+    "@humanwhocodes/config-array" "^0.12.3"
     "@humanwhocodes/module-importer" "^1.0.1"
     "@nodelib/fs.walk" "^1.2.8"
-    "@ungap/structured-clone" "^1.2.0"
     ajv "^6.12.4"
     chalk "^4.0.0"
     cross-spawn "^7.0.2"
     debug "^4.3.2"
-    doctrine "^3.0.0"
     escape-string-regexp "^4.0.0"
-    eslint-scope "^7.2.2"
-    eslint-visitor-keys "^3.4.3"
-    espree "^9.6.1"
+    eslint-scope "^8.0.1"
+    eslint-visitor-keys "^4.0.0"
+    espree "^10.0.1"
     esquery "^1.4.2"
     esutils "^2.0.2"
     fast-deep-equal "^3.1.3"
-    file-entry-cache "^6.0.1"
+    file-entry-cache "^8.0.0"
     find-up "^5.0.0"
     glob-parent "^6.0.2"
-    globals "^13.19.0"
     graphemer "^1.4.0"
     ignore "^5.2.0"
     imurmurhash "^0.1.4"
     is-glob "^4.0.0"
     is-path-inside "^3.0.3"
-    js-yaml "^4.1.0"
     json-stable-stringify-without-jsonify "^1.0.1"
     levn "^0.4.1"
     lodash.merge "^4.6.2"
@@ -638,14 +627,14 @@ eslint@^8.57.0:
     strip-ansi "^6.0.1"
     text-table "^0.2.0"
 
-espree@^9.6.0, espree@^9.6.1:
-  version "9.6.1"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
-  integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
+espree@^10.0.1:
+  version "10.0.1"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-10.0.1.tgz#600e60404157412751ba4a6f3a2ee1a42433139f"
+  integrity sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==
   dependencies:
-    acorn "^8.9.0"
+    acorn "^8.11.3"
     acorn-jsx "^5.3.2"
-    eslint-visitor-keys "^3.4.1"
+    eslint-visitor-keys "^4.0.0"
 
 esquery@^1.4.2:
   version "1.4.2"
@@ -760,12 +749,12 @@ figures@^3.2.0:
   dependencies:
     escape-string-regexp "^1.0.5"
 
-file-entry-cache@^6.0.1:
-  version "6.0.1"
-  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
-  integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
+file-entry-cache@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f"
+  integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==
   dependencies:
-    flat-cache "^3.0.4"
+    flat-cache "^4.0.0"
 
 find-up@^5.0.0:
   version "5.0.0"
@@ -775,18 +764,18 @@ find-up@^5.0.0:
     locate-path "^6.0.0"
     path-exists "^4.0.0"
 
-flat-cache@^3.0.4:
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz"
-  integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
+flat-cache@^4.0.0:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c"
+  integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==
   dependencies:
-    flatted "^3.1.0"
-    rimraf "^3.0.2"
+    flatted "^3.2.9"
+    keyv "^4.5.4"
 
-flatted@^3.1.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz"
-  integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==
+flatted@^3.2.9:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a"
+  integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==
 
 forever-agent@~0.6.1:
   version "0.6.1"
@@ -879,13 +868,18 @@ global-dirs@^3.0.0:
   dependencies:
     ini "2.0.0"
 
-globals@^13.19.0, globals@^13.20.0:
+globals@^13.20.0:
   version "13.20.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82"
   integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==
   dependencies:
     type-fest "^0.20.2"
 
+globals@^14.0.0:
+  version "14.0.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
+  integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
+
 graceful-fs@^4.1.6, graceful-fs@^4.2.0:
   version "4.2.6"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
@@ -1047,6 +1041,11 @@ jsbn@~0.1.0:
   resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz"
   integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
 
+json-buffer@3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
+  integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
+
 json-schema-traverse@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz"
@@ -1086,6 +1085,13 @@ jsprim@^2.0.2:
     json-schema "0.4.0"
     verror "1.10.0"
 
+keyv@^4.5.4:
+  version "4.5.4"
+  resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
+  integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
+  dependencies:
+    json-buffer "3.0.1"
+
 lazy-ass@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz"
@@ -1393,7 +1399,7 @@ reusify@^1.0.4:
   resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
   integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
 
-rimraf@^3.0.0, rimraf@^3.0.2:
+rimraf@^3.0.0:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz"
   integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
diff --git a/extensions/phonetic/tests/src/org/openrefine/phonetic/keyers/BeiderMorseKeyerTest.java b/main/tests/server/src/com/google/refine/clustering/binning/BeiderMorseKeyerTest.java
similarity index 83%
rename from extensions/phonetic/tests/src/org/openrefine/phonetic/keyers/BeiderMorseKeyerTest.java
rename to main/tests/server/src/com/google/refine/clustering/binning/BeiderMorseKeyerTest.java
index adca635f8..85b5b4666 100644
--- a/extensions/phonetic/tests/src/org/openrefine/phonetic/keyers/BeiderMorseKeyerTest.java
+++ b/main/tests/server/src/com/google/refine/clustering/binning/BeiderMorseKeyerTest.java
@@ -1,12 +1,10 @@
 
-package org.openrefine.phonetic.keyers;
+package com.google.refine.clustering.binning;
 
 import static org.testng.Assert.assertEquals;
 
 import org.testng.annotations.Test;
 
-import com.google.refine.clustering.binning.Keyer;
-
 public class BeiderMorseKeyerTest {
 
     Keyer keyer = new BeiderMorseKeyer();
diff --git a/extensions/phonetic/tests/src/org/openrefine/phonetic/keyers/DaitchMokotoffKeyerTest.java b/main/tests/server/src/com/google/refine/clustering/binning/DaitchMokotoffKeyerTest.java
similarity index 84%
rename from extensions/phonetic/tests/src/org/openrefine/phonetic/keyers/DaitchMokotoffKeyerTest.java
rename to main/tests/server/src/com/google/refine/clustering/binning/DaitchMokotoffKeyerTest.java
index 50129ff1b..c699803e3 100644
--- a/extensions/phonetic/tests/src/org/openrefine/phonetic/keyers/DaitchMokotoffKeyerTest.java
+++ b/main/tests/server/src/com/google/refine/clustering/binning/DaitchMokotoffKeyerTest.java
@@ -1,12 +1,10 @@
 
-package org.openrefine.phonetic.keyers;
+package com.google.refine.clustering.binning;
 
 import static org.testng.Assert.assertEquals;
 
 import org.testng.annotations.Test;
 
-import com.google.refine.clustering.binning.Keyer;
-
 public class DaitchMokotoffKeyerTest {
 
     protected Keyer keyer = new DaitchMokotoffKeyer();