vitest-dev/vitest · error · TypeError

Test attachment requires only one of "body" or "path" to be…

Error message

Test attachment requires only one of "body" or "path" to be set. Both are specified.

What it means

Thrown by manageArtifactAttachment when a TestAttachment has both a non-null 'body' and a truthy 'path'. Vitest treats attachments as either inline content (body) or a file reference (path); specifying both is ambiguous and would cause double-handling during serialization. The guard runs for every attachment on artifacts passed to recordArtifact and on annotations that carry an attachment.

Solutions

  1. Pick exactly one source: set `body` for inline content OR `path` for a file/URL, never both.
  2. If you have both bytes and a file, write the bytes to disk first and keep only `path`, or drop the file and keep only `body`.
  3. Strip the unused field explicitly before constructing the attachment: `const { body, ...rest } = source; attach({ ...rest, body })`.
  4. Add a unit test asserting your attachment factory never emits both keys.

Example fix

// before
await ctx.annotate('screenshot', {
  path: '/tmp/shot.png',
  body: pngBytes,
})

// after (inline)
await ctx.annotate('screenshot', { body: pngBytes })
// after (file)
await ctx.annotate('screenshot', { path: '/tmp/shot.png' })
Defensive patterns

Strategy: validation

Validate before calling

function assertAttachment(a: Partial<TestAttachment>) {
  if (a.body != null && a.path) {
    throw new Error('Attachment has both body and path; pick one.')
  }
}
// call before context.annotate / recordArtifact

Type guard

const isBodyOnly = (a: TestAttachment) => a.body != null && !a.path
const isPathOnly = (a: TestAttachment) => !!a.path && a.body == null

Try / catch

try {
  await ctx.annotate(msg, attachment)
} catch (e) {
  if (e instanceof TypeError && /both are specified/i.test(e.message)) {
    // strip one field and retry, or log
  } else throw e
}

Prevention

When it happens

Trigger: Calling context.annotate(message, attachment) or recordArtifact(task, { attachments: [...] }) where a single attachment object sets both `body` (string/Uint8Array) and `path` (string). Also triggered by custom reporters/plugins that construct TestAttachment objects with both fields populated.

Common situations: Copying an attachment object literal that defaulted path and then assigning body; building attachments from a generic helper that always sets path and lets the caller override body; migrating from a reporter that stored both raw bytes and a temp file path.

Related errors


AI-assisted analysis of vitest-dev/vitest@1fa9837ec2 (2026-08-11). Data as JSON: /api/errors/26031d3bea8701d8. Report an issue: GitHub.

Appendix: source

Thrown at packages/vitest/src/runtime/runner/artifact.ts:175

  return promise
}

/**
 * Validates and prepares a test attachment for serialization.
 *
 * This function ensures attachments have either `body` or `path` set (but not both), and converts `Uint8Array` bodies to base64-encoded strings for easier serialization.
 *
 * @param attachment - The attachment to validate and prepare
 *
 * @throws {TypeError} If neither `body` nor `path` is provided
 * @throws {TypeError} If both `body` and `path` are provided
 */
export function manageArtifactAttachment(attachment: TestAttachment): void {
  if (attachment.body == null && !attachment.path) {
    throw new TypeError(`Test attachment requires "body" or "path" to be set. Both are missing.`)
  }
  if (attachment.body && attachment.path) {
    throw new TypeError(`Test attachment requires only one of "body" or "path" to be set. Both are specified.`)
  }
  if (attachment.path && attachment.bodyEncoding) {
    throw new TypeError(`Test attachment with "path" should not have "bodyEncoding" specified.`)
  }
  // convert to a string so it's easier to serialise
  if (attachment.body instanceof Uint8Array) {
    attachment.body = encodeUint8Array(attachment.body)
  }
  if (attachment.body != null) {
    attachment.bodyEncoding ??= 'base64'
  }
}

View on GitHub (pinned to 1fa9837ec2)