getProfiles() {
return result;
}
+ /**
+ * Parses a stored Default profile. The whole profile is deserialised (so no field can be
+ * forgotten), then the identity is forced to the built-in Default.
+ *
+ * @param json the stored Default profile JSON, in the current or an older plugin format
+ * @return the Default profile
+ * @throws java.io.IOException if the JSON cannot be parsed
+ */
+ static ClaudeProfile parseDefaultProfile(String json) throws java.io.IOException {
+ ClaudeProfile def = MAPPER.readValue(json, ClaudeProfile.class);
+ def.setId(ClaudeProfile.DEFAULT_ID);
+ def.setName(ClaudeProfile.DEFAULT_NAME);
+ return def;
+ }
+
/**
* Returns the preference-backed Default profile.
*
diff --git a/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanel.java b/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanel.java
index d5e63e3..7ed04bf 100644
--- a/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanel.java
+++ b/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanel.java
@@ -753,26 +753,22 @@ private void flushFormToCurrentProfile() {
p.setToken(new String(tokenField.getPassword()));
p.setApiKey("");
p.setBaseUrl("");
- p.setOpenaiProxy(false);
- p.setOpenaiSubscription(false);
+ p.setConnectionType(ClaudeProfile.ConnectionType.SUBSCRIPTION);
} else if (rbClaudeApi.isSelected()) {
p.setApiKey(new String(apiKeyField.getPassword()));
p.setToken("");
p.setBaseUrl("");
- p.setOpenaiProxy(false);
- p.setOpenaiSubscription(false);
+ p.setConnectionType(ClaudeProfile.ConnectionType.CLAUDE_API);
} else if (rbOtherApi.isSelected()) {
p.setApiKey(new String(apiKeyField.getPassword()));
p.setBaseUrl(baseUrlField.getText().trim());
p.setToken("");
- p.setOpenaiProxy(false);
- p.setOpenaiSubscription(false);
+ p.setConnectionType(ClaudeProfile.ConnectionType.OTHER_API);
} else if (rbOpenAIProxy.isSelected()) {
p.setApiKey(new String(apiKeyField.getPassword()));
p.setBaseUrl(baseUrlField.getText().trim());
p.setToken("");
- p.setOpenaiProxy(true);
- p.setOpenaiSubscription(false);
+ p.setConnectionType(ClaudeProfile.ConnectionType.OPENAI_PROXY);
} else if (rbChatgptSubscription.isSelected()) {
// chatgpt* token fields are written directly by the sign-in/sign-out button
// handlers, not by this text-field flush — they are opaque credentials, not
@@ -780,14 +776,12 @@ private void flushFormToCurrentProfile() {
p.setToken("");
p.setApiKey("");
p.setBaseUrl("");
- p.setOpenaiProxy(false);
- p.setOpenaiSubscription(true);
+ p.setConnectionType(ClaudeProfile.ConnectionType.OPENAI_SUBSCRIPTION);
} else {
p.setToken("");
p.setApiKey("");
p.setBaseUrl("");
- p.setOpenaiProxy(false);
- p.setOpenaiSubscription(false);
+ p.setConnectionType(ClaudeProfile.ConnectionType.CLAUDE_MANAGED);
}
// Clear model aliases/custom models when switching to a connection type that does not
@@ -843,7 +837,7 @@ private void loadProfileIntoForm(ClaudeProfile p) {
refreshStorageDirField(p);
// Connection type
- ClaudeProfile.ConnectionType ct = p.computeConnectionType();
+ ClaudeProfile.ConnectionType ct = p.getConnectionType();
tokenField.setText("");
apiKeyField.setText("");
baseUrlField.setText("");
@@ -920,17 +914,24 @@ private void onNew() {
loadProfileIntoForm(p);
}
- private void onCopy() {
- flushFormToCurrentProfile();
- ClaudeProfile src = currentProfile();
- if (src == null) return;
- String name = promptProfileName("Copy Profile", nextDefaultName(), null);
- if (name == null) return;
- // Deep-copy
+ /**
+ * Creates a named copy of {@code src} with the same connection settings.
+ *
+ * @param src profile to copy
+ * @param name valid name of the new profile
+ * @return the new profile
+ */
+ static ClaudeProfile copyOf(ClaudeProfile src, String name) {
ClaudeProfile copy = ClaudeProfile.createNamed(name);
+ copy.setConnectionType(src.getConnectionType());
copy.setToken(src.getToken());
copy.setApiKey(src.getApiKey());
copy.setBaseUrl(src.getBaseUrl());
+ copy.setChatgptAccessToken(src.getChatgptAccessToken());
+ copy.setChatgptRefreshToken(src.getChatgptRefreshToken());
+ copy.setChatgptAccountId(src.getChatgptAccountId());
+ copy.setChatgptTokenExpiresAt(src.getChatgptTokenExpiresAt());
+ copy.setChatgptEmail(src.getChatgptEmail());
copy.setProxyMode(src.getProxyMode());
copy.setHttpProxy(src.getHttpProxy());
copy.setHttpsProxy(src.getHttpsProxy());
@@ -940,6 +941,16 @@ private void onCopy() {
copy.setCustomModels(src.getCustomModels());
copy.setModelOrder(src.getModelOrder());
copy.setExtraCliArgs(src.getExtraCliArgs());
+ return copy;
+ }
+
+ private void onCopy() {
+ flushFormToCurrentProfile();
+ ClaudeProfile src = currentProfile();
+ if (src == null) return;
+ String name = promptProfileName("Copy Profile", nextDefaultName(), null);
+ if (name == null) return;
+ ClaudeProfile copy = copyOf(src, name);
profiles.add(copy);
suppressProfileChange = true;
rebuildProfileCombo();
@@ -1004,7 +1015,7 @@ private void onModelAliases() {
return i >= 0 ? i : Integer.MAX_VALUE;
}));
ModelAliasesDialog dlg;
- if (p.computeConnectionType() == ClaudeProfile.ConnectionType.OPENAI_SUBSCRIPTION) {
+ if (p.getConnectionType() == ClaudeProfile.ConnectionType.OPENAI_SUBSCRIPTION) {
ModelAliasesDialog.ModelFetcher fetcher = () -> {
String token = new io.github.nbplugins.claudecodegui.chatgptauth.ChatGptTokenManager()
.getValidAccessToken(p);
diff --git a/src/test/java/io/github/nbplugins/claudecodegui/chatgptauth/ChatGptTokenManagerTest.java b/src/test/java/io/github/nbplugins/claudecodegui/chatgptauth/ChatGptTokenManagerTest.java
index ec6d69c..9c98047 100644
--- a/src/test/java/io/github/nbplugins/claudecodegui/chatgptauth/ChatGptTokenManagerTest.java
+++ b/src/test/java/io/github/nbplugins/claudecodegui/chatgptauth/ChatGptTokenManagerTest.java
@@ -36,7 +36,7 @@ void setUp() {
private static ClaudeProfile signedInProfile(Instant expiresAt) {
ClaudeProfile p = ClaudeProfile.createNamed("chatgpt-test-" + System.nanoTime());
- p.setOpenaiSubscription(true);
+ p.setConnectionType(ClaudeProfile.ConnectionType.OPENAI_SUBSCRIPTION);
p.setChatgptAccessToken("access-old");
p.setChatgptRefreshToken("refresh-old");
p.setChatgptAccountId("acct-1");
@@ -98,7 +98,7 @@ void getValidAccessToken_withinSkewOfExpiry_refreshesAndPersists() throws Except
@Test
void getValidAccessToken_noRecordedExpiry_refreshes() throws Exception {
ClaudeProfile profile = ClaudeProfile.createNamed("chatgpt-test-noexp-" + System.nanoTime());
- profile.setOpenaiSubscription(true);
+ profile.setConnectionType(ClaudeProfile.ConnectionType.OPENAI_SUBSCRIPTION);
profile.setChatgptAccessToken("access-old");
profile.setChatgptRefreshToken("refresh-old");
// chatgptTokenExpiresAt left blank
diff --git a/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfileStoreConnectionTypeTest.java b/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfileStoreConnectionTypeTest.java
new file mode 100644
index 0000000..95d5542
--- /dev/null
+++ b/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfileStoreConnectionTypeTest.java
@@ -0,0 +1,140 @@
+package io.github.nbplugins.claudecodegui.settings;
+
+import io.github.nbplugins.claudecodegui.settings.ClaudeProfile.ConnectionType;
+import java.util.List;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.openide.util.NbPreferences;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Reading the connection type of the Default and of a custom profile from settings
+ * written by older plugin versions (no {@code connectionType}, legacy
+ * {@code openaiProxy}/{@code openaiSubscription} flags) and by the current version
+ * (explicit {@code connectionType}); issue #191.
+ *
+ * Profiles are stored as JSON strings in {@link NbPreferences} and read back through
+ * {@link ClaudeProfileStore#getProfiles()}, the production loading path.
+ */
+class ClaudeProfileStoreConnectionTypeTest {
+
+ private static final String KEY_DEFAULT = "defaultProfile"; // ClaudeProfileStore.KEY_DEFAULT_PROFILE (private)
+
+ @BeforeEach
+ @AfterEach
+ void clean() {
+ NbPreferences.forModule(ClaudeProfileStore.class).remove(KEY_DEFAULT);
+ ClaudeProfileStore.clearProfiles();
+ }
+
+ /** {name, JSON body fields, expected type}. */
+ static Stream settings() {
+ return Stream.of(
+ // --- old settings: no connectionType, legacy flags + credentials ---
+ Arguments.of("old managed", "", ConnectionType.CLAUDE_MANAGED),
+ Arguments.of("old subscription", "\"token\":\"t\"", ConnectionType.SUBSCRIPTION),
+ Arguments.of("old claude api", "\"apiKey\":\"k\"", ConnectionType.CLAUDE_API),
+ Arguments.of("old other api", "\"apiKey\":\"k\",\"baseUrl\":\"https://x/v1\"",
+ ConnectionType.OTHER_API),
+ Arguments.of("old openai proxy (issue 191)",
+ "\"openaiProxy\":true,\"apiKey\":\"k\",\"baseUrl\":\"https://x/v1\"",
+ ConnectionType.OPENAI_PROXY),
+ Arguments.of("old chatgpt subscription",
+ "\"openaiSubscription\":true,\"chatgptAccessToken\":\"a\",\"chatgptRefreshToken\":\"r\"",
+ ConnectionType.OPENAI_SUBSCRIPTION),
+ Arguments.of("old, both openai flags", "\"openaiProxy\":true,\"openaiSubscription\":true",
+ ConnectionType.OPENAI_SUBSCRIPTION),
+ Arguments.of("old, apiKey and token", "\"apiKey\":\"k\",\"token\":\"t\"",
+ ConnectionType.CLAUDE_API),
+ Arguments.of("old, explicit false flags", "\"openaiProxy\":false,\"openaiSubscription\":false",
+ ConnectionType.CLAUDE_MANAGED),
+ // --- new settings: explicit connectionType ---
+ Arguments.of("new managed", "\"connectionType\":\"CLAUDE_MANAGED\"", ConnectionType.CLAUDE_MANAGED),
+ Arguments.of("new subscription", "\"connectionType\":\"SUBSCRIPTION\",\"token\":\"t\"",
+ ConnectionType.SUBSCRIPTION),
+ Arguments.of("new claude api", "\"connectionType\":\"CLAUDE_API\",\"apiKey\":\"k\"",
+ ConnectionType.CLAUDE_API),
+ Arguments.of("new other api",
+ "\"connectionType\":\"OTHER_API\",\"apiKey\":\"k\",\"baseUrl\":\"https://x/v1\"",
+ ConnectionType.OTHER_API),
+ Arguments.of("new openai proxy",
+ "\"connectionType\":\"OPENAI_PROXY\",\"apiKey\":\"k\",\"baseUrl\":\"https://x/v1\"",
+ ConnectionType.OPENAI_PROXY),
+ Arguments.of("new chatgpt subscription", "\"connectionType\":\"OPENAI_SUBSCRIPTION\"",
+ ConnectionType.OPENAI_SUBSCRIPTION),
+ Arguments.of("new wins over stale legacy flags",
+ "\"connectionType\":\"CLAUDE_API\",\"apiKey\":\"k\",\"openaiProxy\":true",
+ ConnectionType.CLAUDE_API),
+ Arguments.of("unknown future type falls back to legacy derivation",
+ "\"connectionType\":\"SOMETHING_NEW\",\"apiKey\":\"k\",\"baseUrl\":\"https://x/v1\"",
+ ConnectionType.OTHER_API));
+ }
+
+ @ParameterizedTest(name = "Default: {0}")
+ @MethodSource("settings")
+ void defaultProfile_readsConnectionType(String name, String fields, ConnectionType expected) {
+ NbPreferences.forModule(ClaudeProfileStore.class).put(KEY_DEFAULT, "{" + fields + "}");
+
+ ClaudeProfile loaded = ClaudeProfileStore.getProfiles().get(0);
+
+ assertTrue(loaded.isDefault());
+ assertEquals("Default", loaded.getName());
+ assertEquals(expected, loaded.getConnectionType());
+ }
+
+ @ParameterizedTest(name = "Custom: {0}")
+ @MethodSource("settings")
+ void customProfile_readsConnectionType(String name, String fields, ConnectionType expected) {
+ String body = "\"name\":\"Work\",\"id\":\"Work\"" + (fields.isEmpty() ? "" : "," + fields);
+ NbPreferences.forModule(ClaudeProfileStore.class).put(ClaudeProfileStore.KEY_PROFILES, "[{" + body + "}]");
+
+ List profiles = ClaudeProfileStore.getProfiles();
+
+ assertEquals(2, profiles.size());
+ assertEquals("Work", profiles.get(1).getName());
+ assertEquals(expected, profiles.get(1).getConnectionType());
+ }
+
+ @Test
+ void defaultProfile_oldOpenaiProxy_keepsCredentials() {
+ NbPreferences.forModule(ClaudeProfileStore.class).put(KEY_DEFAULT,
+ "{\"openaiProxy\":true,\"apiKey\":\"sk\",\"baseUrl\":\"https://x/v1\","
+ + "\"proxyMode\":\"CUSTOM\",\"httpProxy\":\"http://p:3128\","
+ + "\"modelAliases\":{\"sonnet\":\"gpt-x\"},\"extraCliArgs\":\"--verbose\"}");
+
+ ClaudeProfile loaded = ClaudeProfileStore.getProfiles().get(0);
+
+ assertEquals("sk", loaded.getApiKey());
+ assertEquals("https://x/v1", loaded.getBaseUrl());
+ assertEquals(ClaudeProfile.ProxyMode.CUSTOM, loaded.getProxyMode());
+ assertEquals("http://p:3128", loaded.getHttpProxy());
+ assertEquals("gpt-x", loaded.getModelAliases().get("sonnet"));
+ assertEquals("--verbose", loaded.getExtraCliArgs());
+ assertEquals("", loaded.getId());
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("settings")
+ void write_alwaysUsesNewFormat_andIsLossless(String name, String fields, ConnectionType expected) throws Exception {
+ NbPreferences.forModule(ClaudeProfileStore.class).put(KEY_DEFAULT, "{" + fields + "}");
+ ClaudeProfile loaded = ClaudeProfileStore.getProfiles().get(0);
+
+ ClaudeProfileStore.saveProfiles(List.of(loaded));
+ String written = NbPreferences.forModule(ClaudeProfileStore.class).get(KEY_DEFAULT, null);
+
+ assertNotNull(written);
+ assertTrue(written.contains("\"connectionType\":\"" + expected.name() + "\""), written);
+ assertFalse(written.contains("openaiProxy"), written);
+ assertFalse(written.contains("openaiSubscription"), written);
+ ClaudeProfile reloaded = ClaudeProfileStore.getProfiles().get(0);
+ assertEquals(expected, reloaded.getConnectionType());
+ assertEquals(loaded.getApiKey(), reloaded.getApiKey());
+ assertEquals(loaded.getBaseUrl(), reloaded.getBaseUrl());
+ assertEquals(loaded.getToken(), reloaded.getToken());
+ }
+}
diff --git a/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfileStoreTest.java b/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfileStoreTest.java
index c2a0908..39d2e9d 100644
--- a/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfileStoreTest.java
+++ b/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfileStoreTest.java
@@ -179,6 +179,51 @@ void saveAndReload_namedProfile_persistsCorrectly() {
}
}
+ // -------------------------------------------------------------------------
+ // Connection type persistence (issue #191)
+ // -------------------------------------------------------------------------
+
+ @org.junit.jupiter.api.AfterEach
+ void removeStoredDefaultProfile() {
+ org.openide.util.NbPreferences.forModule(ClaudeProfileStore.class)
+ .remove("defaultProfile"); // ClaudeProfileStore.KEY_DEFAULT_PROFILE (private)
+ ClaudeProfileStore.clearProfiles();
+ }
+
+ /**
+ * Regression test for issue #191: "OpenAI-compatible API" selected on the Default
+ * profile was reset to "Claude-compatible API" after reload, because the Default
+ * profile is rebuilt field by field and {@code openaiProxy} was not copied.
+ */
+ @Test
+ void defaultProfile_openaiProxy_survivesSaveAndReload() {
+ ClaudeProfile def = ClaudeProfile.createDefault();
+ def.setConnectionType(ClaudeProfile.ConnectionType.OPENAI_PROXY);
+ def.setApiKey("sk-openai");
+ def.setBaseUrl("https://api.example.com/v1");
+ ClaudeProfileStore.saveProfiles(List.of(def));
+
+ ClaudeProfile loaded = ClaudeProfileStore.getProfiles().get(0);
+ assertTrue(loaded.isDefault());
+ assertEquals(ClaudeProfile.ConnectionType.OPENAI_PROXY, loaded.getConnectionType());
+ assertEquals("sk-openai", loaded.getApiKey());
+ assertEquals("https://api.example.com/v1", loaded.getBaseUrl());
+ }
+
+ /** Companion: the same settings on a named profile were never affected by the bug. */
+ @Test
+ void namedProfile_openaiProxy_survivesSaveAndReload() {
+ ClaudeProfile p = ClaudeProfile.createNamed("OpenAI");
+ p.setConnectionType(ClaudeProfile.ConnectionType.OPENAI_PROXY);
+ p.setApiKey("sk-openai");
+ p.setBaseUrl("https://api.example.com/v1");
+ ClaudeProfileStore.saveProfiles(List.of(ClaudeProfile.createDefault(), p));
+
+ ClaudeProfile loaded = ClaudeProfileStore.findByName("OpenAI");
+ assertEquals("OpenAI", loaded.getName());
+ assertEquals(ClaudeProfile.ConnectionType.OPENAI_PROXY, loaded.getConnectionType());
+ }
+
// -------------------------------------------------------------------------
// saveProfiles — skips Default entries
// -------------------------------------------------------------------------
diff --git a/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfileTest.java b/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfileTest.java
index b7cf60c..b02c029 100644
--- a/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfileTest.java
+++ b/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfileTest.java
@@ -27,7 +27,7 @@ void createDefault_hasBlankId() {
@Test
void createDefault_connectionTypeIsClaudeManaged() {
assertEquals(ConnectionType.CLAUDE_MANAGED,
- ClaudeProfile.createDefault().computeConnectionType());
+ ClaudeProfile.createDefault().getConnectionType());
}
// -------------------------------------------------------------------------
@@ -90,60 +90,48 @@ void validateName_valid_returnsNull() {
// -------------------------------------------------------------------------
@Test
- void computeConnectionType_emptyCredentials_isClaudeManaged() {
+ void getConnectionType_emptyCredentials_isClaudeManaged() {
ClaudeProfile p = ClaudeProfile.createNamed("P");
- assertEquals(ConnectionType.CLAUDE_MANAGED, p.computeConnectionType());
+ assertEquals(ConnectionType.CLAUDE_MANAGED, p.getConnectionType());
}
@Test
- void computeConnectionType_tokenOnly_isSubscription() {
+ void getConnectionType_tokenOnly_isSubscription() {
ClaudeProfile p = ClaudeProfile.createNamed("P");
p.setToken("tok123");
- assertEquals(ConnectionType.SUBSCRIPTION, p.computeConnectionType());
+ assertEquals(ConnectionType.SUBSCRIPTION, p.getConnectionType());
}
@Test
- void computeConnectionType_apiKeyOnly_isClaudeApi() {
+ void getConnectionType_apiKeyOnly_isClaudeApi() {
ClaudeProfile p = ClaudeProfile.createNamed("P");
p.setApiKey("sk-ant-123");
- assertEquals(ConnectionType.CLAUDE_API, p.computeConnectionType());
+ assertEquals(ConnectionType.CLAUDE_API, p.getConnectionType());
}
@Test
- void computeConnectionType_apiKeyAndBaseUrl_isOtherApi() {
+ void getConnectionType_apiKeyAndBaseUrl_isOtherApi() {
ClaudeProfile p = ClaudeProfile.createNamed("P");
p.setApiKey("sk-ant-123");
p.setBaseUrl("https://api.example.com");
- assertEquals(ConnectionType.OTHER_API, p.computeConnectionType());
+ assertEquals(ConnectionType.OTHER_API, p.getConnectionType());
}
@Test
- void computeConnectionType_apiKeyTakesPrecedenceOverToken() {
+ void getConnectionType_apiKeyTakesPrecedenceOverToken() {
ClaudeProfile p = ClaudeProfile.createNamed("P");
p.setToken("tok");
p.setApiKey("key");
- assertEquals(ConnectionType.CLAUDE_API, p.computeConnectionType());
+ assertEquals(ConnectionType.CLAUDE_API, p.getConnectionType());
}
@Test
- void computeConnectionType_openaiSubscription_isOpenaiSubscription() {
+ void getConnectionType_openaiSubscription_isOpenaiSubscription() {
ClaudeProfile p = ClaudeProfile.createNamed("P");
- p.setOpenaiSubscription(true);
- assertEquals(ConnectionType.OPENAI_SUBSCRIPTION, p.computeConnectionType());
+ p.setConnectionType(ClaudeProfile.ConnectionType.OPENAI_SUBSCRIPTION);
+ assertEquals(ConnectionType.OPENAI_SUBSCRIPTION, p.getConnectionType());
}
- @Test
- void computeConnectionType_openaiSubscriptionTakesPrecedenceOverOpenaiProxy() {
- ClaudeProfile p = ClaudeProfile.createNamed("P");
- p.setOpenaiProxy(true);
- p.setOpenaiSubscription(true);
- assertEquals(ConnectionType.OPENAI_SUBSCRIPTION, p.computeConnectionType());
- }
-
- // -------------------------------------------------------------------------
- // ChatGPT OAuth state
- // -------------------------------------------------------------------------
-
@Test
void isSignedIntoChatgpt_falseByDefault() {
ClaudeProfile p = ClaudeProfile.createNamed("P");
@@ -168,7 +156,7 @@ void isSignedIntoChatgpt_falseWhenOnlyAccessTokenPresent() {
@Test
void clearChatgptAuth_clearsAllTokenFieldsButNotFlag() {
ClaudeProfile p = ClaudeProfile.createNamed("P");
- p.setOpenaiSubscription(true);
+ p.setConnectionType(ClaudeProfile.ConnectionType.OPENAI_SUBSCRIPTION);
p.setChatgptAccessToken("access");
p.setChatgptRefreshToken("refresh");
p.setChatgptAccountId("acct-1");
@@ -183,7 +171,7 @@ void clearChatgptAuth_clearsAllTokenFieldsButNotFlag() {
assertEquals("", p.getChatgptAccountId());
assertEquals("", p.getChatgptTokenExpiresAt());
assertEquals("", p.getChatgptEmail());
- assertTrue(p.isOpenaiSubscription(), "clearChatgptAuth must not change the connection-type flag");
+ assertEquals(ConnectionType.OPENAI_SUBSCRIPTION, p.getConnectionType(), "clearChatgptAuth must not change the connection type");
}
// -------------------------------------------------------------------------
@@ -222,7 +210,7 @@ void toEnvVars_claudeApi_doesNotInjectApiKey() {
@Test
void toEnvVars_openaiSubscription_noAuthVars() {
ClaudeProfile p = ClaudeProfile.createNamed("P");
- p.setOpenaiSubscription(true);
+ p.setConnectionType(ClaudeProfile.ConnectionType.OPENAI_SUBSCRIPTION);
Map env = p.toEnvVars();
assertFalse(env.containsKey("ANTHROPIC_AUTH_TOKEN"));
assertFalse(env.containsKey("ANTHROPIC_BASE_URL"));
@@ -470,7 +458,7 @@ void toEnvVars_doesNotInjectModelAliasesForManagedConnection() {
p.setModelAliases(Map.of("sonnet", "old-model-id"));
p.setCustomModels(List.of("old-custom-model"));
- assertEquals(ConnectionType.CLAUDE_MANAGED, p.computeConnectionType());
+ assertEquals(ConnectionType.CLAUDE_MANAGED, p.getConnectionType());
Map env = p.toEnvVars();
assertFalse(env.containsKey("ANTHROPIC_DEFAULT_SONNET_MODEL"),
@@ -484,7 +472,7 @@ void toEnvVars_injectsModelAliasesForOtherApiConnection() {
p.setBaseUrl("https://example.com");
p.setModelAliases(Map.of("sonnet", "my-sonnet-model"));
- assertEquals(ConnectionType.OTHER_API, p.computeConnectionType());
+ assertEquals(ConnectionType.OTHER_API, p.getConnectionType());
Map env = p.toEnvVars();
assertEquals("my-sonnet-model", env.get("ANTHROPIC_DEFAULT_SONNET_MODEL"),
@@ -496,10 +484,10 @@ void toEnvVars_injectsModelAliasesForOpenaiSubscriptionConnection() {
// Without these, subagents (haiku) and side requests (sonnet) reach the
// ChatGPT Codex backend as claude-* model ids and fail with HTTP 400.
ClaudeProfile p = ClaudeProfile.createNamed("Test");
- p.setOpenaiSubscription(true);
+ p.setConnectionType(ClaudeProfile.ConnectionType.OPENAI_SUBSCRIPTION);
p.setModelAliases(Map.of("haiku", "gpt-5.6-luna", "sonnet", "gpt-5.6-terra"));
- assertEquals(ConnectionType.OPENAI_SUBSCRIPTION, p.computeConnectionType());
+ assertEquals(ConnectionType.OPENAI_SUBSCRIPTION, p.getConnectionType());
Map env = p.toEnvVars();
assertEquals("gpt-5.6-luna", env.get("ANTHROPIC_DEFAULT_HAIKU_MODEL"));
@@ -511,7 +499,7 @@ void getComboModelIds_followsModelAliasesDialogOrder() {
// Models mapped to sonnet/opus/haiku stay selectable by their own id, in the
// order of the rows in the Model Aliases dialog
ClaudeProfile p = ClaudeProfile.createNamed("Test");
- p.setOpenaiSubscription(true);
+ p.setConnectionType(ClaudeProfile.ConnectionType.OPENAI_SUBSCRIPTION);
p.setModelAliases(Map.of("sonnet", "gpt-5.6-terra", "opus", "gpt-5.6-terra", "haiku", "gpt-5.6-luna"));
p.setCustomModels(List.of("gpt-5.6-sol", "gpt-5.5"));
p.setModelOrder(List.of("gpt-5.6-sol", "gpt-5.6-luna", "gpt-5.5", "gpt-5.6-terra"));
@@ -522,7 +510,7 @@ void getComboModelIds_followsModelAliasesDialogOrder() {
@Test
void getComboModelIds_skipsOrderEntriesNoLongerAliased_andAppendsUnorderedOnes() {
ClaudeProfile p = ClaudeProfile.createNamed("Test");
- p.setOpenaiSubscription(true);
+ p.setConnectionType(ClaudeProfile.ConnectionType.OPENAI_SUBSCRIPTION);
p.setModelAliases(Map.of("haiku", "gpt-5.6-luna"));
p.setCustomModels(List.of("gpt-5.6-sol"));
p.setModelOrder(List.of("removed-model", "gpt-5.6-sol"));
@@ -533,7 +521,7 @@ void getComboModelIds_skipsOrderEntriesNoLongerAliased_andAppendsUnorderedOnes()
@Test
void getComboModelIds_legacyProfileWithoutOrder_aliasedThenCustom_withoutDuplicates() {
ClaudeProfile p = ClaudeProfile.createNamed("Test");
- p.setOpenaiSubscription(true);
+ p.setConnectionType(ClaudeProfile.ConnectionType.OPENAI_SUBSCRIPTION);
p.setModelAliases(Map.of("sonnet", "gpt-5.6-terra", "opus", "gpt-5.6-terra", "haiku", "gpt-5.6-luna"));
p.setCustomModels(List.of("gpt-5.6-sol", "gpt-5.6-luna"));
@@ -551,7 +539,7 @@ void jsonRoundTrip_withAliasesAndCustomModels_keepsModelOrder_andDoesNotStoreCom
// on load, Jackson tried to fill the immutable list it returns → every profile
// except Default failed to load.
ClaudeProfile p = ClaudeProfile.createNamed("Test");
- p.setOpenaiSubscription(true);
+ p.setConnectionType(ClaudeProfile.ConnectionType.OPENAI_SUBSCRIPTION);
p.setModelAliases(Map.of("haiku", "gpt-5.6-luna"));
p.setCustomModels(List.of("gpt-5.6-sol"));
p.setModelOrder(List.of("gpt-5.6-sol", "gpt-5.6-luna"));
diff --git a/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanelFlushTest.java b/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanelFlushTest.java
index 47693f4..14290a2 100644
--- a/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanelFlushTest.java
+++ b/src/test/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanelFlushTest.java
@@ -155,9 +155,8 @@ void flush_chatgptSubscription_setsFlagAndClearsOtherAuthFields() throws Excepti
invoke(panel, "flushFormToCurrentProfile");
- assertTrue(work.isOpenaiSubscription());
- assertFalse(work.isOpenaiProxy());
- assertEquals("", work.getApiKey());
+ assertEquals(ClaudeProfile.ConnectionType.OPENAI_SUBSCRIPTION, work.getConnectionType());
+ assertEquals("", work.getApiKey());
assertEquals("", work.getBaseUrl());
assertEquals("", work.getToken());
}
@@ -190,7 +189,7 @@ void flush_chatgptSubscription_doesNotAutoPopulateCustomModels() throws Exceptio
@Test
void flush_switchingAwayFromChatgptSubscription_clearsFlag() throws Exception {
ClaudeProfile work = ClaudeProfile.createNamed("Work");
- work.setOpenaiSubscription(true);
+ work.setConnectionType(ClaudeProfile.ConnectionType.OPENAI_SUBSCRIPTION);
work.setChatgptAccessToken("access");
work.setChatgptRefreshToken("refresh");
@@ -204,7 +203,7 @@ void flush_switchingAwayFromChatgptSubscription_clearsFlag() throws Exception {
invoke(panel, "flushFormToCurrentProfile");
- assertFalse(work.isOpenaiSubscription());
+ assertNotEquals(ClaudeProfile.ConnectionType.OPENAI_SUBSCRIPTION, work.getConnectionType());
// Switching connection type does not sign the user out — chatgpt* tokens
// are only cleared explicitly via the "Sign out" button.
assertTrue(work.isSignedIntoChatgpt());
@@ -248,4 +247,38 @@ private static void invoke(Object obj, String name, Class> paramType, Object a
m.setAccessible(true);
m.invoke(obj, arg);
}
+
+ /** Copy Profile must keep the explicitly selected connection type (PR #194 review). */
+ @org.junit.jupiter.params.ParameterizedTest
+ @org.junit.jupiter.params.provider.EnumSource(ClaudeProfile.ConnectionType.class)
+ void copyOf_preservesConnectionType(ClaudeProfile.ConnectionType type) {
+ ClaudeProfile src = ClaudeProfile.createNamed("Src");
+ src.setConnectionType(type); // no credentials: not inferable from the fields
+
+ ClaudeProfile copy = ClaudeProfilesPanel.copyOf(src, "Copy");
+
+ assertEquals("Copy", copy.getName());
+ assertEquals(type, copy.getConnectionType());
+ }
+
+ /** Copy of a signed-in ChatGPT Subscription profile must stay signed in (PR #194 review). */
+ @org.junit.jupiter.api.Test
+ void copyOf_preservesChatgptAuth() {
+ ClaudeProfile src = ClaudeProfile.createNamed("Src");
+ src.setConnectionType(ClaudeProfile.ConnectionType.OPENAI_SUBSCRIPTION);
+ src.setChatgptAccessToken("access");
+ src.setChatgptRefreshToken("refresh");
+ src.setChatgptAccountId("acc-1");
+ src.setChatgptTokenExpiresAt("2026-10-16T20:00:00Z");
+ src.setChatgptEmail("me@example.com");
+
+ ClaudeProfile copy = ClaudeProfilesPanel.copyOf(src, "Copy");
+
+ assertEquals("access", copy.getChatgptAccessToken());
+ assertEquals("refresh", copy.getChatgptRefreshToken());
+ assertEquals("acc-1", copy.getChatgptAccountId());
+ assertEquals("2026-10-16T20:00:00Z", copy.getChatgptTokenExpiresAt());
+ assertEquals("me@example.com", copy.getChatgptEmail());
+ assertTrue(copy.isSignedIntoChatgpt());
+ }
}