Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions apps/desktop/src/main/__tests__/command-palette-retired.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import type { LlmConnection } from '@maka/core/llm-connections';
import { buildCommandList } from '../../renderer/command-palette-commands.js';

function connection(overrides: Partial<LlmConnection> = {}): LlmConnection {
return {
slug: 'openai-live',
name: 'OpenAI Live',
providerType: 'openai',
defaultModel: 'gpt-4.1',
enabled: true,
models: [{ id: 'gpt-4.1' }],
modelSource: 'fetched',
createdAt: 1,
updatedAt: 1,
...overrides,
};
}

// A retained retired row is still enabled: retirement keeps the connection so
// the credential stays visible and deletable, and nothing flips its flag.
const retired = connection({
slug: 'claude-subscription',
name: 'Claude Subscription',
providerType: 'claude-subscription',
defaultModel: 'claude-opus-5',
models: [{ id: 'claude-opus-5' }],
});

function commandIds(connections: LlmConnection[], defaultSlug: string | null): string[] {
return buildCommandList({
locale: 'en',
activeSessionId: undefined,
themePref: 'auto',
connections,
defaultSlug,
onNewChat: () => {},
onOpenSettings: () => {},
onOpenSettingsSection: () => {},
onOpenShortcuts: () => {},
onSetTheme: () => {},
onTestConnection: () => {},
onSetDefaultConnection: () => {},
}).map((command) => command.id);
}

test('a retained retired connection gets no palette commands', () => {
// Settings hides "set as default" and "test connection" for a retired row,
// but the palette used to filter on `enabled` alone — offering commands the
// storage default-target gate and the hidden auth actions then refuse.
const live = connection({ slug: 'anthropic-live', name: 'Anthropic Live' });
const ids = commandIds([connection(), live, retired], 'openai-live');
// Positive control: a live non-default connection keeps both commands, so an
// over-broad filter cannot pass this test by dropping everything.
assert.ok(ids.includes(`connection:set-default:${live.slug}`));
assert.ok(ids.includes(`connection:test:${live.slug}`));
assert.ok(!ids.includes(`connection:set-default:${retired.slug}`));
assert.ok(!ids.includes(`connection:test:${retired.slug}`));
});

test('a stale in-memory default pointing at a retired connection is not testable', () => {
// The catalog releases such a default on load; this covers the window where
// the renderer still holds the old slug.
const ids = commandIds([retired, connection()], retired.slug);
assert.ok(!ids.includes('diag:test-default'));
});
51 changes: 51 additions & 0 deletions apps/desktop/src/main/__tests__/config-transfer-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,55 @@ describe('config-transfer-service', () => {
assert.deepEqual(setCreds, [{ slug: 'deepseek-main', kind: 'api_key', value: 'sk-new' }]);
assert.deepEqual(result.credentials, { applied: 1, skipped: 0 });
});
it('restores the whole bundle when it carries a retained retired connection', async () => {
// A backup taken before the retirement still lists the connection, and the
// catalog refuses to create one. Before this was planned as skipped, the
// refusal threw mid-import: a fresh profile got whichever connections
// happened to be saved first and no settings, credentials, or memory at
// all. The live connection is ordered first here on purpose, so a restored
// abort would look like a partial success rather than a clean failure.
const { deps, saved, setCreds, writtenMemory, updatedSettings } = makeDeps({
connectionStore: {
list: async () => [],
save: async (c) => {
if (c.providerType === 'claude-subscription') {
throw new Error('"claude-subscription" is retired and cannot be added');
}
saved.push(c);
return c;
},
},
});
const bundle = {
schemaVersion: 1,
exportedAt: '',
appVersion: '0.1.0',
includedData: ['connections', 'settings', 'credentials', 'memory'] as const,
data: {
connections: [
conn('deepseek-main'),
{ ...conn('claude-subscription'), providerType: 'claude-subscription' },
],
settings: { theme: 'light' },
credentials: [
{ slug: 'deepseek-main', kind: 'api_key', value: 'sk-live' },
{ slug: 'claude-subscription', kind: 'oauth_token', value: 'retired-secret' },
],
memory: '# imported memory',
},
};

const result = await applyConfigImport(bundle as any, 'skip', deps);

assert.deepEqual(result.connections, { created: 1, overwritten: 0, skipped: 1 });
assert.deepEqual(saved.map((c) => c.slug), ['deepseek-main']);
// The rest of the bundle still lands — the point of the whole fix.
assert.equal(result.settings?.applied, true);
assert.equal(updatedSettings.length, 1);
assert.deepEqual(writtenMemory, ['# imported memory']);
// The retired connection's secret is skipped with it: only a created or
// overwritten slug gets one written.
assert.deepEqual(setCreds, [{ slug: 'deepseek-main', kind: 'api_key', value: 'sk-live' }]);
assert.deepEqual(result.credentials, { applied: 1, skipped: 1 });
});
});
70 changes: 70 additions & 0 deletions apps/desktop/src/main/__tests__/provider-connection-status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import type { LlmConnection } from '@maka/core/llm-connections';
import { connectionChipStatus } from '../../renderer/settings/provider-connection-status.js';

