blob: 3f3fd758d44005847c6e5516f231d39e012dfa9e [file]
/*
* 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 { createJsonErrorResponseHandler } from '@ai-sdk/provider-utils';
import { RetryError } from 'ai';
import { z } from 'zod/v4';
import {
classifyError,
providerFailureDiagnostic,
providerModelFailure,
} from '../provider-error-classification.js';
import type { ModelFailureKind } from '../model-protocol.js';
describe('Provider error classification', () => {
test('projects only bounded allowlisted facts into durable diagnostics', () => {
const diagnostic = providerFailureDiagnostic(
Object.assign(new Error('must not persist sk-secret-or-prompt'), {
name: 'AI_APICallError',
statusCode: 429,
responseHeaders: {
'x-request-id': 'req-123',
authorization: 'Bearer secret',
},
data: {
error: {
code: 'rate_limit_exceeded',
message: 'private provider payload',
},
prompt: 'private customer text',
},
requestBodyValues: { input: 'private request body' },
}),
);
assert.deepEqual(diagnostic, {
errorClass: 'rate_limit',
httpStatus: 429,
providerCode: 'rate_limit_exceeded',
providerRequestId: 'req-123',
retryable: true,
});
const serialized = JSON.stringify(diagnostic);
assert.doesNotMatch(serialized, /secret|private|authorization|request body/i);
});
test('structured usage-limit codes project to billing regardless of status', () => {
const quotaOn401 = Object.assign(new Error('request failed'), {
name: 'AI_APICallError',
statusCode: 401,
data: { error: { code: 'insufficient_quota' } },
});
assert.equal(classifyError(quotaOn401), 'provider_billing');
const balanceOn403 = Object.assign(new Error('request failed'), {
name: 'AI_APICallError',
statusCode: 403,
data: { error: { code: 'insufficient_balance' } },
});
assert.equal(classifyError(balanceOn403), 'provider_billing');
// Explicit provider evidence outranks the numeric HTTP fallback: an
// exhausted quota is a closed window, not a transient throttle to retry.
const quotaOn429 = Object.assign(new Error('request failed'), {
name: 'AI_APICallError',
statusCode: 429,
data: { error: { code: 'insufficient_quota' } },
});
assert.equal(classifyError(quotaOn429), 'provider_billing');
assert.equal(providerModelFailure(quotaOn429).retryable, false);
});
test('plan-window wording on a credential-shaped status projects to billing', () => {
// Providers that gate subscription windows behind 401/403 for validly
// signed-in users (#2516): their own wording outranks the bare status,
// whether the SDK surfaces it as the error message or keeps it only in
// the raw response body after a schema-parse failure.
const planWindow = Object.assign(new Error('Your account plan usage limit has been reached.'), {
name: 'AI_APICallError',
statusCode: 401,
data: { error: { type: 'authentication_error' } },
});
assert.equal(classifyError(planWindow), 'provider_billing');
assert.equal(providerModelFailure(planWindow).retryable, false);
const exhaustedCredits = Object.assign(new Error('Request failed with status code 403'), {
name: 'AI_APICallError',
statusCode: 403,
responseBody: JSON.stringify({
error: { message: 'Your credits have been exhausted for this billing period.' },
}),
});
assert.equal(classifyError(exhaustedCredits), 'provider_billing');
});
test('genuine credential and permission failures stay auth on 401/403', () => {
const invalidKey = Object.assign(new Error('Invalid API key provided'), {
name: 'AI_APICallError',
statusCode: 401,
data: { error: { message: 'Invalid API key. Check your credentials and try again.' } },
});
assert.equal(classifyError(invalidKey), 'auth');
const forbiddenModel = Object.assign(new Error('request failed'), {
name: 'AI_APICallError',
statusCode: 403,
data: { error: { message: 'You do not have access to this model.' } },
});
assert.equal(classifyError(forbiddenModel), 'auth');
const serverErrorOn403 = Object.assign(new Error('request failed'), {
name: 'AI_APICallError',
statusCode: 403,
data: { error: { code: 'server_error' } },
});
assert.equal(classifyError(serverErrorOn403), 'auth');
});
test('classifies exhausted Codex HTML edge 403 retries as provider unavailable', () => {
const exhaustedEdgeRejection = Object.assign(
new Error('Codex OAuth request failed: HTTP 403 Request rejected'),
{
name: 'OpenAiCodexEdgeRejectionError',
statusCode: 403,
data: { error: { code: 'openai_codex_edge_rejection' } },
},
);
assert.equal(classifyError(exhaustedEdgeRejection), 'provider_unavailable');
assert.partialDeepStrictEqual(providerModelFailure(exhaustedEdgeRejection), {
retryable: false,
});
assert.deepEqual(providerFailureDiagnostic(exhaustedEdgeRejection), {
errorClass: 'provider_unavailable',
httpStatus: 403,
providerCode: 'openai_codex_edge_rejection',
retryable: false,
});
const spoofedProviderPayload = Object.assign(new Error('request failed'), {
name: 'AI_APICallError',
statusCode: 403,
data: { error: { code: 'openai_codex_edge_rejection' } },
});
assert.equal(classifyError(spoofedProviderPayload), 'auth');
assert.notEqual(
classifyError({ code: 'openai_codex_edge_rejection', message: 'provider payload' }),
'provider_unavailable',
);
});
test('recovers structured Codex HTTP facts through an SDK wrapper and truncates identifiers', () => {
const cause = Object.assign(new Error('Codex OAuth request failed'), {
name: 'OpenAiCodexHttpError',
statusCode: 400,
data: { error: { code: 'x'.repeat(1_024) } },
responseHeaders: { 'x-request-id': 'r'.repeat(1_024) },
});
const diagnostic = providerFailureDiagnostic(
Object.assign(new Error('Cannot connect to API'), {
name: 'AI_APICallError',
code: 'FETCH_FAILED',
cause,
}),
);
assert.equal(diagnostic.errorClass, 'request_rejected');
assert.equal(diagnostic.httpStatus, 400);
assert.ok((diagnostic.providerCode?.length ?? 0) <= 256);
assert.ok((diagnostic.providerRequestId?.length ?? 0) <= 256);
assert.equal(diagnostic.retryable, false);
});
test('extracts allowlisted fields from JSON string failures without copying the payload', () => {
const summary = providerModelFailure(
JSON.stringify({
error: { message: 'Invalid api_key=sk-test-diagnostic-value', code: 'bad_request' },
request_id: 'req-123',
prompt: 'private customer text',
headers: { 'x-debug': 'internal' },
}),
);
assert.partialDeepStrictEqual(summary, {
message: 'Invalid api_key=sk-test-diagnostic-value (code=bad_request, requestId=req-123)',
code: 'bad_request',
});
assert.equal(JSON.stringify(summary).includes('private customer text'), false);
assert.equal(JSON.stringify(summary).includes('x-debug'), false);
assert.partialDeepStrictEqual(
providerModelFailure({
error: JSON.stringify({
message: 'nested provider rejection',
code: 'nested_error',
prompt: 'another private prompt',
}),
}),
{
message: 'nested provider rejection (code=nested_error)',
code: 'nested_error',
},
);
assert.equal(
providerModelFailure(JSON.stringify([{ prompt: 'private list payload' }])).message,
'Model request failed',
);
});
test('derives retryability from the failure kind alone', () => {
// One real provider shape per kind: the kind is the only authority, so the
// table is the whole retry contract.
const cases: Array<[string, unknown, ModelFailureKind, boolean]> = [
[
'websocket transport',
Object.assign(new Error('closed before completion'), {
name: 'OpenAiResponsesTransportError',
code: 'OPENAI_RESPONSES_WEBSOCKET_TRANSPORT_ERROR',
}),
'network',
true,
],
[
'missing continuation',
Object.assign(new Error('continuation unavailable'), {
name: 'OpenAiResponsesTransportError',
code: 'OPENAI_RESPONSES_CONTINUATION_UNAVAILABLE',
}),
'network',
true,
],
[
'xAI capacity',
Object.assign(new Error('The model is currently at capacity due to high demand.'), {
name: 'AI_APICallError',
data: { error: { code: 'resource-exhausted' } },
}),
'provider_capacity',
true,
],
[
'upstream 503',
Object.assign(new Error('Service unavailable'), {
name: 'AI_APICallError',
statusCode: 503,
}),
'provider_unavailable',
true,
],
[
'bare 429',
Object.assign(new Error('Upstream model provider is unavailable'), {
name: 'AI_APICallError',
statusCode: 429,
data: {
error: {
code: 'rate_limit_error',
message: 'Upstream model provider is temporarily unavailable. Please try again.',
},
},
}),
'rate_limit',
true,
],
[
'truncated stream',
new Error('response stream ended without a finish reason'),
'stream_truncated',
true,
],
[
'stream timeout',
Object.assign(new Error('model stream stalled'), { code: 'MODEL_STREAM_TIMEOUT' }),
'timeout',
true,
],
['fetch timeout', new DOMException('request timed out', 'TimeoutError'), 'timeout', true],
[
'invalid key',
Object.assign(new Error('Invalid API key provided'), {
name: 'AI_APICallError',
statusCode: 401,
}),
'auth',
false,
],
[
'exhausted quota',
Object.assign(new Error('request failed'), {
name: 'AI_APICallError',
statusCode: 429,
data: { error: { code: 'insufficient_quota' } },
}),
'provider_billing',
false,
],
[
'input overflow',
Object.assign(new Error('Bad Request'), {
name: 'AI_APICallError',
statusCode: 400,
data: { error: { code: 'context_length_exceeded' } },
}),
'context_overflow',
false,
],
[
// The AI SDK's own isRetryable flag calls 409 retryable; the kind decides.
'conflict',
Object.assign(new Error('Conflict'), {
name: 'AI_APICallError',
statusCode: 409,
isRetryable: true,
}),
'request_rejected',
false,
],
[
'bad request',
Object.assign(new Error('bad request'), { name: 'AI_APICallError', statusCode: 400 }),
'request_rejected',
false,
],
['fetch failure', new TypeError('fetch failed'), 'network', true],
[
'unclassifiable',
{ type: 'invalid_request_error', message: 'missing required field' },
'unknown',
false,
],
];
for (const [label, error, kind, retryable] of cases) {
assert.equal(classifyError(error), kind, label);
assert.equal(providerModelFailure(error).retryable, retryable, label);
assert.equal(providerFailureDiagnostic(error).retryable, retryable, label);
}
});
test('Retry-After only sets the delay, never the retryability', () => {
const failing = (statusCode: number, responseHeaders?: Record<string, string>) =>
Object.assign(new Error('provider rejected the request'), {
name: 'AI_APICallError',
statusCode,
...(responseHeaders ? { responseHeaders } : {}),
});
const headerCases: Array<[string, Record<string, string> | undefined, number | undefined]> = [
['seconds', { 'retry-after': '40' }, 40_000],
['milliseconds', { 'retry-after-ms': '1500' }, 1_500],
['malformed', { 'retry-after': 'not-a-delay' }, undefined],
['elapsed HTTP-date', { 'retry-after': 'Wed, 21 Oct 2015 07:28:00 GMT' }, undefined],
['absent', undefined, undefined],
];
for (const statusCode of [429, 503]) {
for (const [label, headers, retryAfterMs] of headerCases) {
const failure = providerModelFailure(failing(statusCode, headers));
assert.equal(failure.retryable, true, `${statusCode} ${label}`);
assert.equal(failure.retryAfterMs, retryAfterMs, `${statusCode} ${label}`);
}
}
// A non-retryable kind stays non-retryable however generous the header is.
const refusedWithDelay = Object.assign(new Error('Invalid API key provided'), {
name: 'AI_APICallError',
statusCode: 401,
responseHeaders: { 'retry-after': '40' },
});
assert.partialDeepStrictEqual(providerModelFailure(refusedWithDelay), { retryable: false });
assert.equal(providerModelFailure(refusedWithDelay).retryAfterMs, undefined);
});
test('abort and a spent Codex edge budget override the retryable kinds', () => {
const aborted = new RetryError({
message: 'Retry stopped',
reason: 'abort',
errors: [Object.assign(new Error('Service unavailable'), { statusCode: 503 })],
});
assert.equal(classifyError(aborted), 'abort');
assert.partialDeepStrictEqual(providerModelFailure(aborted), { retryable: false });
const exhaustedEdge = Object.assign(
new Error('Codex OAuth request failed: HTTP 403 Request rejected'),
{
name: 'OpenAiCodexEdgeRejectionError',
statusCode: 403,
data: { error: { code: 'openai_codex_edge_rejection' } },
responseHeaders: { 'retry-after': '40' },
},
);
assert.equal(classifyError(exhaustedEdge), 'provider_unavailable');
assert.partialDeepStrictEqual(providerModelFailure(exhaustedEdge), { retryable: false });
assert.equal(providerModelFailure(exhaustedEdge).retryAfterMs, undefined);
});
test('treats a status-less provider server_error as temporarily unavailable', () => {
const failure = {
type: 'model_failure',
kind: 'unknown',
retryable: false,
message:
'Streaming response failed: [502] Upstream error from Nvidia: Service temporarily overloaded',
code: 'server_error',
};
assert.equal(classifyError(failure), 'provider_unavailable');
assert.partialDeepStrictEqual(providerModelFailure(failure), { retryable: true });
assert.deepEqual(providerFailureDiagnostic(failure), {
errorClass: 'provider_unavailable',
providerCode: 'server_error',
retryable: true,
});
});
test('retries an AI SDK transport failure without an HTTP response', () => {
const failure = Object.assign(
new Error(
'Cannot connect to API: 80E1BDF601000000:error:0A000119:SSL routines:tls_get_more_records:decryption failed or bad record mac:../deps/openssl/openssl/ssl/record/methods/tls_common.c:869:',
),
{
name: 'AI_APICallError',
isRetryable: true,
cause: Object.assign(new TypeError('fetch failed'), {
cause: Object.assign(new Error('decryption failed or bad record mac'), {
code: 'ERR_SSL_DECRYPTION_FAILED_OR_BAD_RECORD_MAC',
}),
}),
},
);
assert.equal(classifyError(failure), 'network');
assert.partialDeepStrictEqual(providerModelFailure(failure), { retryable: true });
assert.equal(providerFailureDiagnostic(failure).retryable, true);
});
test('retries a transport failure identified only by a cause code', () => {
const failure = Object.assign(new Error('request failed'), {
cause: { code: 'ECONNRESET' },
});
assert.equal(classifyError(failure), 'network');
assert.partialDeepStrictEqual(providerModelFailure(failure), { retryable: true });
});
test('an exhausted free tier on 429 is billing, not a throttle to retry', () => {
// OpenCode Zen reports the exhausted free allowance as error.type on a 429.
const freeUsageLimit = Object.assign(new Error('Rate limit exceeded'), {
name: 'AI_APICallError',
statusCode: 429,
data: { error: { type: 'FreeUsageLimitError', message: 'Rate limit exceeded' } },
});
assert.equal(classifyError(freeUsageLimit), 'provider_billing');
assert.partialDeepStrictEqual(providerModelFailure(freeUsageLimit), { retryable: false });
assert.equal(providerFailureDiagnostic(freeUsageLimit).retryable, false);
});
test('classifies provider capacity errors and retries with backoff', () => {
const capacity = () =>
Object.assign(new Error('The model is currently at capacity due to high demand.'), {
name: 'AI_APICallError',
data: { error: { code: 'resource-exhausted' } },
});
assert.equal(classifyError(capacity()), 'provider_capacity');
const topLevelCode = Object.assign(new Error('The model is currently at capacity'), {
code: 'resource-exhausted',
});
assert.equal(classifyError(topLevelCode), 'provider_capacity');
const capacityWithAbortText = Object.assign(new Error('Request aborted by upstream'), {
name: 'AI_APICallError',
data: { error: { code: 'resource-exhausted' } },
});
assert.equal(classifyError(capacityWithAbortText), 'provider_capacity');
const capacityWithRateLimitStatus = Object.assign(new Error('Too many requests'), {
name: 'AI_APICallError',
statusCode: 429,
data: { error: { code: 'resource-exhausted' } },
});
assert.equal(classifyError(capacityWithRateLimitStatus), 'provider_capacity');
assert.partialDeepStrictEqual(providerModelFailure(capacityWithRateLimitStatus), {
retryable: true,
});
assert.deepEqual(providerFailureDiagnostic(capacityWithRateLimitStatus), {
errorClass: 'provider_capacity',
httpStatus: 429,
providerCode: 'resource-exhausted',
retryable: true,
});
assert.equal(
providerFailureDiagnostic(
Object.assign(new Error('The model is at capacity'), {
name: 'AI_APICallError',
statusCode: 503,
data: { error: { code: 'resource-exhausted' } },
}),
).errorClass,
'provider_capacity',
);
const ambiguousQuotaCode = Object.assign(new Error('resource exhausted'), {
name: 'AI_APICallError',
data: { error: { code: 'resource_exhausted' } },
});
assert.notEqual(classifyError(ambiguousQuotaCode), 'provider_capacity');
});
test('classifies context overflow by predicate, carrier shape, and evidence precedence', () => {
const overflow = (message: string, extra: Record<string, unknown> = {}) =>
classifyError(Object.assign(new Error(message), { name: 'AI_APICallError', ...extra }));
const textCases = [
'prompt is too long: 213462 tokens > 200000 maximum',
'request_too_large: Request exceeds the maximum size',
'Your input exceeds the context window of this model',
"Requested token count exceeds the model's maximum context length of 131072 tokens",
'The input token count (1196265) exceeds the maximum number of tokens allowed',
"This model's maximum prompt length is 131072 but the request contains 537812 tokens",
'Please reduce the length of the messages or completion',
"This endpoint's maximum context length is 262144 tokens",
'Prompt contains 5000 tokens; too large for model with 4096 maximum context length',
'invalid params, context window exceeds limit',
'Your request exceeded model token limit: 200000',
'prompt token count of 21000 exceeds the limit of 16384',
'the prompt contains too many tokens',
'Input token limit exceeded: 250000 tokens > 200000 maximum',
'Failed to generate response: context_length_exceeded',
];
for (const message of textCases) {
assert.equal(overflow(message, { statusCode: 400 }), 'context_overflow', message);
}
assert.equal(
overflow('Bad Request', {
statusCode: 400,
data: { error: { message: 'Bad Request', code: 'context_length_exceeded' } },
}),
'context_overflow',
);
assert.equal(
overflow('Bad Request', {
statusCode: 400,
responseBody: '{"error":{"code":"context_length_exceeded"}}',
}),
'context_overflow',
);
assert.equal(
overflow('Request Entity Too Large', {
statusCode: 400,
data: { error: { type: 'request_too_large', message: 'Request Entity Too Large' } },
}),
'context_overflow',
);
assert.equal(
classifyError({
type: 'error',
error: {
type: 'invalid_request_error',
code: 'context_length_exceeded',
message: 'Bad Request',
},
}),
'context_overflow',
);
assert.equal(
classifyError(
"Requested token count exceeds the model's maximum context length of 131072 tokens.",
),
'context_overflow',
);
assert.equal(
classifyError({ type: 'invalid_request_error', message: 'missing required field' }),
'unknown',
);
assert.equal(
overflow('Service Unavailable', {
statusCode: 503,
data: { error: { message: 'Service Unavailable', code: 'context_length_exceeded' } },
}),
'context_overflow',
);
assert.equal(
overflow(
"503 proxy error: Requested token count exceeds the model's maximum context length",
{ statusCode: 503 },
),
'context_overflow',
);
assert.equal(overflow('', { statusCode: 413 }), 'context_overflow');
assert.equal(
overflow('Please rate limit your requests', { statusCode: 503 }),
'provider_unavailable',
);
assert.notEqual(overflow('Failed to generate response', { statusCode: 400 }), 'rate_limit');
assert.equal(overflow('rate_limit_exceeded: slow down'), 'rate_limit');
const vetoedTextCases = [
'Rate limit reached: too many tokens, please wait',
"Too many requests. This endpoint's maximum context length is 262144 tokens.",
"ThrottlingException. This endpoint's maximum context length is 262144 tokens.",
"Quota exceeded. This endpoint's maximum context length is 262144 tokens.",
"Completion has too many tokens. This endpoint's maximum context length is 262144 tokens.",
"Too many tokens were requested for the completion. This endpoint's maximum context length is 262144 tokens.",
"Output token count of 8192 exceeds the limit. This endpoint's maximum context length is 262144 tokens.",
"Too many completion tokens were requested. This endpoint's maximum context length is 262144 tokens.",
"Maximum completion tokens exceeded. This endpoint's maximum context length is 262144 tokens.",
];
for (const message of vetoedTextCases) {
assert.notEqual(overflow(message, { statusCode: 400 }), 'context_overflow', message);
}
for (const message of [
'invalid request: missing required field',
'file size exceeds the limit of 10485760',
]) {
assert.notEqual(overflow(message, { statusCode: 400 }), 'context_overflow', message);
}
assert.equal(
overflow(
"This model's maximum context length is 8192 tokens. However, you requested 10240 tokens (10140 in the messages, 100 in the completion).",
{ statusCode: 400 },
),
'context_overflow',
);
assert.equal(
overflow('Completion has too many tokens for this model', {
statusCode: 400,
data: {
error: {
message: 'Completion has too many tokens for this model',
code: 'context_length_exceeded',
},
},
}),
'context_overflow',
);
});
test('classifies wording retained only in schema-invalid response bodies', async () => {
const handler = createJsonErrorResponseHandler({
errorSchema: z.object({ error: z.object({ message: z.string() }) }),
errorToMessage: (data) => data.error.message,
});
const errorFromBody = async (body: string) =>
(
await handler({
response: new Response(body, { status: 400, statusText: 'Bad Request' }),
url: 'https://api.example.test/v1/chat/completions',
requestBodyValues: {},
})
).value;
const overflowError = await errorFromBody(
'{"error":"Your input exceeds the context window of this model"}',
);
assert.equal(overflowError.message, 'Bad Request');
assert.equal(overflowError.data, undefined);
assert.equal(classifyError(overflowError), 'context_overflow');
const outputCapError = await errorFromBody(
'{"error":"Too many completion tokens were requested. This endpoint\'s maximum context length is 262144 tokens."}',
);
assert.notEqual(classifyError(outputCapError), 'context_overflow');
});
test('preserves provider evidence through the official AI SDK retry wrapper', async () => {
const handler = createJsonErrorResponseHandler({
errorSchema: z.object({
error: z.object({
message: z.string(),
code: z.string().optional(),
}),
}),
errorToMessage: (data) => data.error.message,
});
const apiCallError = async (status: number, body: string) =>
(
await handler({
response: new Response(body, { status, statusText: `HTTP ${status}` }),
url: 'https://api.example.test/v1/chat/completions',
requestBodyValues: {},
})
).value;
const retried = (
lastError: unknown,
reason: 'maxRetriesExceeded' | 'errorNotRetryable' = 'maxRetriesExceeded',
) =>
new RetryError({
message: 'Provider request failed after retries',
reason,
errors: [lastError, lastError, lastError],
});
const rateLimit = await apiCallError(429, '{"error":{"message":"Too many requests"}}');
const unavailable = await apiCallError(503, '{"error":{"message":"Service unavailable"}}');
const overflow = await apiCallError(
503,
'{"error":{"message":"Service unavailable","code":"context_length_exceeded"}}',
);
assert.equal(classifyError(retried(rateLimit)), 'rate_limit');
assert.equal(classifyError(retried(unavailable)), 'provider_unavailable');
assert.equal(classifyError(retried(overflow, 'errorNotRetryable')), 'context_overflow');
assert.equal(
classifyError(
new RetryError({
message: 'Retry stopped',
reason: 'abort',
errors: [new Error('transport stopped')],
}),
),
'abort',
);
assert.equal(
classifyError(
new RetryError({
message: 'Provider request failed after retries',
reason: 'maxRetriesExceeded',
errors: [],
}),
),
'unknown',
);
assert.equal(
classifyError(
Object.assign(new Error('Provider request failed after retries'), {
name: 'AI_RetryError',
lastError: rateLimit,
}),
),
'unknown',
);
});
});
test('auth classification matches authentication without matching authority', () => {
assert.equal(classifyError(new Error('OAuth2 token expired')), 'auth');
assert.equal(
classifyError(new Error('Conversation copy contains durable runtime authority facts')),
'unknown',
);
});