d709467902
## Context Investigating a report where the AI chat showed only a `...` spinner while the network response clearly contained `No AI models are available`. Root cause: terminal stream failures reach the client on **two mismatched channels**. | Representation | Persisted (survives reload) | Rendered by client | |---|---|---| | AI-SDK `error` chunk (inside `stream-chunk`) | ✅ RPUSH'd to Redis | ❌ dropped by `readUIMessageStream` (no message part, no error state) | | typed `stream-error` event | ❌ never persisted | ✅ sets the error atom | Live, the `stream-error` event renders. But on reload, `chatStreamCatchupChunks` replays only the persisted **error chunk** — which the reducer discards — and the streaming indicator never clears. ## Change Collapse to a single typed error contract: - **Suppress the opaque `error` chunk** in the stream job; every failure is surfaced through the typed `stream-error` event. Errors are mapped via `mapErrorToStreamError` so an `AiException` keeps its `AiExceptionCode` (e.g. `API_KEY_NOT_CONFIGURED` → the existing "AI not configured" banner) instead of leaking a raw string. - **Persist the terminal error** next to the accumulated chunks and expose it as an explicit `error { code message }` field on `ChatStreamCatchupChunks`, so a client catching up after a reload recovers it — no dependency on the AI SDK's internal chunk shape. - **Reset per-thread stream state at job start**, so a failed turn's leftover chunks/error never replay on the next stream. - **Client replays the catchup error** as a terminal `stream-error` event, which clears the streaming indicator and renders the error (fixes the infinite spinner on a stream that ended in error). ## Notes - `ChatStreamError` is a new metadata GraphQL type; generated types (twenty-front metadata + client-sdk) were hand-updated to keep the tree consistent and will be reconciled by CI's `graphql:generate` check if anything differs. - Server unit test added for the error mapping. No schema/DB migration. ## Test plan - [ ] With no AI provider configured, send a chat message → error renders immediately (not a spinner). - [ ] Reload the thread → the error still renders (recovered from catchup), indicator not spinning. - [ ] Configure a provider and send again → normal streaming; no stale error from the previous failed turn. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22434?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
70 lines
2.4 KiB
JavaScript
70 lines
2.4 KiB
JavaScript
import { createRequire } from 'module';
|
|
const require = createRequire(import.meta.url);
|
|
|
|
const isCI = process.env.CI === 'true';
|
|
|
|
const jestConfig = {
|
|
// For more information please have a look to official docs https://jestjs.io/docs/configuration/#prettierpath-string
|
|
// Prettier v3 should be supported in jest v30 https://github.com/jestjs/jest/releases/tag/v30.0.0-alpha.1
|
|
prettierPath: null,
|
|
// to enable logs, comment out the following line
|
|
silent: true,
|
|
...(isCI && { reporters: ['./jest-failures-only-reporter.js'] }),
|
|
errorOnDeprecated: true,
|
|
clearMocks: true,
|
|
displayName: 'twenty-server',
|
|
rootDir: './',
|
|
testEnvironment: 'node',
|
|
setupFilesAfterEnv: ['./setupTests.ts'],
|
|
transformIgnorePatterns: [
|
|
// jsdom 29 pulls ESM-only transitive deps (parse5, entities, tough-cookie,
|
|
// @exodus/bytes via html-encoding-sniffer, @csstools/@asamuzakjp css engine),
|
|
// and e2b/@e2b pull ESM-only chalk.
|
|
// jest's CJS runtime can't load their `export` syntax, so let swc transform them.
|
|
'/node_modules/(?!(file-type|@file-type|strtok3|token-types|@borewit|@tokenizer|uint8array-extras|read-next-line|digest-fetch|md5|js-sha256|js-sha512|base-64|charenc|crypt|email-reply-parser|jsdom|html-encoding-sniffer|whatwg-encoding|@exodus|parse5|entities|tough-cookie|@csstools|@asamuzakjp|graphql-upload|fs-capacitor|e2b|@e2b|chalk)/)',
|
|
],
|
|
testRegex: '.*\\.spec\\.ts$',
|
|
transform: {
|
|
// include .mjs so swc transforms ESM-only deps (e.g. jsdom's @csstools/* .mjs)
|
|
'^.+\\.(t|j|mj)s$': [
|
|
'@swc/jest',
|
|
{
|
|
jsc: {
|
|
parser: {
|
|
syntax: 'typescript',
|
|
tsx: false,
|
|
decorators: true,
|
|
},
|
|
transform: {
|
|
decoratorMetadata: true,
|
|
},
|
|
experimental: {
|
|
plugins: [
|
|
[
|
|
'@lingui/swc-plugin',
|
|
{
|
|
stripNonEssentialFields: false,
|
|
},
|
|
],
|
|
],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
moduleNameMapper: {
|
|
'^src/(.*)': '<rootDir>/src/$1',
|
|
'^test/(.*)': '<rootDir>/test/$1',
|
|
'^file-type$': require.resolve('file-type'),
|
|
},
|
|
moduleFileExtensions: ['js', 'mjs', 'json', 'ts'],
|
|
modulePathIgnorePatterns: ['<rootDir>/dist'],
|
|
fakeTimers: {
|
|
enableGlobally: true,
|
|
},
|
|
collectCoverageFrom: ['**/*.(t|j)s'],
|
|
coverageDirectory: '../coverage',
|
|
};
|
|
|
|
export default jestConfig;
|