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 )
-

-    #foreach($item in $list)
-
$item

-    #end
-

-#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>
-
-
-
-    <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>

   </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>

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();

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.

Try harder to return separator guess. Fixes #6516


diff --git a/main/src/com/google/refine/importers/LineBasedFormatGuesser.java b/main/src/com/google/refine/importers/LineBasedFormatGuesser.java
index cff54e771..8987b8b71 100644
--- a/main/src/com/google/refine/importers/LineBasedFormatGuesser.java
+++ b/main/src/com/google/refine/importers/LineBasedFormatGuesser.java
@@ -35,7 +35,7 @@ public class LineBasedFormatGuesser implements FormatGuesser {

     @Override
     public String guess(File file, String encoding, String seedFormat) {
-        SeparatorBasedImporter.Separator sep = SeparatorBasedImporter.guessSeparator(file, encoding);
+        SeparatorBasedImporter.Separator sep = SeparatorBasedImporter.guessSeparator(file, encoding, true);
         if (sep != null) {
             return "text/line-based/*sv";
         }
diff --git a/main/src/com/google/refine/importing/ImportingUtilities.java b/main/src/com/google/refine/importing/ImportingUtilities.java
index 6209f9a7b..0e4898964 100644
--- a/main/src/com/google/refine/importing/ImportingUtilities.java
+++ b/main/src/com/google/refine/importing/ImportingUtilities.java
@@ -439,6 +439,7 @@ public class ImportingUtilities {
                             calculateProgressPercent(update.totalExpectedSize, update.totalRetrievedSize));

                     JSONUtilities.safePut(fileRecord, "size", saveStreamToFile(stream, file, null));
+                    JSONUtilities.safePut(fileRecord, "format", guessBetterFormat(file, request.getCharacterEncoding(), "text"));
                     // TODO: This needs to be refactored to be able to test import from archives
                     if (postProcessRetrievedFile(rawDataDir, file, fileRecord, fileRecords, progress)) {
                         archiveCount++;
@@ -1015,27 +1016,34 @@ public class ImportingUtilities {

             if (location != null) {
                 File file = new File(job.getRawDataDir(), location);
+                bestFormat = guessBetterFormat(file, encoding, bestFormat);
+            }
+        }
+        return bestFormat;
+    }

-                while (true) {
-                    String betterFormat = null;
-
-                    List<FormatGuesser> guessers = ImportingManager.formatToGuessers.get(bestFormat);
-                    if (guessers != null) {
-                        for (FormatGuesser guesser : guessers) {
-                            betterFormat = guesser.guess(file, encoding, bestFormat);
-                            if (betterFormat != null) {
-                                break;
-                            }
+    static String guessBetterFormat(File file, String fileEncoding, String bestFormat) {
+        if (bestFormat != null && file != null) {
+            while (true) {
+                String betterFormat = null;
+
+                List<FormatGuesser> guessers = ImportingManager.formatToGuessers.get(bestFormat);
+                if (guessers != null) {
+                    for (FormatGuesser guesser : guessers) {
+                        betterFormat = guesser.guess(file, fileEncoding, bestFormat);
+                        if (betterFormat != null) {
+                            break;
                         }
                     }
+                }

-                    if (betterFormat != null && !betterFormat.equals(bestFormat)) {
-                        bestFormat = betterFormat;
-                    } else {
-                        break;
-                    }
+                if (betterFormat != null && !betterFormat.equals(bestFormat)) {
+                    bestFormat = betterFormat;
+                } else {
+                    break;
                 }
             }
+
         }
         return bestFormat;
     }

diff --git a/main/tests/server/src/com/google/refine/importing/ImportingUtilitiesTests.java b/main/tests/server/src/com/google/refine/importing/ImportingUtilitiesTests.java
index 1d1a65f37..aa75b61c8 100644
--- a/main/tests/server/src/com/google/refine/importing/ImportingUtilitiesTests.java
+++ b/main/tests/server/src/com/google/refine/importing/ImportingUtilitiesTests.java
@@ -41,7 +41,6 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.net.URISyntaxException;
-import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -69,6 +68,7 @@ import org.apache.commons.fileupload.FileUploadException;
 import org.apache.commons.io.FileSystem;
 import org.apache.commons.io.FileUtils;
 import org.apache.hc.client5.http.ClientProtocolException;
+import org.apache.hc.client5.http.entity.mime.FileBody;
 import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
 import org.apache.hc.client5.http.entity.mime.StringBody;
 import org.apache.hc.core5.http.ContentType;
@@ -80,7 +80,9 @@ import org.testng.annotations.Test;
 import com.google.refine.ProjectMetadata;
 import com.google.refine.importers.ImporterTest;
 import com.google.refine.importers.ImportingParserBase;
+import com.google.refine.importers.LineBasedFormatGuesser;
 import com.google.refine.importers.SeparatorBasedImporter;
+import com.google.refine.importers.TextFormatGuesser;
 import com.google.refine.importing.ImportingUtilities.Progress;
 import com.google.refine.util.JSONUtilities;
 import com.google.refine.util.ParsingUtilities;
@@ -92,6 +94,8 @@ public class ImportingUtilitiesTests extends ImporterTest {
     @BeforeMethod
     public void setUp() {
         super.setUp();
+        ImportingManager.registerFormatGuesser("text", new TextFormatGuesser());
+        ImportingManager.registerFormatGuesser("text/line-based", new LineBasedFormatGuesser());
     }

     @Test
@@ -576,4 +580,90 @@ public class ImportingUtilitiesTests extends ImporterTest {

         assertEquals(fileName, ImportingUtilities.getFileName(fileRecord));
     }
+
+    @Test
+    public void testFormatForMultipleCSVFiles() throws IOException, FileUploadException {
+        testMultipleFiles("birds", "food.small", ".csv", ContentType.create("text/csv"), "text/line-based/*sv");
+    }
+
+    @Test
+    public void testFormatForMultipleTSVFiles() throws IOException, FileUploadException {
+        testMultipleFiles("movies", "presidents", ".tsv", ContentType.create("text/tsv"), "text/line-based/*sv");
+    }
+
+    @Test
+    public void testFormatForMultipleExcelFiles() throws IOException, FileUploadException {
+        testMultipleFiles("dates", "excel95", ".xls",
+                ContentType.create("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), "binary");
+    }
+
+    @Test
+    public void testFormatForMultipleJSONFiles() throws IOException, FileUploadException {
+        testMultipleFiles("grid_small", "grid_small", ".json", ContentType.create("text/json"), "text/json");
+    }
+
+    private void testMultipleFiles(String file1, String file2, String fileSuffix, ContentType contentType, String expectedFormat)
+            throws IOException, FileUploadException {
+
+        String filepath1 = ClassLoader.getSystemResource(file1 + fileSuffix).getPath();
+        String filepath2 = ClassLoader.getSystemResource(file2 + fileSuffix).getPath();
+
+        File tmp1 = File.createTempFile("openrefine-test-" + file1, fileSuffix, job.getRawDataDir());
+        tmp1.deleteOnExit();
+
+        FileUtils.copyFile(new File(filepath1), tmp1);
+
+        File tmp2 = File.createTempFile("openrefine-test-" + file2, fileSuffix, job.getRawDataDir());
+        tmp2.deleteOnExit();
+
+        FileUtils.copyFile(new File(filepath2), tmp2);
+
+        MultipartEntityBuilder builder = MultipartEntityBuilder.create();
+        FileBody fileBody1 = new FileBody(tmp1, contentType);
+        FileBody fileBody2 = new FileBody(tmp2, contentType);
+        builder = builder.addPart("upload", fileBody1);
+        builder = builder.addPart("upload", fileBody2);
+
+        HttpEntity entity = builder.build();
+
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        entity.writeTo(os);
+        ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
+
+        HttpServletRequest req = mock(HttpServletRequest.class);
+        when(req.getContentType()).thenReturn(entity.getContentType());
+        when(req.getMethod()).thenReturn("POST");
+        when(req.getContentLength()).thenReturn((int) entity.getContentLength());
+        when(req.getInputStream()).thenReturn(new MockServletInputStream(is));
+
+        ImportingJob job = ImportingManager.createJob();
+        Map<String, String> parameters = ParsingUtilities.parseParameters(req);
+        ObjectNode config = ParsingUtilities.mapper.createObjectNode();
+        ObjectNode retrievalRecord = ParsingUtilities.mapper.createObjectNode();
+        JSONUtilities.safePut(config, "retrievalRecord", retrievalRecord);
+        JSONUtilities.safePut(config, "state", "loading-raw-data");
+
+        final ObjectNode progress = ParsingUtilities.mapper.createObjectNode();
+        JSONUtilities.safePut(config, "progress", progress);
+
+        ImportingUtilities.retrieveContentFromPostRequest(req, parameters, job.getRawDataDir(), retrievalRecord,
+                new ImportingUtilities.Progress() {
+
+                    @Override
+                    public void setProgress(String message, int percent) {
+                        if (message != null) {
+                            JSONUtilities.safePut(progress, "message", message);
+                        }
+                        JSONUtilities.safePut(progress, "percent", percent);
+                    }
+
+                    @Override
+                    public boolean isCanceled() {
+                        return job.canceled;
+                    }
+                });
+
+        assertEquals(expectedFormat, JSONUtilities.getArray(retrievalRecord, "files").get(0).get("format").asText());
+        assertEquals(expectedFormat, JSONUtilities.getArray(retrievalRecord, "files").get(1).get("format").asText());
+    }
 }

File format info was not getting set when multiple files are uploaded.

Fixes #6772

Changes proposed in this pull request:
- Set format in the import flow for multiple files
- Identify format based on the file, added guessBetterFormat() method
- Tested with multiple file types the format is getting set correctly

File type detection fails when multiple files are imported at once

def parser(log: str) -> dict[str, str]:
    import re
    import xml.etree.ElementTree as ET

    results: dict[str, str] = {}

    # Helper: merge statuses with precedence fail > skip > pass
    def merge_status(name: str, new_status: str):
        if new_status not in ("pass", "fail", "skip"):
            return
        old_status = results.get(name)
        if old_status is None:
            results[name] = new_status
            return
        if old_status == new_status:
            return
        # Precedence: fail > skip > pass
        priority = {"fail": 3, "skip": 2, "pass": 1}
        if priority[new_status] > priority[old_status]:
            results[name] = new_status

    # -----------------------
    # XML-based parsing pass
    # -----------------------

    # Non-greedy patterns for JUnit/TestNG XML blocks
    xml_pattern = re.compile(
        r"(<testng-results[\s\S]*?</testng-results>|<testsuite[\s\S]*?</testsuite>)",
        re.IGNORECASE,
    )

    for match in xml_pattern.finditer(log):
        xml_block = match.group(1)
        try:
            root = ET.fromstring(xml_block)
        except ET.ParseError:
            # Skip this block but continue with others
            continue

        tag_lower = root.tag.lower()

        # Handle TestNG summary reports (<testng-results>)
        if tag_lower.endswith("testng-results"):
            # Look for all test-method elements anywhere
            for tm in root.findall(".//test-method"):
                # Skip configuration methods
                if tm.attrib.get("is-config", "false").lower() == "true":
                    continue
                name = tm.attrib.get("name", "")
                if not name:
                    continue
                # Try to get class name from surrounding <class> element
                class_elem = tm.find("..")
                class_name = ""
                # xml.etree doesn't support parent lookup directly, so search ancestors:
                # We instead search for the nearest <class> ancestor manually:
            # Since parent lookup is tricky, fall back to the attribute 'class' on <test-method> if present
            # or to a surrounding <class> element found via xpath with name match, which is usually enough.
            # Re-parse with a simpler loop using suites/tests/classes for better class name resolution.
            for suite in root.findall(".//suite"):
                for test in suite.findall(".//test"):
                    for clazz in test.findall(".//class"):
                        class_name = clazz.attrib.get("name", "")
                        for tm in clazz.findall("test-method"):
                            if tm.attrib.get("is-config", "false").lower() == "true":
                                continue
                            m_name = tm.attrib.get("name", "")
                            if not m_name:
                                continue
                            fq_name = f"{class_name}.{m_name}" if class_name else m_name
                            status_attr = tm.attrib.get("status", "").upper()
                            # Map TestNG status to our statuses
                            if status_attr == "PASS":
                                status = "pass"
                            elif status_attr in ("FAIL", "FAILURE"):
                                status = "fail"
                            elif status_attr == "SKIP":
                                status = "skip"
                            else:
                                # Fallback: if there is an exception element, mark as fail; else pass
                                if tm.find(".//exception") is not None:
                                    status = "fail"
                                else:
                                    status = "pass"
                            merge_status(fq_name, status)
            continue

        # Handle JUnit-style <testsuite> reports
        if tag_lower.endswith("testsuite"):
            for tc in root.findall(".//testcase"):
                class_name = tc.attrib.get("classname", "")
                name = tc.attrib.get("name", "")
                if not name:
                    continue
                fq_name = f"{class_name}.{name}" if class_name else name

                # Determine status via children
                status = "pass"
                if tc.find("skipped") is not None:
                    status = "skip"
                elif tc.find("failure") is not None or tc.find("error") is not None:
                    status = "fail"

                merge_status(fq_name, status)

    # ---------------------------
    # Text-based parsing fallback
    # ---------------------------

    # Compile regexes for common frameworks

    # pytest: path::test_name STATUS
    pytest_re = re.compile(
        r"^(?P<name>\S+::\S+)\s+"
        r"(?P<status>PASSED|FAILED|SKIPPED|ERROR|XFAILED|XPASSED)\b"
    )

    # unittest / nose: ClassName.test_method ... ok/FAIL/ERROR/skipped
    unittest_re = re.compile(
        r"^(?P<class>[A-Za-z_]\w*)\.(?P<method>[A-Za-z_]\w*)\s+\.\.\.\s+"
        r"(?P<status>ok|FAIL|ERROR|skipped)\b"
    )

    # go test: --- STATUS: TestName
    gotest_re = re.compile(
        r"^---\s+(?P<status>PASS|FAIL|SKIP):\s+(?P<name>\S+)"
    )

    # Jest-like symbol lines: ✓/✕/○ test name
    jest_symbol_re = re.compile(r"^\s*([✓✕○])\s+(?P<name>.+)")

    # Map various raw statuses to our canonical ones
    def map_status(raw: str) -> str | None:
        r = raw.upper()
        if r in ("PASSED", "OK", "XPASSED", "PASS"):
            return "pass"
        if r in ("FAILED", "FAIL", "ERROR", "XFAILED"):
            return "fail"
        if r in ("SKIPPED", "SKIP", "SKIPPED (EXPECTED FAILURE)"):
            return "skip"
        return None

    for line in log.splitlines():
        line = line.rstrip("\n")

        # pytest
        m = pytest_re.match(line)
        if m:
            name = m.group("name")
            status = map_status(m.group("status"))
            if status:
                merge_status(name, status)
            continue

        # unittest
        m = unittest_re.match(line)
        if m:
            name = f"{m.group('class')}.{m.group('method')}"
            status = map_status(m.group("status"))
            if status:
                merge_status(name, status)
            continue

        # go test
        m = gotest_re.match(line)
        if m:
            name = m.group("name")
            status = map_status(m.group("status"))
            if status:
                merge_status(name, status)
            continue

        # Jest symbols
        m = jest_symbol_re.match(line)
        if m:
            symbol = m.group(1)
            name = m.group("name").strip()
            if symbol == "✓":
                status = "pass"
            elif symbol == "✕":
                status = "fail"
            elif symbol == "○":
                status = "skip"
            else:
                status = None
            if status:
                merge_status(name, status)
            continue

    return results

export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq nodejs npm
./refine build

find . -maxdepth 6 -path '*surefire-reports/*.xml' -print0 | xargs -0 -I {} sh -c 'echo "===== {} ====="; cat {}'


diff --git a/extensions/database/pom.xml b/extensions/database/pom.xml
index 1403e93f5..f79fa6797 100644
--- a/extensions/database/pom.xml
+++ b/extensions/database/pom.xml
@@ -104,7 +104,7 @@
     <dependency>
       <groupId>org.xerial</groupId>
       <artifactId>sqlite-jdbc</artifactId>
-      <version>3.46.0.0</version>
+      <version>3.46.0.1</version>
     </dependency>
     <dependency>
       <groupId>com.fasterxml.jackson.core</groupId>
diff --git a/extensions/gdata/pom.xml b/extensions/gdata/pom.xml
index 1dee209aa..3842e9f18 100644
--- a/extensions/gdata/pom.xml
+++ b/extensions/gdata/pom.xml
@@ -83,7 +83,7 @@
     <dependency>
       <groupId>com.google.apis</groupId>
       <artifactId>google-api-services-drive</artifactId>
-      <version>v3-rev20240628-2.0.0</version>
+      <version>v3-rev20240722-2.0.0</version>
     </dependency>
     <dependency>
       <groupId>com.google.apis</groupId>
diff --git a/main/src/com/google/refine/importers/SeparatorBasedImporter.java b/main/src/com/google/refine/importers/SeparatorBasedImporter.java
index 9cf332947..56a3614b3 100644
--- a/main/src/com/google/refine/importers/SeparatorBasedImporter.java
+++ b/main/src/com/google/refine/importers/SeparatorBasedImporter.java
@@ -69,6 +69,10 @@ import com.google.refine.util.JSONUtilities;

 public class SeparatorBasedImporter extends TabularImportingParserBase {

+    // Excel limits: 1M rows x 16K columns, 32K characters max per cell
+    public static final int MAX_COLUMNS = 16 * 1024; // default 512
+    // TODO: Perhaps use a lower default and make user configurable?
+    public static final int MAX_CHARACTERS_PER_CELL = 32 * 1024; // default 4096
     public static final int GUESSER_LINE_COUNT = 100;
     char DEFAULT_QUOTE_CHAR = new CsvParserSettings().getFormat().getQuote();

@@ -145,7 +149,8 @@ public class SeparatorBasedImporter extends TabularImportingParserBase {
         AbstractParser parser;
         if (tsv) {
             TsvParserSettings settings = new TsvParserSettings();
-            settings.setMaxCharsPerColumn(256 * 1024); // TODO: Perhaps use a lower default and make user configurable?
+            settings.setMaxCharsPerColumn(MAX_CHARACTERS_PER_CELL);
+            settings.setMaxColumns(MAX_COLUMNS);
             settings.setIgnoreLeadingWhitespaces(false);
             settings.setIgnoreTrailingWhitespaces(false);
             parser = new TsvParser(settings);
@@ -161,7 +166,8 @@ public class SeparatorBasedImporter extends TabularImportingParserBase {
                 settings.setUnescapedQuoteHandling(UnescapedQuoteHandling.RAISE_ERROR);
             }
             settings.setKeepQuotes(!processQuotes);
-            settings.setMaxCharsPerColumn(256 * 1024); // TODO: Perhaps use a lower default and make user configurable?
+            settings.setMaxCharsPerColumn(MAX_CHARACTERS_PER_CELL);
+            settings.setMaxColumns(MAX_COLUMNS);
             parser = new CsvParser(settings);
         }
         try (final LineNumberReader lnReader = new LineNumberReader(reader);) {
diff --git a/pom.xml b/pom.xml
index cb9da7068..d7d4d5ad3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -98,7 +98,7 @@
     <jsoup.version>1.18.1</jsoup.version>
     <odfdom-java.version>0.12.0</odfdom-java.version> <!-- do not update to 0.10.0, see issue #4397 -->
     <vicino.version>1.2.1</vicino.version>
-    <clojure.version>1.11.3</clojure.version>
+    <clojure.version>1.11.4</clojure.version>
     <httpclient5.version>5.3.1</httpclient5.version>
     <httpcore.version>4.4.16</httpcore.version>
     <univocity-csv.version>2.9.1</univocity-csv.version>
@@ -109,7 +109,7 @@
     <juniversalchardet.version>2.5.0</juniversalchardet.version>
     <testng.version>7.10.2</testng.version>
     <httpmime.version>4.5.14</httpmime.version>
-    <owasp-encoder.version>1.2.3</owasp-encoder.version>
+    <owasp-encoder.version>1.3.0</owasp-encoder.version>
     <jasypt.version>1.9.3</jasypt.version>
     <mockito.version>5.12.0</mockito.version>

diff --git a/main/tests/cypress/package.json b/main/tests/cypress/package.json
index cb6f849df..5f0cb8503 100644
--- a/main/tests/cypress/package.json
+++ b/main/tests/cypress/package.json
@@ -16,7 +16,7 @@
         "lint": "prettier --check . && eslint ."
     },
     "dependencies": {
-        "cypress": "13.13.1",
+        "cypress": "13.13.2",
         "cypress-file-upload": "^5.0.8"
     },
     "devDependencies": {
diff --git a/main/tests/cypress/yarn.lock b/main/tests/cypress/yarn.lock
index 65866a6b7..f7ed19f18 100644
--- a/main/tests/cypress/yarn.lock
+++ b/main/tests/cypress/yarn.lock
@@ -7,10 +7,10 @@
   resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf"
   integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==

-"@cypress/request@^3.0.0":
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.0.tgz#7f58dfda087615ed4e6aab1b25fffe7630d6dd85"
-  integrity sha512-GKFCqwZwMYmL3IBoNeR2MM1SnxRIGERsQOTWeQKoYBt2JLqcqiy7JXqO894FLrpjZYqGxW92MNwRH2BN56obdQ==
+"@cypress/request@^3.0.1":
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.1.tgz#72d7d5425236a2413bd3d8bb66d02d9dc3168960"
+  integrity sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==
   dependencies:
     aws-sign2 "~0.7.0"
     aws4 "^1.8.0"
@@ -25,7 +25,7 @@
     json-stringify-safe "~5.0.1"
     mime-types "~2.1.19"
     performance-now "^2.1.0"
-    qs "~6.10.3"
+    qs "6.10.4"
     safe-buffer "^5.1.2"
     tough-cookie "^4.1.3"
     tunnel-agent "^0.6.0"
@@ -436,12 +436,12 @@ cypress-file-upload@^5.0.8:
   resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1"
   integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g==

-cypress@13.13.1:
-  version "13.13.1"
-  resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.13.1.tgz#860c1142a6e58ea412a764f0ce6ad07567721129"
-  integrity sha512-8F9UjL5MDUdgC/S5hr8CGLHbS5gGht5UOV184qc2pFny43fnkoaKxlzH/U6//zmGu/xRTaKimNfjknLT8+UDFg==
+cypress@13.13.2:
+  version "13.13.2"
+  resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.13.2.tgz#c71f8d92056c430b1b879e5313f6de25ccce0eda"
+  integrity sha512-PvJQU33933NvS1StfzEb8/mu2kMy4dABwCF+yd5Bi7Qly1HOVf+Bufrygee/tlmty/6j5lX+KIi8j9Q3JUMbhA==
   dependencies:
-    "@cypress/request" "^3.0.0"
+    "@cypress/request" "^3.0.1"
     "@cypress/xvfb" "^1.2.4"
     "@types/sinonjs__fake-timers" "8.1.1"
     "@types/sizzle" "^2.3.2"
@@ -1317,10 +1317,10 @@ punycode@^2.1.0, punycode@^2.1.1:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==

-qs@~6.10.3:
-  version "6.10.5"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.5.tgz#974715920a80ff6a262264acd2c7e6c2a53282b4"
-  integrity sha512-O5RlPh0VFtR78y79rgcgKK4wbAI0C5zGVLztOIdpWX6ep368q5Hv6XRxDvXuZ9q3C6v+e3n8UfZZJw7IIG27eQ==
+qs@6.10.4:
+  version "6.10.4"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.4.tgz#6a3003755add91c0ec9eacdc5f878b034e73f9e7"
+  integrity sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==
   dependencies:
     side-channel "^1.0.4"

diff --git a/main/tests/server/src/com/google/refine/importers/ImporterTest.java b/main/tests/server/src/com/google/refine/importers/ImporterTest.java
index ec6649714..75e5cdac4 100644
--- a/main/tests/server/src/com/google/refine/importers/ImporterTest.java
+++ b/main/tests/server/src/com/google/refine/importers/ImporterTest.java
@@ -96,7 +96,7 @@ public abstract class ImporterTest extends RefineTest {
                 -1,
                 options,
                 exceptions);
-        assertEquals(exceptions.size(), 0);
+        assertEquals(exceptions.size(), 0, "Unexpected exception(s) thrown: " + exceptions);
         project.update();
     }

@@ -111,7 +111,7 @@ public abstract class ImporterTest extends RefineTest {
                 -1,
                 options,
                 exceptions);
-        assertEquals(exceptions.size(), 0);
+        assertEquals(exceptions.size(), 0, "Unexpected exception(s) thrown: " + exceptions);
         project.update();
     }

@@ -143,7 +143,7 @@ public abstract class ImporterTest extends RefineTest {
                 -1,
                 options,
                 exceptions);
-        assertEquals(exceptions.size(), 0);
+        assertEquals(exceptions.size(), 0, "Unexpected exception(s) thrown: " + exceptions);
         XmlImportUtilities.createColumnsFromImport(project, rootColumnGroup);
         project.columnModel.update();
     }
diff --git a/main/tests/server/src/com/google/refine/importers/SeparatorBasedImporterTests.java b/main/tests/server/src/com/google/refine/importers/SeparatorBasedImporterTests.java
index 92357de95..4f59715c0 100644
--- a/main/tests/server/src/com/google/refine/importers/SeparatorBasedImporterTests.java
+++ b/main/tests/server/src/com/google/refine/importers/SeparatorBasedImporterTests.java
@@ -46,6 +46,7 @@ import java.util.List;

 import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.input.CharSequenceReader;
 import org.slf4j.LoggerFactory;
 import org.testng.Assert;
 import org.testng.annotations.AfterMethod;
@@ -298,6 +299,39 @@ public class SeparatorBasedImporterTests extends ImporterTest {
         assertProjectEquals(project, expectedProject);
     }

+    @Test(dataProvider = "CSV-TSV-AutoDetermine")
+    public void readManyColumns(String sep) {
+        // create input
+        int width = 16 * 1024; // Excel supports 16K columns max, so let's test that
+        String inputSeparator = sep == null ? "\t" : sep;
+        StringBuilder input = new StringBuilder();
+        String[] colNames = new String[width];
+        for (int i = 0; i < width; i++) {
+            String name = "col" + (i + 1);
+            input.append(name + inputSeparator);
+            colNames[i] = name;
+        }
+        input = input.deleteCharAt(input.length() - 1); // we don't need the last separator
+        input.append('\n');
+        String[] data = new String[width];
+        for (int i = 0; i < width; i++) {
+            String value = "data" + (i + 1);
+            input.append(value + inputSeparator);
+            data[i] = value;
+        }
+        input = input.deleteCharAt(input.length() - 1); // we don't need the last separator
+
+        prepareOptions(sep, -1, 0, 0, 1, false, false);
+        parseOneFile(SUT, new CharSequenceReader(input));
+
+        Project expectedProject = createProject(
+                colNames,
+                new Serializable[][] {
+                        data,
+                });
+        assertProjectEquals(project, expectedProject);
+    }
+
     @Test(groups = {}, dataProvider = "CSV-TSV-AutoDetermine")
     public void readQuotedData(String sep) {
         // create input

Fixes #6723

Changes proposed in this pull request:
- Increase TSV/CSV column limit to 32K (from 512)
- Make cryptic test assertion failures easier to understand

Increase TSV/CSV column limit to 32K (from 512). Fixes #6723


diff --git a/src/main/java/com/thealgorithms/backtracking/FloodFill.java b/src/main/java/com/thealgorithms/backtracking/FloodFill.java
index 01b26916..eb4939b3 100644
--- a/src/main/java/com/thealgorithms/backtracking/FloodFill.java
+++ b/src/main/java/com/thealgorithms/backtracking/FloodFill.java
@@ -39,6 +39,7 @@ public class FloodFill {
      * @param oldColor The old color which is to be replaced in the image
      */
     public static void floodFill(int[][] image, int x, int y, int newColor, int oldColor) {
+        if (newColor == oldColor) return;
         if (x < 0 || x >= image.length) return;
         if (y < 0 || y >= image[x].length) return;
         if (getPixel(image, x, y) != oldColor) return;

diff --git a/src/test/java/com/thealgorithms/backtracking/FloodFillTest.java b/src/test/java/com/thealgorithms/backtracking/FloodFillTest.java
index 7a59b210..8b860b5b 100644
--- a/src/test/java/com/thealgorithms/backtracking/FloodFillTest.java
+++ b/src/test/java/com/thealgorithms/backtracking/FloodFillTest.java
@@ -93,4 +93,14 @@ class FloodFillTest {
         FloodFill.floodFill(image, 0, 1, 4, 1);
         assertArrayEquals(expected, image);
     }
+
+    @Test
+    void testForSameNewAndOldColor() {
+        int[][] image = {{1, 1, 2}, {1, 0, 0}, {1, 1, 1}};
+
+        int[][] expected = {{1, 1, 2}, {1, 0, 0}, {1, 1, 1}};
+
+        FloodFill.floodFill(image, 0, 1, 1, 1);
+        assertArrayEquals(expected, image);
+    }
 }

<!-- For completed items, change [ ] to [x] -->

- [x] I have read [CONTRIBUTING.md](https://github.com/TheAlgorithms/Java/blob/master/CONTRIBUTING.md).
- [x] This pull request is all my own work -- I have not plagiarized it.
- [x] All filenames are in PascalCase.
- [x] All functions and variable names follow Java naming conventions.
- [ ] All new algorithms have a URL in their comments that points to Wikipedia or other similar explanations.

Fixed #4358
Fixed case when both newColor and oldColor is passed as same integer causing infinite recursion by adding extra base condition.
Also added testcase for same and after that ran all testcase, which resulted as all passed.

<img width="1511" alt="Screenshot 2023-09-09 at 6 04 36 PM" src="https://github.com/TheAlgorithms/Java/assets/76104205/b046b071-d9c3-443b-83c4-16075332724c">

#4358 Fix : Floodfill infinite recursion due to same color

def parse_java_mvn(log: str) -> dict[str, str]:
    """Parse Maven logs into {identifier: status}."""
    results = {}

    failure_re = re.compile(
        r"^\[ERROR\]\s+(?P<class>[A-Za-z0-9_$.]+)\.(?P<method>[A-Za-z0-9_$.]+?)(?::(?P<line>\d+))?\b"
    )
    running_re = re.compile(r"^(?:\[INFO\]\s+)?Running\s+([A-Za-z0-9_$.]+)$")
    summary_re = re.compile(
        r"Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)"
    )

    seen_running = set()
    current_running = None

    for raw in log.splitlines():
        line = raw.strip()
        if not line:
            continue
        rm = running_re.match(line)
        if rm:
            running_name = rm.group(1)
            current_running = running_name
            seen_running.add(running_name)
            continue
        fm = failure_re.match(line)
        if fm:
            clazz = fm.group("class")
            method = fm.group("method")
            test_id = f"{clazz}.{method}"
            results[test_id] = "FAILED"
            results.setdefault(clazz, "FAILED")
            continue

        sm = summary_re.search(line)
        if sm:
            _tests_run, failures, errors, skipped = map(int, sm.groups())
            if failures > 0 or errors > 0:
                status = "FAILED"
            elif skipped > 0:
                status = "SKIPPED"
            else:
                status = "PASSED"
            if current_running:
                results.setdefault(current_running, status)
            current_running = None

    if not results and seen_running:
        for clazz in seen_running:
            results.setdefault(clazz, "PASSED")
    return results

bash -c '{ mvn --batch-mode test ; } > /tmp/test.log 2>&1'


diff --git a/main/webapp/modules/core/scripts/dialogs/column-reordering-dialog.js b/main/webapp/modules/core/scripts/dialogs/column-reordering-dialog.js
index 83310818b..dfd0fe056 100644
--- a/main/webapp/modules/core/scripts/dialogs/column-reordering-dialog.js
+++ b/main/webapp/modules/core/scripts/dialogs/column-reordering-dialog.js
@@ -86,6 +86,7 @@ ColumnReorderingDialog.prototype._dismiss = function() {
 };

 ColumnReorderingDialog.prototype._commit = function() {
+    var self = this;
     var columnNames = this._elmts.columnContainer.find('div').map(function() { return this.getAttribute("column"); }).get();

     Refine.postCoreProcess(
@@ -93,8 +94,9 @@ ColumnReorderingDialog.prototype._commit = function() {
         null,
         { "columnNames" : JSON.stringify(columnNames) },
         { modelsChanged: true, rowIdsPreserved: true }, // TODO could add recordIdsPreserved: true if the record key column did not change
-        { includeEngine: false }
+        {
+          includeEngine: false,
+          onDone: function() { self._dismiss(); }
+        }
     );
-
-    this._dismiss();
 };
diff --git a/main/webapp/modules/core/scripts/dialogs/common-transform-dialog.js b/main/webapp/modules/core/scripts/dialogs/common-transform-dialog.js
index 1632078c7..d66f60632 100644
--- a/main/webapp/modules/core/scripts/dialogs/common-transform-dialog.js
+++ b/main/webapp/modules/core/scripts/dialogs/common-transform-dialog.js
@@ -106,29 +106,32 @@ commonTransformDialog.prototype._dismiss = function() {

 commonTransformDialog.prototype._commit = function(expression) {
-      var doTextTransform = function(columnName, expression, onError, repeat, repeatCount) {
+  var self = this;
+  var columnNames = [];
+  this._elmts.columnContainer.find('div').each(function() {
+    if ($(this).find('input[type="checkbox"]')[0].checked) {
+      var name = this.getAttribute('column');
+      columnNames.push(name);
+    }
+  });
+  var doTextTransform = function(index, expression, onError, repeat, repeatCount) {
+    if (index < columnNames.length) {
       Refine.postCoreProcess(
         "text-transform",
         {
-          columnName: columnName,
+          columnName: columnNames[index],
           expression: expression,
           onError: onError,
           repeat: repeat,
           repeatCount: repeatCount
         },
         null,
-        { cellsChanged: true, rowIdsPreserved: true }
+        { cellsChanged: true, rowIdsPreserved: true },
+        { onDone: function() { doTextTransform(index + 1, expression, onError, repeat, repeatCount) } }
       );
-  };
-
-	this._elmts.columnContainer.find('div').each(function() {
-    if ($(this).find('input[type="checkbox"]')[0].checked) {
-      var name = this.getAttribute('column');
-	    doTextTransform(name,expression, "keep-original", false, "");
+    } else {
+      self._dismiss();
     }
-  });
-
-
-    this._dismiss();
-
+  };
+  doTextTransform(0, expression, "keep-original", false, "");
 };
diff --git a/main/webapp/modules/core/scripts/dialogs/expression-column-dialog.js b/main/webapp/modules/core/scripts/dialogs/expression-column-dialog.js
index ca2334e71..55f64b02c 100644
--- a/main/webapp/modules/core/scripts/dialogs/expression-column-dialog.js
+++ b/main/webapp/modules/core/scripts/dialogs/expression-column-dialog.js
@@ -1,5 +1,5 @@

-var doTextTransform = function(columnName, expression, onError, repeat, repeatCount) {
+var doTextTransform = function(columnName, expression, onError, repeat, repeatCount, onDone) {
     Refine.postCoreProcess(
       "text-transform",
       {
@@ -10,7 +10,8 @@ var doTextTransform = function(columnName, expression, onError, repeat, repeatCo
         repeatCount: repeatCount
       },
       null,
-      { cellsChanged: true, rowIdsPreserved: true }
+      { cellsChanged: true, rowIdsPreserved: true },
+      { onDone }
     );
 };

@@ -87,7 +88,6 @@ ExpressionColumnDialog.prototype._dismiss = function() {

 ExpressionColumnDialog.prototype._transform = function() {
   this._postSelect();
-  this._dismiss();
 };

 ExpressionColumnDialog.prototype._postSelect = function() {
@@ -96,7 +96,7 @@ ExpressionColumnDialog.prototype._postSelect = function() {
     if ($(this).find('input[type="checkbox"]')[0].checked) {
       var name = this.getAttribute('column');
       // alert("doTextTransform on: " + name + "; expression: " + self._expression);
-	  doTextTransform(name, self._expression, self._onError, self._repeat, self._repeatCount)
+	  doTextTransform(name, self._expression, self._onError, self._repeat, self._repeatCount, self._dismiss)
     }
   });

diff --git a/main/webapp/modules/core/scripts/facets/list-facet.js b/main/webapp/modules/core/scripts/facets/list-facet.js
index ffcc6e1fd..7fe7567e7 100644
--- a/main/webapp/modules/core/scripts/facets/list-facet.js
+++ b/main/webapp/modules/core/scripts/facets/list-facet.js
@@ -583,8 +583,6 @@ class ListFacet extends Facet {
     var commit = function() {
       var text = elmts.textarea[0].value;

-      MenuSystem.dismissAll();
-
       var edit = { to : text };
       if (choice === self._blankChoice) {
         edit.fromBlank = true;
@@ -609,6 +607,7 @@ class ListFacet extends Facet {
         },
         {
           onDone: function(o) {
+            MenuSystem.dismissAll();
             var selection = [];
             var gotSelection = false;
             for (var i = 0; i < self._selection.length; i++) {
diff --git a/main/webapp/modules/core/scripts/util/csrf.js b/main/webapp/modules/core/scripts/util/csrf.js
index d156916d6..a904a523f 100644
--- a/main/webapp/modules/core/scripts/util/csrf.js
+++ b/main/webapp/modules/core/scripts/util/csrf.js
@@ -20,11 +20,7 @@ CSRFUtil.postCSRF = function(url, data, success, dataType, failCallback) {
     return CSRFUtil.wrapCSRF(function(token) {
         var fullData = data || {};
         if (typeof fullData == 'string') {
-            if (fullData.includes('?')) {
-              fullData = fullData + "&" + $.param({csrf_token: token});
-            } else {
-              fullData = fullData + "?" + $.param({csrf_token: token});
-            }
+            fullData = fullData + "&" + $.param({csrf_token: token});
         } else {
             fullData['csrf_token'] = token;
         }
diff --git a/main/webapp/modules/core/scripts/views/data-table/menu-edit-cells.js b/main/webapp/modules/core/scripts/views/data-table/menu-edit-cells.js
index f2b7978b5..584d81d42 100644
--- a/main/webapp/modules/core/scripts/views/data-table/menu-edit-cells.js
+++ b/main/webapp/modules/core/scripts/views/data-table/menu-edit-cells.js
@@ -32,7 +32,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */

 DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
-  var doTextTransform = function(expression, onError, repeat, repeatCount) {
+  var doTextTransform = function(expression, onError, repeat, repeatCount, onDone) {
     Refine.postCoreProcess(
       "text-transform",
       {
@@ -42,7 +42,8 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
         repeatCount: repeatCount
       },
       { expression: expression },
-      { cellsChanged: true }
+      { cellsChanged: true },
+      { onDone },
     );
   };

@@ -72,9 +73,9 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
         previewWidget.getExpression(true),
         $('input[name="text-transform-dialog-onerror-choice"]:checked')[0].value,
         elmts.repeatCheckbox[0].checked,
-        elmts.repeatCountInput[0].value
+        elmts.repeatCountInput[0].value,
+        dismiss
       );
-      dismiss();
     });

     var o = DataTableView.sampleVisibleRows(column);
@@ -285,8 +286,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
         text_to_find = '"'+text_to_find+'"';
       }
       expression = 'value.replace('+text_to_find+',"'+replacement_text+'")';
-      doTextTransform(expression, "keep-original", false, "");
-      dismiss();
+      doTextTransform(expression, "keep-original", false, "", dismiss);
     });
   };

@@ -383,10 +383,9 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
         "split-multi-value-cells",
         config,
         null,
-        { rowsChanged: true }
+        { rowsChanged: true },
+        { onDone: dismiss }
       );
-
-      dismiss();
     });
   };

@@ -701,9 +700,9 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
         "key-value-columnize",
         config,
         null,
-        { modelsChanged: true }
+        { modelsChanged: true },
+        { onDone: dismiss }
       );
-      dismiss();
     });

     var valueColumnIndex = -1;
diff --git a/main/webapp/modules/core/scripts/views/data-table/menu-edit-column.js b/main/webapp/modules/core/scripts/views/data-table/menu-edit-column.js
index 9c9dec8fa..a27988a18 100644
--- a/main/webapp/modules/core/scripts/views/data-table/menu-edit-column.js
+++ b/main/webapp/modules/core/scripts/views/data-table/menu-edit-column.js
@@ -166,9 +166,9 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
           httpHeaders: JSON.stringify(elmts.setHttpHeadersContainer.find("input").serializeArray())
         },
         null,
-        { modelsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true }
+        { modelsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true },
+        { onDone: dismiss }
       );
-      dismiss();
     });
   };

@@ -358,9 +358,9 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
         "split-column",
         config,
         null,
-        { modelsChanged: true }
+        { modelsChanged: true },
+        { onDone: dismiss }
       );
-      dismiss();
     });
   };

@@ -399,7 +399,6 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
       // function called in a callback
       var deleteColumns = function() {
         if (deleteJoinedColumns) {
-          console.log (theProject);
           var columnsToKeep = theProject.columnModel.columns
           .map (function (col) {return col.name;})
           .filter (function(colName) {
@@ -466,7 +465,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
           },
           { expression: expression },
           { modelsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true },
-          { onFinallyDone: deleteColumns}
+          { onDone: dismiss, onFinallyDone: deleteColumns}
         );
       }
       else {
@@ -476,7 +475,7 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
             onError,
             repeat,
             repeatCount,
-            { onFinallyDone: deleteColumns});
+            { onDone: dismiss, onFinallyDone: deleteColumns});
       }
     };
     // core of doJoinColumn
@@ -549,7 +548,6 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
       });
     elmts.okButton.on('click',function() {
       transform();
-      dismiss();
     });
     elmts.cancelButton.on('click',function() {
       dismiss();
diff --git a/main/webapp/modules/core/scripts/views/data-table/menu-reconcile.js b/main/webapp/modules/core/scripts/views/data-table/menu-reconcile.js
index 54f649774..9b5d0cd1f 100644
--- a/main/webapp/modules/core/scripts/views/data-table/menu-reconcile.js
+++ b/main/webapp/modules/core/scripts/views/data-table/menu-reconcile.js
@@ -221,10 +221,9 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
             schemaSpace: service.schemaSpace
         },
         null,
-        { cellsChanged: true, columnStatsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true }
+        { cellsChanged: true, columnStatsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true },
+        { onDone: function() { DialogSystem.dismissUntil(level - 1); } }
         );
