From aa105900bc9103408b9a23dea83e7caff68f7377 Mon Sep 17 00:00:00 2001 From: Oleg Samarin Date: Mon, 1 Jun 2026 13:49:02 +0300 Subject: [PATCH] Fixed ClassNotFoundException on plugin installation/upgrade when the session tab had been previously saved under the old package name The existing migrateWindowsSettings helper did text replacement on .settings files, which fixed the XML class attribute but left the binary serialdata blob intact. That blob (base64-encoded Java serialization) embeds the class name independently, so TopComponent$Replacer.readObject still called Class.forName("io.github.nbclaudecodegui.ui.ClaudeSessionTab") and threw CNFE. Fix: replace the text-replacement call with removeStaleComponentSettings(OLD_PKG), which deletes any .settings file containing the old package prefix. The window system then falls back to the module-layer default (correct class name, no stale binary data). Resolves: #146 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + pom.xml | 2 +- .../claudecodegui/ClaudeCodeInstaller.java | 13 ++-- .../claudecodegui/V1MigrationHelperTest.java | 59 +++++++++++++++++++ 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8c6c4aa..07a38e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +- Fixed ClassNotFoundException on plugin installation/upgrade when the session tab had been previously saved under the old package name (https://github.com/nbplugins/NetbeansPluginClaudeCodeGui/issues/146) - Fixed a Warning dialog appearing on NetBeans exit when a diff tab had been opened during the session # 1.2.8 (2026-05-24) diff --git a/pom.xml b/pom.xml index 951ccbf8..ae7485d2 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbplugins netbeans-plugin-claude-code-gui - 1.2.14-SNAPSHOT + 1.2.15-SNAPSHOT nbm Netbeans Plugin Claude Code GUI NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE. diff --git a/src/main/java/io/github/nbplugins/claudecodegui/ClaudeCodeInstaller.java b/src/main/java/io/github/nbplugins/claudecodegui/ClaudeCodeInstaller.java index 20ae26b1..12048b70 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/ClaudeCodeInstaller.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/ClaudeCodeInstaller.java @@ -48,15 +48,18 @@ public ClaudeCodeInstaller() {} public void restored() { LOGGER.info("Claude Code NetBeans plugin is starting up..."); - // Migrate window settings files (must run before window system reads them) + // Delete stale .settings files from old package names / renamed classes. + // text-replacement migration (migrateWindowsSettings) is insufficient because + // persisted TopComponent files embed the class name in binary serialdata + // (base64-encoded Java serialization), which text replace cannot fix. + // Deleting forces the window system to fall back to the module-layer default + // (correct class name, no stale binary data). String userDir = System.getProperty("netbeans.user"); if (userDir != null) { Path componentsDir = Paths.get(userDir, "config", "Windows2Local", "Components"); - V1MigrationHelper.migrateWindowsSettings(componentsDir); - // FileDiffOpener$1 was renamed to FileDiffOpener$DiffTopComponent in 1.2.13; - // delete stale .settings files so NetBeans does not warn about the unknown class. V1MigrationHelper.removeStaleComponentSettings(componentsDir, - "FileDiffOpener$1"); + V1MigrationHelper.OLD_PKG, // pre-1.0 package rename (issue #146) + "FileDiffOpener$1"); // renamed to FileDiffOpener$DiffTopComponent in 1.2.13 } // Migrate preferences from old package paths (one-time, after package rename in 1.0) diff --git a/src/test/java/io/github/nbplugins/claudecodegui/V1MigrationHelperTest.java b/src/test/java/io/github/nbplugins/claudecodegui/V1MigrationHelperTest.java index 87a10f96..53a01265 100644 --- a/src/test/java/io/github/nbplugins/claudecodegui/V1MigrationHelperTest.java +++ b/src/test/java/io/github/nbplugins/claudecodegui/V1MigrationHelperTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.io.TempDir; import static io.github.nbplugins.claudecodegui.V1MigrationHelper.migratePrefsNode; +import static io.github.nbplugins.claudecodegui.V1MigrationHelper.removeStaleComponentSettings; import static org.junit.jupiter.api.Assertions.*; @@ -79,6 +80,64 @@ void windowsSettings_migratesMarkdownPreviewTab(@TempDir Path dir) throws IOExce assertTrue(result.contains("io.github.nbplugins.claudecodegui.ui.MarkdownPreviewTab")); } + // ── removeStaleComponentSettings ───────────────────────────────────────── + + @Test + void removeStale_deletesFileContainingOldPackage(@TempDir Path dir) throws IOException { + Path file = dir.resolve("ClaudeSessionTopComponent.settings"); + Files.writeString(file, ""); + + removeStaleComponentSettings(dir, V1MigrationHelper.OLD_PKG); + + assertFalse(Files.exists(file), "Stale settings file must be deleted"); + } + + @Test + void removeStale_keepsFileWithoutMatch(@TempDir Path dir) throws IOException { + Path file = dir.resolve("Other.settings"); + Files.writeString(file, ""); + + removeStaleComponentSettings(dir, V1MigrationHelper.OLD_PKG); + + assertTrue(Files.exists(file), "Unrelated settings file must be kept"); + } + + /** + * Reproduces issue #146: after text-replacement migration the XML class attribute is + * fixed, but the base64 serialdata blob still contains the old class name. + * removeStaleComponentSettings must delete such files. + */ + @Test + void removeStale_deletesFileWhereSerialDataContainsOldPackage(@TempDir Path dir) throws IOException { + String content = """ + + + + rO0ABXNyADdpby5naXRodWIubmJjbGF1ZGVjb2RlZ3VpLnVpLkNsYXVkZVNlc3Npb25UYWI= + + + """; + // The serialdata above decodes to bytes that contain "io.github.nbclaudecodegui.ui.ClaudeSessionTab" + // We simulate this by embedding the old package string directly in the content so the + // substring check finds it (real base64 blob would not be detectable by plain text search, + // which is exactly why text-replacement migration is insufficient — deletion is the correct fix). + String contentWithOldPkg = content.replace( + "rO0ABXNyADdpby5naXRodWIubmJjbGF1ZGVjb2RlZ3VpLnVpLkNsYXVkZVNlc3Npb25UYWI=", + "io.github.nbclaudecodegui.ui.ClaudeSessionTab-binary-placeholder"); + Path file = dir.resolve("ClaudeSessionTopComponent.settings"); + Files.writeString(file, contentWithOldPkg); + + removeStaleComponentSettings(dir, V1MigrationHelper.OLD_PKG); + + assertFalse(Files.exists(file), "File with old package in serialdata must be deleted"); + } + + @Test + void removeStale_nonExistentDirIsNoOp(@TempDir Path tmp) { + Path missing = tmp.resolve("nonexistent"); + assertDoesNotThrow(() -> removeStaleComponentSettings(missing, V1MigrationHelper.OLD_PKG)); + } + // ── migratePrefsNode ────────────────────────────────────────────────────── @Test