{"record":{"id":"512a729cfec9e8db","repo":"vitest-dev/vitest","slug":"each-tag-defined-in-test-tags-must-have-a-name","errorCode":null,"errorMessage":"Each tag defined in \"test.tags\" must have a \"name\" property, received: ${JSON.stringify(tag)}","messagePattern":"Each tag defined in \"test\\.tags\" must have a \"name\" property, received: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/vitest/src/node/config/resolveConfig.ts","lineNumber":290,"sourceCode":"  if ('poolOptions' in resolved) {\n    logger.deprecate('`test.poolOptions` was removed in Vitest 4. All previous `poolOptions` are now top-level options. Please, refer to the migration guide: https://v4.vitest.dev/guide/migration#pool-rework')\n  }\n\n  if ('workspace' in resolved) {\n    throw new Error('The `test.workspace` option was removed in Vitest 4. Please, migrate to `test.projects` instead. See https://vitest.dev/guide/projects for examples.')\n  }\n\n  resolved.pool ??= 'forks'\n\n  resolved.project = toArray(resolved.project)\n  resolved.provide ??= {}\n\n  // shallow copy tags array to avoid mutating user config\n  resolved.tags = [...resolved.tags || []]\n  const definedTags = new Set<string>()\n  resolved.tags.forEach((tag) => {\n    if (!tag.name || typeof tag.name !== 'string') {\n      throw new Error(`Each tag defined in \"test.tags\" must have a \"name\" property, received: ${JSON.stringify(tag)}`)\n    }\n    if (definedTags.has(tag.name)) {\n      throw new Error(`Tag name \"${tag.name}\" is already defined in \"test.tags\". Tag names must be unique.`)\n    }\n    if (/\\s/.test(tag.name)) {\n      throw new Error(`Tag name \"${tag.name}\" is invalid. Tag names cannot contain spaces.`)\n    }\n    if (/[!()*|&]/.test(tag.name)) {\n      throw new Error(`Tag name \"${tag.name}\" is invalid. Tag names cannot contain \"!\", \"*\", \"&\", \"|\", \"(\", or \")\".`)\n    }\n    if (/^\\s*(?:and|or|not)\\s*$/i.test(tag.name)) {\n      throw new Error(`Tag name \"${tag.name}\" is invalid. Tag names cannot be a logical operator like \"and\", \"or\", \"not\".`)\n    }\n    if (typeof tag.retry === 'object' && typeof tag.retry.condition === 'function') {\n      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.`)\n    }\n    if (tag.priority != null && (typeof tag.priority !== 'number' || tag.priority < 0)) {\n      throw new TypeError(`Tag \"${tag.name}\": priority must be a non-negative number.`)","sourceCodeStart":272,"sourceCodeEnd":308,"githubUrl":"https://github.com/vitest-dev/vitest/blob/1fa9837ec26533512fdcad8baebf249771bd340a/packages/vitest/src/node/config/resolveConfig.ts#L272-L308","documentation":"Every entry in `test.tags` must be an object with a string `name`. During resolution Vitest iterates `resolved.tags` and rejects the first entry whose `name` is falsy or non-string, including the JSON of the offending tag so the user can see exactly which entry is malformed.","triggerScenarios":"Passing a plain string (`tags: ['smoke']`) instead of objects; an entry like `{ label: 'smoke' }` missing `name`; `tags: [{ name: '' }]` or `tags: [{ name: null }]`; an undefined element slipped in via spread.","commonSituations":"Treating `tags` like Mocha/Pytest string tags; copy-pasting from docs of a different framework; building tags programmatically and forgetting the `name` field.","solutions":["Wrap each tag as `{ name: 'smoke' }`.","Validate tags before exporting config: `tags.every(t => t && typeof t.name === 'string' && t.name)`.","If you want a shorthand, map strings to objects in the config: `tags: ['smoke','flaky'].map(name => ({ name }))`."],"exampleFix":"// before\nexport default defineConfig({ test: { tags: ['smoke', { label: 'flaky' }] } })\n\n// after\nexport default defineConfig({ test: { tags: [{ name: 'smoke' }, { name: 'flaky' }] } })","handlingStrategy":"validation","validationCode":"type Tag = { name: string; [k: string]: unknown }\nfunction normalizeTags(input: unknown[]): Tag[] {\n  return input.map((t, i) => {\n    const tag = typeof t === 'string' ? { name: t } : (t as Tag)\n    if (!tag || typeof tag.name !== 'string' || !tag.name) {\n      throw new Error(`tags[${i}] must have a non-empty string \"name\"; got ${JSON.stringify(t)}`)\n    }\n    return tag\n  })\n}\n\nexport default defineConfig({ test: { tags: normalizeTags(rawTags) } })","typeGuard":"function isTag(v: unknown): v is { name: string } {\n  return typeof v === 'object' && v !== null\n    && typeof (v as any).name === 'string' && (v as any).name.length > 0\n}","tryCatchPattern":null,"preventionTips":["Always use the object form `{ name }` for tags, never bare strings.","Validate the tags array in a small helper before exporting config.","Document the tag schema for contributors adding new tags."],"tags":["config","tags","validation","resolveconfig"],"backgroundTag":null,"analyzedSha":"1fa9837ec26533512fdcad8baebf249771bd340a","analyzedAt":"2026-08-11T16:11:39.638Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}