-
-        DialogSystem.dismissUntil(level - 1);
     });

     $('<button class="button"></button>').text($.i18n('core-buttons/cancel')).on('click',function() {
@@ -395,9 +394,9 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
           "recon-copy-across-columns",
           null,
           config,
-          { rowsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true }
+          { rowsChanged: true, rowIdsPreserved: true, recordIdsPreserved: true },
+          { onDone: dismiss }
         );
-        dismiss();
       }
     });
   };
diff --git a/modules/grel/src/main/java/com/google/refine/expr/functions/ToString.java b/modules/grel/src/main/java/com/google/refine/expr/functions/ToString.java
index f4b11b037..bac23d0c1 100644
--- a/modules/grel/src/main/java/com/google/refine/expr/functions/ToString.java
+++ b/modules/grel/src/main/java/com/google/refine/expr/functions/ToString.java
@@ -35,8 +35,8 @@ package com.google.refine.expr.functions;

 import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
+import java.util.IllegalFormatException;
 import java.util.Properties;
-import java.util.UnknownFormatConversionException;

 import com.google.refine.expr.EvalError;
 import com.google.refine.grel.ControlFunctionRegistry;
@@ -59,7 +59,7 @@ public class ToString implements Function {
                 } else if (o1 instanceof Number) {
                     try {
                         return String.format((String) o2, o1);
-                    } catch (UnknownFormatConversionException e) {
+                    } catch (IllegalFormatException e) {
                         return new EvalError(EvalErrorMessage.unknown_format_conversion(e.getMessage()));
                     }
                 }

