diff --git a/apps/web/src/services/ai/__tests__/element-tools-dispatcher.test.ts b/apps/web/src/services/ai/__tests__/element-tools-dispatcher.test.ts index 97cdc945c..3ea91f9c7 100644 --- a/apps/web/src/services/ai/__tests__/element-tools-dispatcher.test.ts +++ b/apps/web/src/services/ai/__tests__/element-tools-dispatcher.test.ts @@ -153,8 +153,8 @@ describe('dispatchElementToolCalls (multi-tag batch)', () => { * advertised-but-not-executable output). */ describe('SUPPORTED_EMBEDDED_ELEMENT_TOOLS drift guard', () => { - it('covers every add_*_v0 name pen-mcp exposes', () => { - const penMcpAddNames = [...ELEMENT_TOOL_NAMES].filter((n) => /^add_.+_v0$/.test(n)); + it('covers every add_*_vN name pen-mcp exposes', () => { + const penMcpAddNames = [...ELEMENT_TOOL_NAMES].filter((n) => /^add_.+_v\d+$/.test(n)); const shimSet = new Set(SUPPORTED_EMBEDDED_ELEMENT_TOOLS); const missing = penMcpAddNames.filter((n) => !shimSet.has(n)); expect( diff --git a/apps/web/src/services/ai/design-parser.ts b/apps/web/src/services/ai/design-parser.ts index 3314fe877..2566ae6b8 100644 --- a/apps/web/src/services/ai/design-parser.ts +++ b/apps/web/src/services/ai/design-parser.ts @@ -103,7 +103,7 @@ export function tryParseAllElementToolOutputs(raw: string): DesignOutputShape[] parsed.arguments && typeof parsed.arguments === 'object' ? (parsed.arguments as Record) : {}; - if (/^add_[a-z_]+_v0$/.test(parsed.name)) { + if (/^add_[a-z_]+_v\d+$/.test(parsed.name)) { shapes.push({ kind: 'element-tool', name: parsed.name, arguments: args, raw: match[0] }); } else if (parsed.name === 'batch_design') { const dsl = typeof args.operations === 'string' ? args.operations : ''; diff --git a/packages/pen-ai-skills/src/corpus/output-parser.ts b/packages/pen-ai-skills/src/corpus/output-parser.ts index 410e8561c..7c2446eec 100644 --- a/packages/pen-ai-skills/src/corpus/output-parser.ts +++ b/packages/pen-ai-skills/src/corpus/output-parser.ts @@ -15,7 +15,10 @@ import type { ParsedOutput } from './types'; */ const OP_TOOL_RE_G = /\s*([\s\S]*?)\s*<\/op_tool>/g; -const ELEMENT_TOOL_NAME_RE = /^add_[a-z_]+_v0$/; +// Accepts any numeric version suffix (_v0, _v1, …). The family +// currently ships v0 (byte-parity, hex literals) and v1 (theme- +// aware with $color-* refs). Future _v2+ lands here automatically. +const ELEMENT_TOOL_NAME_RE = /^add_[a-z_]+_v\d+$/; /** * Reasoning-model chain-of-thought wrapper. MiniMax M-series, diff --git a/packages/pen-mcp/src/__tests__/design-prompt-elements.test.ts b/packages/pen-mcp/src/__tests__/design-prompt-elements.test.ts index c57eba180..4fe46708a 100644 --- a/packages/pen-mcp/src/__tests__/design-prompt-elements.test.ts +++ b/packages/pen-mcp/src/__tests__/design-prompt-elements.test.ts @@ -37,10 +37,10 @@ describe('get_design_prompt — elements section', () => { const def = DESIGN_TOOL_DEFINITIONS.find((t) => t.name === 'get_design_prompt'); const description = def?.description ?? ''; const elementTools = DESIGN_TOOL_DEFINITIONS.map((t) => t.name).filter((n) => - /^add_.*_v0$/.test(n), + /^add_.*_v\d+$/.test(n), ); // (1) any element-tool name that DOES appear must be in the registry - const namedInDescription = description.match(/add_[a-z_]+_v0/g) ?? []; + const namedInDescription = description.match(/add_[a-z_]+_v\d+/g) ?? []; for (const named of namedInDescription) { expect(elementTools, `description references ${named} which is not in registry`).toContain( named, @@ -76,7 +76,7 @@ describe('get_design_prompt — elements section', () => { // such tool MUST be mentioned in the elements skill content. const content = buildDesignPrompt('elements'); const elementTools = DESIGN_TOOL_DEFINITIONS.map((t) => t.name).filter((n) => - /^add_.*_v0$/.test(n), + /^add_.*_v\d+$/.test(n), ); expect(elementTools.length).toBeGreaterThan(0); for (const n of elementTools) { diff --git a/packages/pen-mcp/src/__tests__/element-tool-registry-parity.test.ts b/packages/pen-mcp/src/__tests__/element-tool-registry-parity.test.ts index 781679570..f65764707 100644 --- a/packages/pen-mcp/src/__tests__/element-tool-registry-parity.test.ts +++ b/packages/pen-mcp/src/__tests__/element-tool-registry-parity.test.ts @@ -52,9 +52,9 @@ describe('pen-mcp element-tool registry parity', () => { expect(ELEMENT_TOOL_DEFINITIONS.length).toBeGreaterThanOrEqual(42); }); - it('Every tool name matches the add_X_v0 convention', () => { + it('Every tool name matches the add_X_vN convention (accepts _v0, _v1, …)', () => { for (const def of ELEMENT_TOOL_DEFINITIONS) { - expect(def.name, `tool definition: ${def.name}`).toMatch(/^add_[a-z_]+_v0$/); + expect(def.name, `tool definition: ${def.name}`).toMatch(/^add_[a-z_]+_v\d+$/); } });