Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
682a295
fix(runtime): distinguish account limits from auth errors
me2seeks Aug 11, 2026
77eae24
fix(desktop): preserve neutral provider failures
me2seeks Aug 13, 2026
00fc6c3
fix(runtime): bound provider summaries and reuse the shared classifier
me2seeks Aug 17, 2026
b7167c5
fix(runtime-host): preserve provider failure codes in turn snapshots
me2seeks Aug 17, 2026
60a741d
fix(desktop): render only bounded provider summaries verbatim
me2seeks Aug 17, 2026
e12418a
fix(runtime): unify provider failure authority
me2seeks Aug 18, 2026
c5f61d0
fix(runtime): preserve provider message provenance
me2seeks Aug 18, 2026
3a40805
test(runtime): align provider failure assertions
me2seeks Aug 18, 2026
473da2e
fix(runtime): preserve structured cause semantics
me2seeks Aug 18, 2026
c222c93
fix(cli): localize runtime error notices
me2seeks Aug 18, 2026
c45aef2
fix(runtime): preserve structured provider failures
me2seeks Aug 19, 2026
fffab8f
fix(runtime): prioritize structured context overflow
me2seeks Aug 19, 2026
0480094
fix(runtime): keep context overflow out of generic retry
me2seeks Aug 19, 2026
d289a9a
fix(runtime-host): advance provider failure epoch
me2seeks Aug 19, 2026
ced82da
fix(runtime): tighten provider failure provenance and taxonomy inputs
me2seeks Aug 20, 2026
ff142c4
chore(core): restore the ASF header on provider-failure after the rebase
me2seeks Aug 23, 2026
34020c3
fix(runtime,cli): derive retry decisions from the normalized failure …
me2seeks Aug 25, 2026
0f9bb7e
fix(runtime): ensure evidence fields are always string
me2seeks Aug 25, 2026
5ce32b9
style: format pi-transcript
me2seeks Aug 25, 2026
e524782
chore: retrigger CI
me2seeks Aug 25, 2026
c564909
fix(cli): align pi-transcript with main materializer API
me2seeks Aug 25, 2026
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
189 changes: 189 additions & 0 deletions apps/desktop/src/main/__tests__/provider-failure-presentation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import type { SessionEvent } from '@maka/core/events';

import { sessionEventErrorMessage } from '../../renderer/model-connection-errors.js';
import { describeSessionErrorReason } from '../../renderer/session-error-presentation.js';
import {
deriveFailedTurnRecovery,
describeTurnErrorClass,
} from '../../renderer/session-status-presentation.js';
import { commandPaletteConnectionTestFailureMessage } from '../../renderer/app-shell-copy.js';
import { connectionTestFailureMessage } from '../../renderer/settings/provider-panel-shared.js';

