{"id":"64cf1c411aa87362","repo":"vitest-dev/vitest","slug":"expected-string-coverage-payload-received-typeo","errorCode":null,"errorMessage":"Expected string coverage payload, received ${typeof coverage}, ${JSON.stringify(coverage)}","messagePattern":"Expected string coverage payload, received (.+?), (.+?)","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/vitest/src/node/coverage.ts","lineNumber":293,"sourceCode":"      await fs.rm(this.coverageFilesDirectory, {\n        recursive: true,\n        force: true,\n        maxRetries: 10,\n      })\n    }\n\n    await fs.mkdir(this.coverageFilesDirectory, { recursive: true })\n\n    this.coverageFiles = new Map()\n  }\n\n  onAfterSuiteRun({ coverage, environment, projectName, testFiles }: AfterSuiteRunMeta): void {\n    if (!coverage) {\n      return\n    }\n\n    if (typeof coverage !== 'string') {\n      throw new TypeError(`Expected string coverage payload, received ${typeof coverage}, ${JSON.stringify(coverage)}`)\n    }\n    const filename = coverage\n\n    let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT)\n\n    if (!entry) {\n      entry = {}\n      this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry)\n    }\n\n    const testFilenames = testFiles.join()\n    entry[environment] ??= {}\n    // If there's a result from previous run, overwrite it\n    entry[environment][testFilenames] = filename\n  }\n\n  async readCoverageFiles<CoverageType>({ onFileRead, onFinished, onDebug }: {\n    /** Callback invoked with a single coverage result */","sourceCodeStart":275,"sourceCodeEnd":311,"githubUrl":"https://github.com/vitest-dev/vitest/blob/d568f8ce3739b532d5bf2c1ee1e45e8a8a473d09/packages/vitest/src/node/coverage.ts#L275-L311","documentation":"BlobReporter.onAfterSuiteRun (coverage.ts:293) requires the worker's coverage payload to be a string — the path to the coverage file the worker wrote. A non-string (object, etc.) means the IPC/transport contract is broken; vitest won't try to interpret arbitrary coverage objects here.","triggerScenarios":"A custom pool/environment/transport sending coverage as a structured object instead of a file path string; tampered or malformed worker IPC payload reaching onAfterSuiteRun.","commonSituations":"Custom forks/tinypool implementations that bypass the standard coverage serialization; tests that mock worker messages incorrectly.","solutions":["Ensure worker code writes coverage to a file and sends only the file path string as coverage.","Use vitest's standard coverage transport (don't short-circuit onAfterSuiteRun).","If writing a custom pool, mirror how @vitest/coverage-* workers serialize coverage."],"exampleFix":"// before (custom worker)\nchild.send({ coverage: coverageJsonObject })\n\n// after\nconst path = await writeCoverageFile(coverageJsonObject)\nchild.send({ coverage: path })","handlingStrategy":"validation","validationCode":"function assertCoverageString(payload: unknown): asserts payload is string {\n  if (typeof payload !== 'string') {\n    throw new TypeError(`coverage must be a file-path string, got ${typeof payload}`)\n  }\n}","typeGuard":"function isCoveragePath(c: unknown): c is string {\n  return typeof c === 'string' && c.length > 0\n}","tryCatchPattern":null,"preventionTips":["Always serialize coverage to a file in the worker and send only the path.","Don't bypass vitest's standard coverage transport in custom pools.","Add a contract test asserting worker messages carry string coverage."],"tags":["coverage","blob","ipc","worker"],"analyzedSha":"d568f8ce3739b532d5bf2c1ee1e45e8a8a473d09","analyzedAt":"2026-08-03T20:23:56.861Z","schemaVersion":2}