import { describe, expect, it } from 'vitest'; import type { AiMechanicAuthoringRequest } from '$lib/ai/mechanic-authoring'; import { parseConditionalMechanicDraft } from '$lib/calculations/mechanic-editor'; import { AI_MECHANIC_AUTHORING_INSTRUCTIONS, AI_MECHANIC_PROMPT_VERSION, buildAiMechanicAuthoringInput } from './mechanic-authoring-prompt'; const request: AiMechanicAuthoringRequest = { context: { sourceKind: 'talent', sourceName: 'Predatory Aura', sourceDescription: 'Restore Health based on the current Firepower Multiplier.' }, currentDraft: { version: 1, simulationReadiness: 'captured_only', scenarioInputs: [], pools: [], clauses: [] }, valueColumns: [ { key: 'tierI', variantKey: 'I', label: 'TIER I' }, { key: 'tierII', variantKey: 'II', label: 'TIER II' } ], allowAssignedAbility: false, allowProtocolUpgradeScaling: false, transcript: [] }; describe('AI mechanic authoring prompt', () => { it('advertises exact catalog Attribute references and displayed-percent semantics', () => { const input = JSON.parse(buildAiMechanicAuthoringInput({ request, attributes: [{ id: 'firepower-multiplier', name: 'Firepower Multiplier', description: null, unit: '%', type: null, category: null, calculationType: null }], events: [], actions: [], recipeKeys: [] })); expect(input.canonicalCatalog.grammar.valueReferences).toContain('attribute'); expect(input.canonicalCatalog.grammar.referenceShapes).toContainEqual({ ref: { source: 'attribute', attributeId: 'replace-with-an-exact-catalog-attribute-id' } }); expect(input.examplesAndRules).toEqual(expect.arrayContaining([ expect.stringContaining('exact supplied catalog ID'), expect.stringContaining('Divide that reference by 100') ])); expect(AI_MECHANIC_AUTHORING_INSTRUCTIONS).toContain('Never invent an Attribute ID'); }); it('advertises continuous restoration as a supported outcome capability', () => { const input = JSON.parse(buildAiMechanicAuthoringInput({ request, attributes: [{ id: '1bce573c-80f7-4487-b1d3-c4dc598b5244', name: 'Firepower Multiplier', description: null, unit: '%', type: null, category: null, calculationType: null }], events: [{ key: 'enemy_killed', label: 'Enemy killed' }], actions: [], recipeKeys: [] })); expect(AI_MECHANIC_PROMPT_VERSION).toBe(6); expect(input.canonicalCatalog.grammar.outcome.optional).toEqual( expect.arrayContaining(['delivery', 'durationSeconds']) ); expect(input.canonicalCatalog.grammar.outcome.delivery).toEqual([ 'instant', 'continuous' ]); expect(input.canonicalCatalog.grammar.continuousRestorationOutcomeShape) .toMatchObject({ kind: 'restoration', resource: 'health', delivery: 'continuous', durationSeconds: 3, variants: { 'replace-with-each-required-variant-key': { value: 240, expression: { left: 240, right: { left: { ref: { source: 'attribute' } }, right: 100 } } } } }); expect(input.examplesAndRules).toEqual(expect.arrayContaining([ expect.stringContaining('Continuous Health restoration is supported'), expect.stringContaining('not a per-second value'), expect.stringContaining('Do not replace continuous delivery with a periodic reducer') ])); expect(AI_MECHANIC_AUTHORING_INSTRUCTIONS).toContain( 'Prompt contract version: 6' ); const advertised = structuredClone( input.canonicalCatalog.grammar.continuousRestorationOutcomeShape ); const tierI = JSON.parse(JSON.stringify( advertised.variants['replace-with-each-required-variant-key'] ).replaceAll( 'replace-with-an-exact-catalog-attribute-id', '1bce573c-80f7-4487-b1d3-c4dc598b5244' )); const tierII = structuredClone(tierI); tierII.value = 336; tierII.expression.left = 336; advertised.variants = { I: tierI, II: tierII }; const parsed = parseConditionalMechanicDraft({ version: 1, simulationReadiness: 'ready', scenarioInputs: [], pools: [], clauses: [{ key: 'predator-aura', label: 'Predator Aura', event: 'enemy_killed', if: { kind: 'compare', left: { source: 'event', path: 'values.distanceMeters' }, operator: 'lte', right: 20 }, reducer: { kind: 'none' }, mutations: [], outcomes: [advertised] }] }, { variantKeys: ['I', 'II'], allowedAttributeIds: new Set(['1bce573c-80f7-4487-b1d3-c4dc598b5244']) }); expect(parsed.ok).toBe(true); }); it('advertises authoritative current Health percentage with strict band semantics', () => { const input = JSON.parse(buildAiMechanicAuthoringInput({ request, attributes: [], events: [{ key: 'tick', label: 'Simulation tick' }], actions: [], recipeKeys: ['health_threshold_bands'] })); expect(input.canonicalCatalog.grammar.combatReferences.currentHealthPercent) .toMatchObject({ label: 'Current Health percentage', description: expect.stringContaining('0 through 100') }); expect(input.canonicalCatalog.grammar.referenceShapes).toContainEqual({ ref: { source: 'combat', path: 'currentHealthPercent', timing: 'current' } }); expect(input.examplesAndRules).toEqual(expect.arrayContaining([ expect.stringContaining('operator:"lt"'), expect.stringContaining('<25, >=25 and <50, and >=50 and <75') ])); }); it('advertises Precise Trauma through target expiry without inventing target tick iteration', () => { const skillDamageId = 'skill-damage-catalog-id'; const input = JSON.parse(buildAiMechanicAuthoringInput({ request: { ...request, context: { ...request.context, sourceName: 'Precise Trauma', sourceDescription: 'Hits apply a debuff for 1 second, up to 10 stacks. Body shots add 1 stack and headshots add 2 stacks.' } }, attributes: [{ id: skillDamageId, name: 'Skill Damage', description: null, unit: null, type: null, category: null, calculationType: null }], events: [{ key: 'weapon_hit', label: 'Weapon hit' }], actions: [ { key: 'body_shot', label: 'Body shot' }, { key: 'headshot', label: 'Headshot' } ], recipeKeys: ['body_shot', 'headshot'] })); const advertised = structuredClone(input.canonicalCatalog.grammar.preciseTraumaShape); const outcome = advertised.clauses[0].outcomes[0]; const baseVariant = JSON.parse(JSON.stringify( outcome.variants['replace-with-each-required-variant-key'] ).replaceAll( 'replace-with-the-exact-skill-damage-catalog-id', skillDamageId )); outcome.variants = { I: structuredClone(baseVariant), II: structuredClone(baseVariant) }; expect(advertised.pools[0]).toMatchObject({ scope: 'target', max: 10, expiresAfterSeconds: 1 }); expect(advertised.clauses[0].mutations[0]).toEqual({ kind: 'add', pool: 'trauma', entity: { kind: 'event_target' }, value: 1 }); expect(advertised.clauses[1].mutations[0]).toMatchObject({ value: 2 }); expect(outcome).toMatchObject({ phase: 'on_exit', kind: 'direct_damage', scope: 'target', statePool: 'trauma', perStack: true, variants: { I: { expression: { ref: { source: 'attribute', attributeId: skillDamageId } } } } }); expect(input.examplesAndRules).toEqual(expect.arrayContaining([ expect.stringContaining('uses the entity previous state'), expect.stringContaining('without inventing generic repeated target ticks') ])); expect(JSON.stringify(input.canonicalCatalog.grammar.preciseTraumaShape)) .not.toContain('entitiesFromPool'); const parsed = parseConditionalMechanicDraft(advertised, { variantKeys: ['I', 'II'], allowedAttributeIds: new Set([skillDamageId]), allowedRecipeKeys: new Set(['body_shot', 'headshot']) }); expect(parsed.ok).toBe(true); }); });