describe('provider failure presentation', () => {
test('keeps provider account and access failures distinct in both locales', () => {
assert.equal(describeSessionErrorReason('usage_limit'), '模型使用额度已用完');
assert.equal(describeSessionErrorReason('provider_permission'), '模型服务拒绝访问');
assert.equal(describeSessionErrorReason('usage_limit', 'en'), 'Model usage limit reached');
assert.equal(describeSessionErrorReason('provider_permission', 'en'), 'Provider access denied');
});

test('does not present a bare 403 as an authentication failure', () => {
assert.equal(describeTurnErrorClass('403'), '未知错误');
assert.deepEqual(
deriveFailedTurnRecovery({
errorClass: 'usage_limit',
partialOutputRetained: false,
toolActivityCount: 0,
erroredToolCount: 0,
}),
{
action: 'check_account',
label: '检查模型服务的额度、套餐或恢复时间',
},
);
});

test('does not present a provider permission code as a local permission wait', () => {
assert.equal(describeTurnErrorClass('permission_required'), '等待权限确认');
assert.equal(describeTurnErrorClass('permission_error'), '未知错误');
});

test('preserves the bounded provider summary for a neutral Kimi plan-limit event', () => {
const message =
"You've reached your usage limit for this billing cycle. Your quota will be refreshed in the next cycle. " +
'To continue now, purchase extra usage or upgrade your plan: https://www.kimi.com/code/#pricing ' +
'(code=permission_error, status=403)';
const event: Extract<SessionEvent, { type: 'error' }> = {
type: 'error',
id: 'event-kimi-plan-limit',
turnId: 'turn-kimi-plan-limit',
ts: 1,
recoverable: false,
code: 'permission_error',
boundedProviderMessage: true,
message,
};

assert.equal(sessionEventErrorMessage(event), message);
assert.equal(sessionEventErrorMessage(event, 'en'), message);
});

test('preserves a bounded provider summary without a provider code', () => {
const event: Extract<SessionEvent, { type: 'error' }> = {
type: 'error',
id: 'event-provider-summary',
turnId: 'turn-provider-summary',
ts: 1,
recoverable: false,
boundedProviderMessage: true,
message: 'Provider request failed safely.',
};

assert.equal(sessionEventErrorMessage(event), event.message);
assert.equal(sessionEventErrorMessage(event, 'en'), event.message);
});

test('does not render a coded message verbatim without the bounded-provider marker', () => {
const event: Extract<SessionEvent, { type: 'error' }> = {
type: 'error',
id: 'event-ecodes',
turnId: 'turn-ecodes',
ts: 1,
recoverable: false,
code: 'ECONNRESET',
message: 'socket hang up at internal-connect.ts:42 (raw internal text)',
};

assert.equal(sessionEventErrorMessage(event), '任务运行失败,请稍后重试。');
assert.equal(sessionEventErrorMessage(event, 'en'), 'The task run failed. Try again later.');
});

test('uses generic copy when an error has neither a known reason nor provider evidence', () => {
const event: Extract<SessionEvent, { type: 'error' }> = {
type: 'error',
id: 'event-unknown',
turnId: 'turn-unknown',
ts: 1,
recoverable: false,
message: '403 permission denied',
};

assert.equal(sessionEventErrorMessage(event), '任务运行失败,请稍后重试。');
assert.equal(sessionEventErrorMessage(event, 'en'), 'The task run failed. Try again later.');
});

test('does not reclassify a neutral connection-test 403 as authentication', () => {
const result = {
ok: false,
statusCode: 403,
errorClass: 'unknown' as const,
errorMessage: '403 permission_error usage limit',
};

assert.equal(
connectionTestFailureMessage(result, {
auth: 'AUTH SHOULD NOT WIN',
recheck: 'RECHECK',
}, 'en'),
'RECHECK',
);
assert.equal(
commandPaletteConnectionTestFailureMessage(result, 'en'),
'The connection test failed. Try again later.',
);
});

test('renders only the Runtime-marked connection-test provider summary verbatim', () => {
const message = 'Plan allowance exhausted. (code=permission_error, status=403)';
const result = {
ok: false,
statusCode: 403,
errorClass: 'unknown' as const,
providerFailure: {
errorClass: 'RequestRejected' as const,
httpStatus: 403,
providerCode: 'permission_error',
retryable: false,
message,
boundedProviderMessage: true as const,
},
};

assert.equal(
connectionTestFailureMessage(result, { auth: 'AUTH', recheck: 'RECHECK' }, 'en'),
message,
);
assert.equal(commandPaletteConnectionTestFailureMessage(result, 'en'), message);
});

test('preserves structured account meaning without provider message text', () => {
const result = {
ok: false,
statusCode: 429,
errorClass: 'provider_unavailable' as const,
providerFailure: {
errorClass: 'UsageLimit' as const,
httpStatus: 429,
providerCode: 'usage_limit_reached',
retryable: false,
},
};

assert.equal(
connectionTestFailureMessage(result, { auth: 'AUTH', recheck: 'RECHECK' }, 'en'),
'Model usage limit reached',
);
assert.equal(commandPaletteConnectionTestFailureMessage(result, 'en'), 'Model usage limit reached');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,38 @@ test('preserves the Host-tested model and diagnostics for the existing Desktop U
errorClass: 'provider_unavailable',
},
);
const providerFailure = {
errorClass: 'RequestRejected' as const,
httpStatus: 403,
providerCode: 'permission_error',
retryable: false,
message: 'Plan allowance exhausted. (code=permission_error, status=403)',
boundedProviderMessage: true as const,
};
assert.deepEqual(
projectHostConnectionTest({
kind: 'committed',
catalogRevision: 10,
connection: { connectionId: 'connection-1', revision: 7 },
test: {
kind: 'failed',
checkedAt: '2026-08-05T00:00:02.000Z',
modelId: 'model-1',
latencyMs: 300,
statusCode: 403,
errorClass: 'unknown',
providerFailure,
},
}),
{
ok: false,
modelTested: 'model-1',
latencyMs: 300,
statusCode: 403,
errorClass: 'unknown',
providerFailure,
},
);
});

function catalog(): ConnectionCatalogSnapshot {
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/main/runtime-host-connections-ipc-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,9 @@ export function projectHostConnectionTest(result: ConnectionTestRunResult): Conn
errorClass: result.test.errorClass === 'invalid_response'
? 'unknown'
: result.test.errorClass,
...(result.test.providerFailure === undefined
? {}
: { providerFailure: result.test.providerFailure }),
};
}

