90d3fab483
Add ~330 tests across server (Jest), client (Vitest), editor-ext (Vitest)
and packages/mcp (node:test) for the gitmost features added since
053a9c0d: AI chat, AI agent roles, public-share assistant, MCP per-user
auth, HTML embed, page templates/embed, realtime tree, tree
expand/collapse, and the AI-settings UI.
Test-tooling fixes (prerequisite, were silently hiding coverage):
- Repair 3 page-template specs broken by the 11-arg TransclusionService
constructor; they never compiled, so template access-control / content
-leak / unsync-strip coverage was fictitious.
- Build @docmost/editor-ext before server tests via a `pretest` hook;
the stale dist omitted the new HtmlEmbed/PageEmbed exports (TS2305).
- Let jest resolve the .tsx email templates: add `tsx` to
moduleFileExtensions and widen the ts-jest transform to (t|j)sx?.
Behaviour-preserving "extract pure core" refactors that the tests drive:
- server: resolveShareAssistantRequest + uiMessageTextLength
(public-share controller), decideBasicGate + mapAuthResultToResponse
(mcp), buildErrorAssistantRecord (ai-chat), jsonbObject export (roles).
- client: render-raw-html + shouldExecute/canEdit, decide-embed-state,
page-embed picker utils, tree-socket reducers, open/close branch maps,
isEndpointConfigured/resolveKeyField; buildTreeWithChildren now treats
a permission-trimmed orphan as a root instead of crashing.
Deferred (need a test DB or HTTP harness, documented in the specs):
repo-level Postgres integration tests and the public-share XFF E2E.
Pre-existing DI/lib0-ESM suite failures are untouched and out of scope.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
84 lines
3.5 KiB
TypeScript
84 lines
3.5 KiB
TypeScript
import { describeProviderError } from './ai-error.util';
|
|
|
|
/**
|
|
* Unit tests for describeProviderError: the shared formatter used both for the
|
|
* server log line and for the error text streamed back to the client. This
|
|
* pins the behaviour, including the one behaviour change introduced when the
|
|
* two inline formatters were unified: a truncated, single-line snippet of the
|
|
* provider `responseBody`/`text` is appended (so a misconfigured endpoint's
|
|
* HTML error page is diagnosable). The util guarantees the API key is never in
|
|
* the response body, so this is safe to surface.
|
|
*/
|
|
describe('describeProviderError', () => {
|
|
it('uses the fallback for a null/empty/undefined error', () => {
|
|
expect(describeProviderError(null, 'AI stream error')).toBe(
|
|
'AI stream error',
|
|
);
|
|
expect(describeProviderError('', 'AI stream error')).toBe('AI stream error');
|
|
expect(describeProviderError(undefined)).toBe('Unknown error');
|
|
});
|
|
|
|
it('returns a non-empty plain string error as-is', () => {
|
|
expect(describeProviderError('boom')).toBe('boom');
|
|
});
|
|
|
|
it('formats statusCode + message', () => {
|
|
expect(
|
|
describeProviderError({ statusCode: 401, message: 'Unauthorized' }),
|
|
).toBe('401: Unauthorized');
|
|
});
|
|
|
|
it('falls back to message when there is no statusCode', () => {
|
|
expect(describeProviderError({ message: 'nope' })).toBe('nope');
|
|
});
|
|
|
|
it('appends a whitespace-collapsed response body snippet', () => {
|
|
const out = describeProviderError({
|
|
statusCode: 502,
|
|
message: 'Bad Gateway',
|
|
responseBody: '<html>\n <body>upstream error</body>\n</html>',
|
|
});
|
|
expect(out.startsWith('502: Bad Gateway | response body: ')).toBe(true);
|
|
// Newlines and runs of spaces are collapsed to single spaces.
|
|
expect(out).toContain('<html> <body>upstream error</body> </html>');
|
|
});
|
|
|
|
it('reads `text` when responseBody is absent', () => {
|
|
expect(describeProviderError({ message: 'e', text: 'body-text' })).toBe(
|
|
'e | response body: body-text',
|
|
);
|
|
});
|
|
|
|
it('truncates a long body to 300 chars + ellipsis', () => {
|
|
const out = describeProviderError({
|
|
message: 'e',
|
|
responseBody: 'x'.repeat(500),
|
|
});
|
|
expect(out).toContain('…');
|
|
// 'e | response body: ' + 300 chars + '…'
|
|
expect(out.length).toBeLessThan('e | response body: '.length + 305);
|
|
});
|
|
|
|
it('uses the fallback for a numeric or boolean (non-object, non-string) error', () => {
|
|
// typeof number / boolean is neither 'object' nor a non-empty 'string', so
|
|
// the early branch returns the fallback verbatim.
|
|
expect(describeProviderError(500, 'AI stream error')).toBe('AI stream error');
|
|
expect(describeProviderError(0, 'AI stream error')).toBe('AI stream error');
|
|
expect(describeProviderError(true)).toBe('Unknown error');
|
|
expect(describeProviderError(false, 'fb')).toBe('fb');
|
|
});
|
|
|
|
it('statusCode present but message undefined => "<code>:" with no trailing space', () => {
|
|
// `${code}: ${undefined ?? ''}`.trim() collapses to just "<code>:".
|
|
expect(describeProviderError({ statusCode: 503 })).toBe('503:');
|
|
// The trailing space after the colon is trimmed away.
|
|
expect(describeProviderError({ statusCode: 503 }).endsWith(': ')).toBe(false);
|
|
});
|
|
|
|
it('object with neither message nor statusCode nor body => fallback', () => {
|
|
expect(describeProviderError({}, 'AI stream error')).toBe('AI stream error');
|
|
// An object carrying only unrelated keys is still treated as message-less.
|
|
expect(describeProviderError({ foo: 'bar' } as never)).toBe('Unknown error');
|
|
});
|
|
});
|