function connection(overrides: Partial<LlmConnection> = {}): LlmConnection {
return {
slug: 'openai-live',
name: 'OpenAI Live',
providerType: 'openai',
defaultModel: 'gpt-4.1',
enabled: true,
models: [{ id: 'gpt-4.1' }],
modelSource: 'fetched',
createdAt: 1,
updatedAt: 1,
...overrides,
};
}

const retired = connection({
slug: 'claude-subscription',
name: 'Claude Subscription',
providerType: 'claude-subscription',
defaultModel: 'claude-opus-5',
models: [{ id: 'claude-opus-5' }],
});

test('a retired connection reads as broken rather than repairable', () => {
// Nothing else in the list marks this row, so without a status the only
// signal that it has to go is on the detail page the user has no reason to
// open.
assert.deepEqual(connectionChipStatus(retired, 'zh'), {
label: '已停用 · 请删除',
tone: 'error',
});
assert.deepEqual(connectionChipStatus(retired, 'en'), {
label: 'Retired · delete it',
tone: 'error',
});
});

test('retirement outranks every repairable state', () => {
// Each of these would otherwise render a "sign in again" or "it failed, try
// again" status, and for a retired provider both point at nothing.
for (const overrides of [
{ lastTestStatus: 'needs_reauth' as const },
{ lastTestStatus: 'error' as const },
{ lastTestStatus: 'verified' as const },
{ enabled: false },
]) {
assert.deepEqual(
connectionChipStatus({ ...retired, ...overrides }, 'zh'),
{ label: '已停用 · 请删除', tone: 'error' },
`retirement must win over ${JSON.stringify(overrides)}`,
);
}
});