diff --git a/main/tests/cypress/cypress/e2e/project/undo_redo/warn_for_history_deletion.cy.js b/main/tests/cypress/cypress/e2e/project/undo_redo/warn_for_history_deletion.cy.js
index 6c61ebdf9..8faa02bd5 100644
--- a/main/tests/cypress/cypress/e2e/project/undo_redo/warn_for_history_deletion.cy.js
+++ b/main/tests/cypress/cypress/e2e/project/undo_redo/warn_for_history_deletion.cy.js
@@ -83,4 +83,50 @@ describe(__filename, function () {
     // There are no future changes anymore, since they have been deleted
     cy.get('.history-panel-body .history-future a').should('not.exist');
   });
+
+  it('Apply a text transform after undoing some changes', function () {
+    cy.loadAndVisitProject('food.mini');
+
+    // delete NDB_No
+    cy.deleteColumn('NDB_No');
+    cy.get('#or-proj-undoRedo').should('to.contain', '1 / 1');
+    cy.get('.history-panel-body .history-now').should(
+      'to.contain',
+      'Remove column NDB_No'
+    );
+
+    // Open the Undo/Redo panel
+    cy.get('#or-proj-undoRedo').click();
+
+    // undo the change
+    cy.get(
+      '.history-panel-body .history-past a.history-entry:first-of-type'
+    ).click();
+    cy.get('.history-panel-body .history-now').should(
+      'to.contain',
+      'Create project'
+    );
+    cy.get('.history-panel-body .history-future').should(
+      'to.contain',
+      'Remove column NDB_No'
+    );
+
+    cy.columnActionClick('Shrt_Desc', ['Edit cells', 'Transform']);
+
+    cy.typeExpression('value.replace("E","e")');
+    cy.get('.dialog-footer .button-primary').click();
+
+    // ensure we get a dialog warning us for deletion of changes
+    cy.get('.dialog-header').should('to.contain', 'Confirm deletion of project history');
+    // confirm the dialog
+    cy.get('.dialog-footer .button-primary').contains('Apply anyway').click();
+
+    // ensure value has been changed in the grid
+    cy.get('.menu-container.data-table-cell-editor').should('not.exist');
+    cy.assertCellEquals(1, 'Shrt_Desc', 'BUTTeR,WHIPPeD,WITH SALT');
+
+    // There are no future changes anymore, since they have been deleted
+    cy.get('.history-panel-body .history-future a').should('not.exist');
+  });
+
 });
diff --git a/modules/grel/src/test/java/com/google/refine/expr/functions/ToStringTests.java b/modules/grel/src/test/java/com/google/refine/expr/functions/ToStringTests.java
index 1b7974011..a38b7549b 100644
--- a/modules/grel/src/test/java/com/google/refine/expr/functions/ToStringTests.java
+++ b/modules/grel/src/test/java/com/google/refine/expr/functions/ToStringTests.java
@@ -59,6 +59,7 @@ public class ToStringTests extends GrelTestBase {
         assertTrue(invoke("toString", 100L, "%") instanceof EvalError);
         assertTrue(invoke("toString", 100L, "%.") instanceof EvalError);
         assertTrue(invoke("toString", 100L, "%0") instanceof EvalError);
+        assertTrue(invoke("toString", 12L, "%2.2f") instanceof EvalError);

         // test with large number
         assertEquals(invoke("toString", 1000000000000000000L, "%d"), "1000000000000000000");

Fixes #6943

Changes proposed in this pull request:
- Broaden illegal format exception caught.

Broaden illegal format exception caught. Fixes #6943