diff --git a/packages/vitest/src/node/config/serializeConfig.ts b/packages/vitest/src/node/config/serializeConfig.ts index b09e7fb39..3c2800450 100644 --- a/packages/vitest/src/node/config/serializeConfig.ts +++ b/packages/vitest/src/node/config/serializeConfig.ts @@ -1,3 +1,4 @@ +import type { SerializedDiffOptions } from '@vitest/utils/diff' import type { SerializedConfig } from '../../runtime/config' import type { TestProject } from '../project' import type { ApiConfig } from '../types/config' @@ -42,8 +43,7 @@ export function serializeConfig(project: TestProject): SerializedConfig { allowWrite: api?.allowWrite, } })(project.isBrowserEnabled() ? config.browser.api : config.api), - // TODO: non serializable function? - diff: config.diff, + diff: serializeDiffOptions(config.diff), retry: config.retry, repeats: config.repeats, disableConsoleIntercept: config.disableConsoleIntercept, @@ -162,3 +162,43 @@ export function serializeConfig(project: TestProject): SerializedConfig { disableColors: isAgent && !isForceColor(), } } + +const serializableDiffKeys = [ + 'aAnnotation', + 'aIndicator', + 'bAnnotation', + 'bIndicator', + 'commonIndicator', + 'contextLines', + 'emptyFirstOrLastLinePlaceholder', + 'expand', + 'includeChangeCounts', + 'omitAnnotationLines', + 'printBasicPrototype', + 'maxDepth', + 'truncateThreshold', + 'truncateAnnotation', +] satisfies (keyof SerializedDiffOptions)[] + +// `diff` can be an inline object containing color/compareKeys functions +// (`DiffOptions`). Those functions are not structured-cloneable (threads pool) +// and are silently dropped over `child_process` IPC (forks pool), so passing +// the raw object to workers throws `DataCloneError`. Forward only the +// serializable fields declared by `SerializedDiffOptions`, and only the ones +// actually set — explicit `undefined` values would override the diff defaults +// when the worker merges the options. The function-based options still +// require the file-path form, which workers import locally. +function serializeDiffOptions( + diff: string | SerializedDiffOptions | undefined, +): string | SerializedDiffOptions | undefined { + if (diff == null || typeof diff === 'string') { + return diff + } + const result: SerializedDiffOptions = {} + for (const key of serializableDiffKeys) { + if (diff[key] !== undefined) { + (result as Record)[key] = diff[key] + } + } + return result +} diff --git a/test/e2e/test/reporters/custom-diff-config.test.ts b/test/e2e/test/reporters/custom-diff-config.test.ts index 96b6d2be3..ba3078635 100644 --- a/test/e2e/test/reporters/custom-diff-config.test.ts +++ b/test/e2e/test/reporters/custom-diff-config.test.ts @@ -21,3 +21,31 @@ test('invalid diff config file', async () => { expect(stderr).toContain('invalid diff config file') expect(stderr).toContain('Must have a default export with config object') }) + +// https://github.com/vitest-dev/vitest/issues/8663 +// An inline `diff` object may carry non-serializable color functions. They must +// be stripped before the config is sent to workers, otherwise the `threads` +// pool throws a `DataCloneError` (structured clone) and the `forks` pool drops +// them over IPC. The serializable annotations must still be applied. +test.each(['forks', 'threads'] as const)( + 'inline diff config object with color functions works in %s pool', + async (pool) => { + const filename = resolve('./fixtures/reporters/custom-diff-config.test.ts') + const { stderr } = await runVitest({ + root: './fixtures/reporters', + pool, + diff: { + aAnnotation: 'Expected to be', + bAnnotation: 'But got', + // @ts-expect-error color functions are not part of the public diff type, + // but users pass them at runtime — this is what used to crash the worker. + aColor: (s: string) => s, + }, + }, [filename]) + + expect(stderr).not.toContain('could not be cloned') + expect(stderr).not.toContain('DataCloneError') + expect(stderr).toContain('Expected to be') + expect(stderr).toContain('But got') + }, +)