vitest-dev/vitest · error · Error

Tag name " " is invalid. Tag names cannot contain "!", "*"…

Error message

Tag name "${tag.name}" is invalid. Tag names cannot contain "!", "*", "&", "|", "(", or ")".

What it means

Tag names cannot contain any of `! ( ) * & |` because those characters are operators in Vitest's tag filter expression grammar (`!not`, `&and`, `|or`, `*` wildcard, parentheses grouping). Allowing them in a name would make filter expressions ambiguous, so they are rejected outright.

Solutions

  1. Strip or replace the reserved chars: use `core-api` instead of `core&api`.
  2. Validate names against the allowed set: `/^[^!()*&|\s]+$/`.
  3. If you need logical grouping at filter time, use the operators in the CLI expression, not in the tag name.

Example fix

// before
export default defineConfig({ test: { tags: [{ name: 'core&api' }] } })

// after
export default defineConfig({ test: { tags: [{ name: 'core-api' }] } })
Defensive patterns

Strategy: validation

Validate before calling

const RESERVED = /[!()*&|]/
function assertSafeTagName(name: string): void {
  if (RESERVED.test(name)) {
    throw new Error(`Tag name "${name}" contains reserved filter-expression chars`)
  }
}

rawTags.forEach(t => assertSafeTagName(t.name))

Type guard

function isFilterSafeName(name: string): boolean {
  return typeof name === 'string' && !/[!()*&|]/.test(name)
}

Prevention

When it happens

Trigger: `{ name: 'flaky!' }`, `{ name: 'a*b' }`, `{ name: 'core&api' }`, `{ name: '(smoke)' }`, `{ name: 'a|b' }`.

Common situations: Reusing labels from another system that permits these chars; building names from CI branch names that contain `*` or `&`; copy-paste that includes markdown emphasis (`**bold**`).

Related errors


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

Appendix: source

Thrown at packages/vitest/src/node/config/resolveConfig.ts:299

  resolved.project = toArray(resolved.project)
  resolved.provide ??= {}

  // shallow copy tags array to avoid mutating user config
  resolved.tags = [...resolved.tags || []]
  const definedTags = new Set<string>()
  resolved.tags.forEach((tag) => {
    if (!tag.name || typeof tag.name !== 'string') {
      throw new Error(`Each tag defined in "test.tags" must have a "name" property, received: ${JSON.stringify(tag)}`)
    }
    if (definedTags.has(tag.name)) {
      throw new Error(`Tag name "${tag.name}" is already defined in "test.tags". Tag names must be unique.`)
    }
    if (/\s/.test(tag.name)) {
      throw new Error(`Tag name "${tag.name}" is invalid. Tag names cannot contain spaces.`)
    }
    if (/[!()*|&]/.test(tag.name)) {
      throw new Error(`Tag name "${tag.name}" is invalid. Tag names cannot contain "!", "*", "&", "|", "(", or ")".`)
    }
    if (/^\s*(?:and|or|not)\s*$/i.test(tag.name)) {
      throw new Error(`Tag name "${tag.name}" is invalid. Tag names cannot be a logical operator like "and", "or", "not".`)
    }
    if (typeof tag.retry === 'object' && typeof tag.retry.condition === 'function') {
      throw new TypeError(`Tag "${tag.name}": retry.condition function cannot be used inside a config file. Use a RegExp pattern instead, or define the function in your test file.`)
    }
    if (tag.priority != null && (typeof tag.priority !== 'number' || tag.priority < 0)) {
      throw new TypeError(`Tag "${tag.name}": priority must be a non-negative number.`)
    }
    definedTags.add(tag.name)
  })

  resolved.name = typeof options.name === 'string'
    ? options.name
    : (options.name?.label || '')

  resolved.color = typeof options.name !== 'string' ? options.name?.color : undefined

View on GitHub (pinned to 1fa9837ec2)