fix: clean up deleted provider state correctly (#696)
This commit is contained in:
committed by
GitHub
Unverified
parent
07f3c310b5
commit
9b56d80d22
@@ -36,6 +36,11 @@ async function writeOpenClawJson(config: unknown): Promise<void> {
|
||||
await writeFile(join(openclawDir, 'openclaw.json'), JSON.stringify(config, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
async function readOpenClawJson(): Promise<Record<string, unknown>> {
|
||||
const content = await readFile(join(testHome, '.openclaw', 'openclaw.json'), 'utf8');
|
||||
return JSON.parse(content) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
async function readAuthProfiles(agentId: string): Promise<Record<string, unknown>> {
|
||||
const content = await readFile(join(testHome, '.openclaw', 'agents', agentId, 'agent', 'auth-profiles.json'), 'utf8');
|
||||
return JSON.parse(content) as Record<string, unknown>;
|
||||
@@ -118,6 +123,188 @@ describe('saveProviderKeyToOpenClaw', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeProviderKeyFromOpenClaw', () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.restoreAllMocks();
|
||||
await rm(testHome, { recursive: true, force: true });
|
||||
await rm(testUserData, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('removes only the default api-key profile for a provider', async () => {
|
||||
await writeAgentAuthProfiles('main', {
|
||||
version: 1,
|
||||
profiles: {
|
||||
'custom-abc12345:default': {
|
||||
type: 'api_key',
|
||||
provider: 'custom-abc12345',
|
||||
key: 'sk-main',
|
||||
},
|
||||
'custom-abc12345:backup': {
|
||||
type: 'api_key',
|
||||
provider: 'custom-abc12345',
|
||||
key: 'sk-backup',
|
||||
},
|
||||
},
|
||||
order: {
|
||||
'custom-abc12345': [
|
||||
'custom-abc12345:default',
|
||||
'custom-abc12345:backup',
|
||||
],
|
||||
},
|
||||
lastGood: {
|
||||
'custom-abc12345': 'custom-abc12345:default',
|
||||
},
|
||||
});
|
||||
|
||||
const { removeProviderKeyFromOpenClaw } = await import('@electron/utils/openclaw-auth');
|
||||
|
||||
await removeProviderKeyFromOpenClaw('custom-abc12345', 'main');
|
||||
|
||||
const mainProfiles = await readAuthProfiles('main');
|
||||
expect(mainProfiles.profiles).toEqual({
|
||||
'custom-abc12345:backup': {
|
||||
type: 'api_key',
|
||||
provider: 'custom-abc12345',
|
||||
key: 'sk-backup',
|
||||
},
|
||||
});
|
||||
expect(mainProfiles.order).toEqual({
|
||||
'custom-abc12345': ['custom-abc12345:backup'],
|
||||
});
|
||||
expect(mainProfiles.lastGood).toEqual({});
|
||||
});
|
||||
|
||||
it('cleans stale default-profile references even when the profile object is already missing', async () => {
|
||||
await writeAgentAuthProfiles('main', {
|
||||
version: 1,
|
||||
profiles: {
|
||||
'custom-abc12345:backup': {
|
||||
type: 'api_key',
|
||||
provider: 'custom-abc12345',
|
||||
key: 'sk-backup',
|
||||
},
|
||||
},
|
||||
order: {
|
||||
'custom-abc12345': [
|
||||
'custom-abc12345:default',
|
||||
'custom-abc12345:backup',
|
||||
],
|
||||
},
|
||||
lastGood: {
|
||||
'custom-abc12345': 'custom-abc12345:default',
|
||||
},
|
||||
});
|
||||
|
||||
const { removeProviderKeyFromOpenClaw } = await import('@electron/utils/openclaw-auth');
|
||||
|
||||
await removeProviderKeyFromOpenClaw('custom-abc12345', 'main');
|
||||
|
||||
const mainProfiles = await readAuthProfiles('main');
|
||||
expect(mainProfiles.profiles).toEqual({
|
||||
'custom-abc12345:backup': {
|
||||
type: 'api_key',
|
||||
provider: 'custom-abc12345',
|
||||
key: 'sk-backup',
|
||||
},
|
||||
});
|
||||
expect(mainProfiles.order).toEqual({
|
||||
'custom-abc12345': ['custom-abc12345:backup'],
|
||||
});
|
||||
expect(mainProfiles.lastGood).toEqual({});
|
||||
});
|
||||
|
||||
it('does not remove oauth default profiles when deleting only an api key', async () => {
|
||||
await writeAgentAuthProfiles('main', {
|
||||
version: 1,
|
||||
profiles: {
|
||||
'openai-codex:default': {
|
||||
type: 'oauth',
|
||||
provider: 'openai-codex',
|
||||
access: 'acc',
|
||||
refresh: 'ref',
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
'openai-codex': ['openai-codex:default'],
|
||||
},
|
||||
lastGood: {
|
||||
'openai-codex': 'openai-codex:default',
|
||||
},
|
||||
});
|
||||
|
||||
const { removeProviderKeyFromOpenClaw } = await import('@electron/utils/openclaw-auth');
|
||||
|
||||
await removeProviderKeyFromOpenClaw('openai-codex', 'main');
|
||||
|
||||
const mainProfiles = await readAuthProfiles('main');
|
||||
expect(mainProfiles.profiles).toEqual({
|
||||
'openai-codex:default': {
|
||||
type: 'oauth',
|
||||
provider: 'openai-codex',
|
||||
access: 'acc',
|
||||
refresh: 'ref',
|
||||
expires: 1,
|
||||
},
|
||||
});
|
||||
expect(mainProfiles.order).toEqual({
|
||||
'openai-codex': ['openai-codex:default'],
|
||||
});
|
||||
expect(mainProfiles.lastGood).toEqual({
|
||||
'openai-codex': 'openai-codex:default',
|
||||
});
|
||||
});
|
||||
|
||||
it('removes api-key defaults for oauth-capable providers that support api keys', async () => {
|
||||
await writeAgentAuthProfiles('main', {
|
||||
version: 1,
|
||||
profiles: {
|
||||
'minimax-portal:default': {
|
||||
type: 'api_key',
|
||||
provider: 'minimax-portal',
|
||||
key: 'sk-minimax',
|
||||
},
|
||||
'minimax-portal:oauth-backup': {
|
||||
type: 'oauth',
|
||||
provider: 'minimax-portal',
|
||||
access: 'acc',
|
||||
refresh: 'ref',
|
||||
expires: 1,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
'minimax-portal': [
|
||||
'minimax-portal:default',
|
||||
'minimax-portal:oauth-backup',
|
||||
],
|
||||
},
|
||||
lastGood: {
|
||||
'minimax-portal': 'minimax-portal:default',
|
||||
},
|
||||
});
|
||||
|
||||
const { removeProviderKeyFromOpenClaw } = await import('@electron/utils/openclaw-auth');
|
||||
|
||||
await removeProviderKeyFromOpenClaw('minimax-portal', 'main');
|
||||
|
||||
const mainProfiles = await readAuthProfiles('main');
|
||||
expect(mainProfiles.profiles).toEqual({
|
||||
'minimax-portal:oauth-backup': {
|
||||
type: 'oauth',
|
||||
provider: 'minimax-portal',
|
||||
access: 'acc',
|
||||
refresh: 'ref',
|
||||
expires: 1,
|
||||
},
|
||||
});
|
||||
expect(mainProfiles.order).toEqual({
|
||||
'minimax-portal': ['minimax-portal:oauth-backup'],
|
||||
});
|
||||
expect(mainProfiles.lastGood).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeOpenClawConfig', () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
@@ -292,4 +479,86 @@ describe('auth-backed provider discovery', () => {
|
||||
anthropic: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('removes all matching auth profiles for a deleted provider so it does not reappear', async () => {
|
||||
await writeOpenClawJson({
|
||||
agents: {
|
||||
list: [
|
||||
{ id: 'main', name: 'Main', default: true, workspace: '~/.openclaw/workspace', agentDir: '~/.openclaw/agents/main/agent' },
|
||||
{ id: 'work', name: 'Work', workspace: '~/.openclaw/workspace-work', agentDir: '~/.openclaw/agents/work/agent' },
|
||||
],
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
'custom-abc12345': {
|
||||
baseUrl: 'https://api.moonshot.cn/v1',
|
||||
api: 'openai-completions',
|
||||
},
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
profiles: {
|
||||
'custom-abc12345:oauth': {
|
||||
type: 'oauth',
|
||||
provider: 'custom-abc12345',
|
||||
access: 'acc',
|
||||
refresh: 'ref',
|
||||
expires: 1,
|
||||
},
|
||||
'custom-abc12345:secondary': {
|
||||
type: 'api_key',
|
||||
provider: 'custom-abc12345',
|
||||
key: 'sk-inline',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await writeAgentAuthProfiles('main', {
|
||||
version: 1,
|
||||
profiles: {
|
||||
'custom-abc12345:default': {
|
||||
type: 'api_key',
|
||||
provider: 'custom-abc12345',
|
||||
key: 'sk-main',
|
||||
},
|
||||
'custom-abc12345:backup': {
|
||||
type: 'api_key',
|
||||
provider: 'custom-abc12345',
|
||||
key: 'sk-backup',
|
||||
},
|
||||
},
|
||||
order: {
|
||||
'custom-abc12345': [
|
||||
'custom-abc12345:default',
|
||||
'custom-abc12345:backup',
|
||||
],
|
||||
},
|
||||
lastGood: {
|
||||
'custom-abc12345': 'custom-abc12345:backup',
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
getActiveOpenClawProviders,
|
||||
getOpenClawProvidersConfig,
|
||||
removeProviderFromOpenClaw,
|
||||
} = await import('@electron/utils/openclaw-auth');
|
||||
|
||||
await expect(getActiveOpenClawProviders()).resolves.toEqual(new Set(['custom-abc12345']));
|
||||
|
||||
await removeProviderFromOpenClaw('custom-abc12345');
|
||||
|
||||
const mainProfiles = await readAuthProfiles('main');
|
||||
const config = await readOpenClawJson();
|
||||
const result = await getOpenClawProvidersConfig();
|
||||
|
||||
expect(mainProfiles.profiles).toEqual({});
|
||||
expect(mainProfiles.order).toEqual({});
|
||||
expect(mainProfiles.lastGood).toEqual({});
|
||||
expect((config.auth as { profiles?: Record<string, unknown> }).profiles).toEqual({});
|
||||
expect((config.models as { providers?: Record<string, unknown> }).providers).toEqual({});
|
||||
expect(result.providers).toEqual({});
|
||||
await expect(getActiveOpenClawProviders()).resolves.toEqual(new Set());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ const mocks = vi.hoisted(() => ({
|
||||
getProviderConfig: vi.fn(),
|
||||
getProviderDefaultModel: vi.fn(),
|
||||
removeProviderFromOpenClaw: vi.fn(),
|
||||
removeProviderKeyFromOpenClaw: vi.fn(),
|
||||
saveOAuthTokenToOpenClaw: vi.fn(),
|
||||
saveProviderKeyToOpenClaw: vi.fn(),
|
||||
setOpenClawDefaultModel: vi.fn(),
|
||||
@@ -46,6 +47,7 @@ vi.mock('@electron/utils/provider-registry', () => ({
|
||||
|
||||
vi.mock('@electron/utils/openclaw-auth', () => ({
|
||||
removeProviderFromOpenClaw: mocks.removeProviderFromOpenClaw,
|
||||
removeProviderKeyFromOpenClaw: mocks.removeProviderKeyFromOpenClaw,
|
||||
saveOAuthTokenToOpenClaw: mocks.saveOAuthTokenToOpenClaw,
|
||||
saveProviderKeyToOpenClaw: mocks.saveProviderKeyToOpenClaw,
|
||||
setOpenClawDefaultModel: mocks.setOpenClawDefaultModel,
|
||||
@@ -71,6 +73,7 @@ vi.mock('@electron/utils/logger', () => ({
|
||||
import {
|
||||
syncAgentModelOverrideToRuntime,
|
||||
syncDefaultProviderToRuntime,
|
||||
syncDeletedProviderApiKeyToRuntime,
|
||||
syncDeletedProviderToRuntime,
|
||||
syncSavedProviderToRuntime,
|
||||
} from '@electron/services/providers/provider-runtime-sync';
|
||||
@@ -116,6 +119,7 @@ describe('provider-runtime-sync refresh strategy', () => {
|
||||
mocks.setOpenClawDefaultModelWithOverride.mockResolvedValue(undefined);
|
||||
mocks.saveProviderKeyToOpenClaw.mockResolvedValue(undefined);
|
||||
mocks.removeProviderFromOpenClaw.mockResolvedValue(undefined);
|
||||
mocks.removeProviderKeyFromOpenClaw.mockResolvedValue(undefined);
|
||||
mocks.updateAgentModelProvider.mockResolvedValue(undefined);
|
||||
mocks.updateSingleAgentModelProvider.mockResolvedValue(undefined);
|
||||
mocks.listAgentsSnapshot.mockResolvedValue({ agents: [] });
|
||||
@@ -137,6 +141,34 @@ describe('provider-runtime-sync refresh strategy', () => {
|
||||
expect(gateway.debouncedReload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('removes both runtime and stored account keys when deleting a custom provider', async () => {
|
||||
const gateway = createGateway('running');
|
||||
const customProvider = createProvider({
|
||||
id: 'moonshot-cn',
|
||||
type: 'custom',
|
||||
baseUrl: 'https://api.moonshot.cn/v1',
|
||||
});
|
||||
|
||||
await syncDeletedProviderToRuntime(customProvider, 'moonshot-cn', gateway as GatewayManager);
|
||||
|
||||
expect(mocks.removeProviderFromOpenClaw).toHaveBeenCalledWith('custom-moonshot');
|
||||
expect(mocks.removeProviderFromOpenClaw).toHaveBeenCalledWith('moonshot-cn');
|
||||
expect(mocks.removeProviderFromOpenClaw).toHaveBeenCalledTimes(2);
|
||||
expect(gateway.debouncedRestart).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('only clears the api-key profile when deleting a provider api key', async () => {
|
||||
const openaiProvider = createProvider({
|
||||
id: 'openai-personal',
|
||||
type: 'openai',
|
||||
});
|
||||
|
||||
await syncDeletedProviderApiKeyToRuntime(openaiProvider, 'openai-personal');
|
||||
|
||||
expect(mocks.removeProviderKeyFromOpenClaw).toHaveBeenCalledWith('openai');
|
||||
expect(mocks.removeProviderFromOpenClaw).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses debouncedReload after switching default provider when gateway is running', async () => {
|
||||
const gateway = createGateway('running');
|
||||
await syncDefaultProviderToRuntime('moonshot', gateway as GatewayManager);
|
||||
@@ -153,6 +185,34 @@ describe('provider-runtime-sync refresh strategy', () => {
|
||||
expect(gateway.debouncedRestart).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses gpt-5.4 as the browser OAuth default model for OpenAI', async () => {
|
||||
mocks.getProvider.mockResolvedValue(
|
||||
createProvider({
|
||||
id: 'openai-personal',
|
||||
type: 'openai',
|
||||
model: undefined,
|
||||
}),
|
||||
);
|
||||
mocks.getProviderAccount.mockResolvedValue({ authMode: 'oauth_browser' });
|
||||
mocks.getProviderSecret.mockResolvedValue({
|
||||
type: 'oauth',
|
||||
accessToken: 'access',
|
||||
refreshToken: 'refresh',
|
||||
expiresAt: 123,
|
||||
email: 'user@example.com',
|
||||
subject: 'project-1',
|
||||
});
|
||||
|
||||
const gateway = createGateway('running');
|
||||
await syncDefaultProviderToRuntime('openai-personal', gateway as GatewayManager);
|
||||
|
||||
expect(mocks.setOpenClawDefaultModel).toHaveBeenCalledWith(
|
||||
'openai-codex',
|
||||
'openai-codex/gpt-5.4',
|
||||
expect.any(Array),
|
||||
);
|
||||
});
|
||||
|
||||
it('syncs a targeted agent model override to runtime provider registry', async () => {
|
||||
mocks.getAllProviders.mockResolvedValue([
|
||||
createProvider({
|
||||
|
||||
@@ -119,6 +119,38 @@ describe('provider metadata', () => {
|
||||
expect(shouldShowProviderModelId(siliconflow, true)).toBe(true);
|
||||
});
|
||||
|
||||
it('shows OAuth model overrides only in dev mode and preserves defaults', () => {
|
||||
const openai = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'openai');
|
||||
const google = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'google');
|
||||
const minimax = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'minimax-portal');
|
||||
const minimaxCn = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'minimax-portal-cn');
|
||||
const qwen = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'qwen-portal');
|
||||
|
||||
expect(openai).toMatchObject({ showModelId: true, showModelIdInDevModeOnly: true, defaultModelId: 'gpt-5.4' });
|
||||
expect(google).toMatchObject({ showModelId: true, showModelIdInDevModeOnly: true, defaultModelId: 'gemini-3-pro-preview' });
|
||||
expect(minimax).toMatchObject({ showModelId: true, showModelIdInDevModeOnly: true, defaultModelId: 'MiniMax-M2.7' });
|
||||
expect(minimaxCn).toMatchObject({ showModelId: true, showModelIdInDevModeOnly: true, defaultModelId: 'MiniMax-M2.7' });
|
||||
expect(qwen).toMatchObject({ showModelId: true, showModelIdInDevModeOnly: true, defaultModelId: 'coder-model' });
|
||||
|
||||
expect(shouldShowProviderModelId(openai, false)).toBe(false);
|
||||
expect(shouldShowProviderModelId(google, false)).toBe(false);
|
||||
expect(shouldShowProviderModelId(minimax, false)).toBe(false);
|
||||
expect(shouldShowProviderModelId(minimaxCn, false)).toBe(false);
|
||||
expect(shouldShowProviderModelId(qwen, false)).toBe(false);
|
||||
|
||||
expect(shouldShowProviderModelId(openai, true)).toBe(true);
|
||||
expect(shouldShowProviderModelId(google, true)).toBe(true);
|
||||
expect(shouldShowProviderModelId(minimax, true)).toBe(true);
|
||||
expect(shouldShowProviderModelId(minimaxCn, true)).toBe(true);
|
||||
expect(shouldShowProviderModelId(qwen, true)).toBe(true);
|
||||
|
||||
expect(resolveProviderModelForSave(openai, ' ', true)).toBe('gpt-5.4');
|
||||
expect(resolveProviderModelForSave(google, ' ', true)).toBe('gemini-3-pro-preview');
|
||||
expect(resolveProviderModelForSave(minimax, ' ', true)).toBe('MiniMax-M2.7');
|
||||
expect(resolveProviderModelForSave(minimaxCn, ' ', true)).toBe('MiniMax-M2.7');
|
||||
expect(resolveProviderModelForSave(qwen, ' ', true)).toBe('coder-model');
|
||||
});
|
||||
|
||||
it('saves OpenRouter and SiliconFlow model overrides by default', () => {
|
||||
const openrouter = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'openrouter');
|
||||
const siliconflow = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'siliconflow');
|
||||
|
||||
Reference in New Issue
Block a user