Expand Down
32 changes: 23 additions & 9 deletions apps/desktop/src/renderer/app-shell-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { TextFileImportPreflightFailureReason } from '@maka/core/text-file-
import type { UiLocale } from '@maka/core/ui-locale';
import { generalizedErrorMessage, generalizedErrorMessageChinese } from '@maka/core/redaction';
import { getShellCopy } from './locales/shell-copy.js';
import { describeProviderAccountFailure } from './session-error-presentation.js';

const SESSION_READ_MESSAGES_ERROR_MARKER = 'MAKA_SESSION_READ_MESSAGES_ERROR:';

Expand Down Expand Up @@ -60,20 +61,33 @@ export function openPathActionErrorMessage(

export function commandPaletteConnectionTestFailureMessage(result: ConnectionTestResult, locale: UiLocale): string {
const fallback = commandPaletteConnectionTestFailureFallback(result, locale);
if (!result.errorMessage) return fallback;
return localizedErrorMessage(new Error(result.errorMessage), fallback, locale);
const failure = result.providerFailure;
return failure?.boundedProviderMessage === true && failure.message
? failure.message
: fallback;
}

function commandPaletteConnectionTestFailureFallback(result: ConnectionTestResult, locale: UiLocale): string {
const accountFailure = describeProviderAccountFailure(result.providerFailure?.errorClass, locale);
if (accountFailure) return accountFailure;
const copy = getShellCopy(locale).commandActions.connectionFailures;
if (result.statusCode === 429) return copy.rateLimit;
if (result.errorClass === 'timeout') return copy.timeout;
if (result.errorClass === 'auth' || result.statusCode === 401 || result.statusCode === 403) {
return copy.auth;
switch (result.providerFailure?.errorClass) {
case 'Auth':
return copy.auth;
case 'Timeout':
return copy.timeout;
case 'RateLimit':
return copy.rateLimit;
case 'Network':
return copy.network;
case 'ProviderUnavailable':
return copy.provider;
default:
break;
}
if (result.errorClass === 'timeout') return copy.timeout;
if (result.errorClass === 'auth') return copy.auth;
if (result.errorClass === 'network') return copy.network;
if (result.errorClass === 'provider_unavailable' || (result.statusCode && result.statusCode >= 500)) {
return copy.provider;
}
if (result.errorClass === 'provider_unavailable') return copy.provider;
return copy.unknown;
}
Loading
Loading