test('a live connection keeps its existing statuses', () => {
assert.equal(connectionChipStatus(connection({ lastTestStatus: 'verified' }), 'zh'), null);
assert.deepEqual(connectionChipStatus(connection({ lastTestStatus: 'needs_reauth' }), 'zh'), {
label: '需要重新登录',
tone: 'attention',
});
assert.deepEqual(connectionChipStatus(connection({ enabled: false }), 'zh'), {
label: '暂不可用',
tone: 'neutral',
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ function catalogWithoutDefault(): ConnectionCatalogSnapshot {
{
connectionId: CONNECTION_ID,
revision: 2,
slug: 'claude-subscription',
name: 'Claude OAuth',
providerType: 'claude-subscription',
slug: 'codex-subscription',
name: 'Codex OAuth',
providerType: 'openai-codex',
enabled: true,
enabledModelIds: ['claude-opus-5', 'claude-haiku-4-5'],
models: [{ id: 'claude-opus-5' }, { id: 'claude-haiku-4-5' }],
enabledModelIds: ['gpt-5-codex', 'gpt-5-codex-mini'],
models: [{ id: 'gpt-5-codex' }, { id: 'gpt-5-codex-mini' }],
modelSource: 'fallback',
modelsFetchedAt: 0,
},
Expand Down Expand Up @@ -70,9 +70,9 @@ describe('synchronizeRuntimeHostAccountConnection', () => {
});
const { client, selected } = accountClient(rejected);

await synchronizeRuntimeHostAccountConnection(client, 'claude-subscription');
await synchronizeRuntimeHostAccountConnection(client, 'openai-codex');

assert.deepEqual(selected(), { connectionId: CONNECTION_ID, modelId: 'claude-opus-5' });
assert.deepEqual(selected(), { connectionId: CONNECTION_ID, modelId: 'gpt-5-codex' });
});

it('selects a default model when model discovery throws', async () => {
Expand All @@ -81,9 +81,9 @@ describe('synchronizeRuntimeHostAccountConnection', () => {
};
const { client, selected } = accountClient(throwing);

await synchronizeRuntimeHostAccountConnection(client, 'claude-subscription');
await synchronizeRuntimeHostAccountConnection(client, 'openai-codex');

assert.deepEqual(selected(), { connectionId: CONNECTION_ID, modelId: 'claude-opus-5' });
assert.deepEqual(selected(), { connectionId: CONNECTION_ID, modelId: 'gpt-5-codex' });
});

it('leaves an existing default alone', async () => {
Expand All @@ -93,10 +93,10 @@ describe('synchronizeRuntimeHostAccountConnection', () => {
});
const { client, selectCalls } = accountClient(rejected, {
...catalogWithoutDefault(),
defaultTarget: { connectionId: CONNECTION_ID, modelId: 'claude-haiku-4-5' },
defaultTarget: { connectionId: CONNECTION_ID, modelId: 'gpt-5-codex-mini' },
});

await synchronizeRuntimeHostAccountConnection(client, 'claude-subscription');
await synchronizeRuntimeHostAccountConnection(client, 'openai-codex');

assert.equal(selectCalls(), 0);
});
Expand Down
49 changes: 17 additions & 32 deletions apps/desktop/src/main/__tests__/runtime-host-oauth-ipc-main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ test('presents both Host OAuth methods without exposing the authorization URL',
});

test('adapts every Host OAuth provider through one Desktop flow', async () => {
const provider = 'claude-subscription' as const;
const provider = 'openai-codex' as const;
const handlers = new Map<
string,
Parameters<RuntimeHostOAuthIpcDeps['ipcMain']['handle']>[1]
Expand All @@ -67,8 +67,8 @@ test('adapts every Host OAuth provider through one Desktop flow', async () => {
{
connectionId: '00000000-0000-4000-8000-000000000001',
revision: 1,
slug: 'claude-subscription',
name: 'Claude Code',
slug: 'openai-codex',
name: 'OpenAI Codex',
providerType: provider,
enabled: true,
enabledModelIds: [...PROVIDER_DEFAULTS[provider].fallbackModels],
Expand All @@ -86,14 +86,16 @@ test('adapts every Host OAuth provider through one Desktop flow', async () => {
},
startOAuthLogin: async (nextAttemptId, connectionId) => {
attemptId = nextAttemptId;
// Codex device login presents with `open_external`; the paste-code
// presentation this fixture used has no producer, so asserting it proved
// the desktop bridge against a flow no provider takes.
void presentation
.requestAuthorizationCode(
'https://claude.example/authorize',
.openExternal(
'https://codex.example/authorize',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
'STATE-HINT',
new AbortController().signal,
)
.then((authorizationCode) => {
assert.equal(authorizationCode, 'authorization-code#state');
.then(() => {
phase = 'authenticated';
});
return oauthProjection(nextAttemptId, connectionId, 'awaiting_authorization');
Expand Down Expand Up @@ -136,14 +138,6 @@ test('adapts every Host OAuth provider through one Desktop flow', async () => {
fetchedAt: 1,
};
},
fetchOAuthAccountUsage: async () => ({
kind: 'available' as const,
provider,
quota: {
fiveHour: { utilization: 20, resetsAt: '2026-08-05T12:00:00.000Z' },
fetchedAt: 1,
},
}),
setDefaultConnectionTarget: async (expectedCatalogRevision, target) => {
assert.equal(expectedCatalogRevision, catalog.revision);
catalog = { ...catalog, revision: catalog.revision + 1, defaultTarget: target };
Expand Down Expand Up @@ -188,19 +182,19 @@ test('adapts every Host OAuth provider through one Desktop flow', async () => {

assert.deepEqual([...handlers.keys()].sort(), [...RUNTIME_HOST_OAUTH_IPC_CHANNELS].sort());

for (const prefix of ['claude-subscription', 'openai-codex', 'xai-oauth']) {
for (const prefix of ['openai-codex', 'xai-oauth']) {
assert.equal(handlers.has(`${prefix}:get-auth-url`), true);
assert.equal(handlers.has(`${prefix}:complete-authorization`), true);
assert.equal(handlers.has(`${prefix}:get-account-state`), true);
assert.equal(handlers.has(`${prefix}:logout`), true);
}
const authorization = await invoke(handlers, 'claude-subscription:get-auth-url');
const authorization = await invoke(handlers, 'openai-codex:get-auth-url');
assert.deepEqual(authorization, { authRequestId: attemptId, stateHint: 'STATE-HINT' });
assert.deepEqual(opened, ['https://claude.example/authorize']);
assert.deepEqual(opened, ['https://codex.example/authorize']);
assert.deepEqual(
await invoke(
handlers,
'claude-subscription:complete-authorization',
'openai-codex:complete-authorization',
attemptId,
'authorization-code#state',
),
Expand All @@ -211,16 +205,11 @@ test('adapts every Host OAuth provider through one Desktop flow', async () => {
connectionId: catalog.connections[0]?.connectionId,
modelId,
});
assert.deepEqual(await invoke(handlers, 'claude-subscription:refresh-quota'), {
ok: true,
});
assert.deepEqual(await invoke(handlers, 'claude-subscription:get-account-state'), {
// No quota: reporting it required the retired provider's own client identity,
// so the account state carries the runtime state alone.
assert.deepEqual(await invoke(handlers, 'openai-codex:get-account-state'), {
provider,
runtimeState: 'authenticated',
quota: {
fiveHour: { utilization: 20, resetsAt: '2026-08-05T12:00:00.000Z' },
fetchedAt: 1,
},
});
});

Expand Down Expand Up @@ -285,10 +274,6 @@ test('keeps a committed OAuth login successful when model discovery fails', asyn
fetchConnectionModels: async () => {
throw new Error('provider temporarily unavailable');
},
fetchOAuthAccountUsage: async () => ({
kind: 'unavailable' as const,
reason: 'provider_unavailable' as const,
}),
setDefaultConnectionTarget: async () => {
throw new Error('Default selection must not run after failed discovery');
},
Expand Down Expand Up @@ -327,7 +312,7 @@ function oauthProjection(
return {
attemptId,
connectionId,
provider: 'claude-subscription' as const,
provider: 'openai-codex' as const,
phase,
};
}
Expand Down
Loading