Skip to main content
Glama
schema-shorthand.test.ts18.2 kB
/** * Tests for schema shorthand utilities * TIER 2: Token efficiency through input shorthand */ import { describe, it, expect } from 'vitest'; import { // Position parsePosition, parsePositions, formatPosition, // Damage parseDamage, formatDamage, parseMultiDamage, // Duration parseDuration, formatDuration, toRounds, // Range parseRange, formatRange, // Area of Effect parseAreaOfEffect, formatAreaOfEffect, // Dice rollDice, averageDice, // Zod schemas PositionSchema, DamageSchema, DurationSchema, RangeSchema, AreaOfEffectSchema } from '../../src/utils/schema-shorthand.js'; describe('Schema Shorthand Utilities', () => { // ═══════════════════════════════════════════════════════════════════════════ // POSITION PARSING // ═══════════════════════════════════════════════════════════════════════════ describe('parsePosition', () => { it('parses "x,y" format', () => { const pos = parsePosition('10,5'); expect(pos).toEqual({ x: 10, y: 5, z: 0 }); }); it('parses "x,y,z" format', () => { const pos = parsePosition('10,5,3'); expect(pos).toEqual({ x: 10, y: 5, z: 3 }); }); it('handles whitespace', () => { const pos = parsePosition(' 10 , 5 '); expect(pos).toEqual({ x: 10, y: 5, z: 0 }); }); it('accepts object input with z', () => { const pos = parsePosition({ x: 10, y: 5, z: 3 }); expect(pos).toEqual({ x: 10, y: 5, z: 3 }); }); it('accepts object input without z', () => { const pos = parsePosition({ x: 10, y: 5 }); expect(pos).toEqual({ x: 10, y: 5, z: 0 }); }); }); describe('parsePositions', () => { it('parses array of mixed formats', () => { const positions = parsePositions(['0,0', '10,5', { x: 20, y: 10 }]); expect(positions).toEqual([ { x: 0, y: 0, z: 0 }, { x: 10, y: 5, z: 0 }, { x: 20, y: 10, z: 0 } ]); }); }); describe('formatPosition', () => { it('formats without z by default', () => { expect(formatPosition({ x: 10, y: 5, z: 0 })).toBe('10,5'); }); it('includes z when non-zero', () => { expect(formatPosition({ x: 10, y: 5, z: 3 })).toBe('10,5,3'); }); it('includes z when forced', () => { expect(formatPosition({ x: 10, y: 5, z: 0 }, true)).toBe('10,5,0'); }); }); // ═══════════════════════════════════════════════════════════════════════════ // DAMAGE PARSING // ═══════════════════════════════════════════════════════════════════════════ describe('parseDamage', () => { it('parses basic dice notation "2d6"', () => { const damage = parseDamage('2d6'); expect(damage).not.toBeNull(); expect(damage?.count).toBe(2); expect(damage?.sides).toBe(6); expect(damage?.modifier).toBe(0); expect(damage?.type).toBe('untyped'); expect(damage?.average).toBe(7); expect(damage?.min).toBe(2); expect(damage?.max).toBe(12); }); it('parses dice with positive modifier "2d6+3"', () => { const damage = parseDamage('2d6+3'); expect(damage?.modifier).toBe(3); expect(damage?.average).toBe(10); expect(damage?.min).toBe(5); expect(damage?.max).toBe(15); }); it('parses dice with negative modifier "2d6-2"', () => { const damage = parseDamage('2d6-2'); expect(damage?.modifier).toBe(-2); expect(damage?.average).toBe(5); expect(damage?.min).toBe(0); // Min is floored at 0 expect(damage?.max).toBe(10); }); it('parses dice with damage type "2d6 fire"', () => { const damage = parseDamage('2d6 fire'); expect(damage?.type).toBe('fire'); }); it('parses full notation "2d6+3 fire"', () => { const damage = parseDamage('2d6+3 fire'); expect(damage?.count).toBe(2); expect(damage?.sides).toBe(6); expect(damage?.modifier).toBe(3); expect(damage?.type).toBe('fire'); }); it('handles single die "d20"', () => { const damage = parseDamage('d20'); expect(damage?.count).toBe(1); expect(damage?.sides).toBe(20); expect(damage?.dice).toBe('d20'); }); it('handles various damage types', () => { expect(parseDamage('1d8 slashing')?.type).toBe('slashing'); expect(parseDamage('1d6 piercing')?.type).toBe('piercing'); expect(parseDamage('2d10 radiant')?.type).toBe('radiant'); expect(parseDamage('3d6 necrotic')?.type).toBe('necrotic'); }); it('handles abbreviated damage types', () => { expect(parseDamage('1d6 slash')?.type).toBe('slashing'); expect(parseDamage('1d6 pierc')?.type).toBe('piercing'); }); it('returns null for invalid notation', () => { expect(parseDamage('invalid')).toBeNull(); expect(parseDamage('2x6')).toBeNull(); }); }); describe('formatDamage', () => { it('formats basic damage', () => { const damage = parseDamage('2d6')!; expect(formatDamage(damage)).toBe('2d6'); }); it('formats damage with modifier', () => { const damage = parseDamage('2d6+3')!; expect(formatDamage(damage)).toBe('2d6+3'); }); it('formats damage with type', () => { const damage = parseDamage('2d6+3 fire')!; expect(formatDamage(damage)).toBe('2d6+3 fire'); }); }); describe('parseMultiDamage', () => { it('parses multiple damage expressions', () => { const damages = parseMultiDamage('2d6+3 slashing + 1d6 fire'); expect(damages).toHaveLength(2); expect(damages[0].type).toBe('slashing'); expect(damages[1].type).toBe('fire'); }); }); // ═══════════════════════════════════════════════════════════════════════════ // DURATION PARSING // ═══════════════════════════════════════════════════════════════════════════ describe('parseDuration', () => { it('parses rounds "10r"', () => { const duration = parseDuration('10r'); expect(duration).not.toBeNull(); expect(duration?.value).toBe(10); expect(duration?.unit).toBe('rounds'); expect(duration?.rounds).toBe(10); }); it('parses minutes "1m"', () => { const duration = parseDuration('1m'); expect(duration?.value).toBe(1); expect(duration?.unit).toBe('minutes'); expect(duration?.rounds).toBe(10); }); it('parses hours "1h"', () => { const duration = parseDuration('1h'); expect(duration?.value).toBe(1); expect(duration?.unit).toBe('hours'); expect(duration?.rounds).toBe(600); }); it('parses days "7d"', () => { const duration = parseDuration('7d'); expect(duration?.value).toBe(7); expect(duration?.unit).toBe('days'); expect(duration?.rounds).toBe(7 * 14400); }); it('parses long-form "10 rounds"', () => { const duration = parseDuration('10 rounds'); expect(duration?.rounds).toBe(10); }); it('parses "instant"', () => { const duration = parseDuration('instant'); expect(duration?.unit).toBe('instantaneous'); expect(duration?.rounds).toBe(0); }); it('parses "concentration"', () => { const duration = parseDuration('concentration'); expect(duration?.unit).toBe('concentration'); }); it('parses "permanent"', () => { const duration = parseDuration('permanent'); expect(duration?.unit).toBe('permanent'); expect(duration?.rounds).toBe(Infinity); }); it('returns null for invalid duration', () => { expect(parseDuration('invalid')).toBeNull(); }); }); describe('formatDuration', () => { it('formats short form', () => { expect(formatDuration(parseDuration('10r')!)).toBe('10r'); expect(formatDuration(parseDuration('1h')!)).toBe('1h'); expect(formatDuration(parseDuration('7d')!)).toBe('7d'); }); it('formats long form', () => { expect(formatDuration(parseDuration('10r')!, false)).toBe('10 rounds'); expect(formatDuration(parseDuration('1h')!, false)).toBe('1 hour'); }); }); describe('toRounds', () => { it('converts string duration to rounds', () => { expect(toRounds('10r')).toBe(10); expect(toRounds('1m')).toBe(10); expect(toRounds('1h')).toBe(600); }); it('passes through numbers', () => { expect(toRounds(10)).toBe(10); }); it('extracts rounds from Duration object', () => { expect(toRounds(parseDuration('1h')!)).toBe(600); }); }); // ═══════════════════════════════════════════════════════════════════════════ // RANGE PARSING // ═══════════════════════════════════════════════════════════════════════════ describe('parseRange', () => { it('parses simple melee range "5ft"', () => { const range = parseRange('5ft'); expect(range?.normal).toBe(5); expect(range?.long).toBeNull(); expect(range?.type).toBe('melee'); }); it('parses ranged notation "30/120"', () => { const range = parseRange('30/120'); expect(range?.normal).toBe(30); expect(range?.long).toBe(120); expect(range?.type).toBe('ranged'); }); it('parses reach notation "10 reach"', () => { const range = parseRange('10 reach'); expect(range?.normal).toBe(10); expect(range?.type).toBe('reach'); }); it('parses "touch"', () => { const range = parseRange('touch'); expect(range?.normal).toBe(5); expect(range?.type).toBe('melee'); }); it('parses "self"', () => { const range = parseRange('self'); expect(range?.normal).toBe(0); }); }); describe('formatRange', () => { it('formats melee range', () => { expect(formatRange(parseRange('5ft')!)).toBe('5ft'); }); it('formats ranged notation', () => { expect(formatRange(parseRange('30/120')!)).toBe('30/120'); }); it('formats reach', () => { expect(formatRange(parseRange('10 reach')!)).toBe('10ft reach'); }); }); // ═══════════════════════════════════════════════════════════════════════════ // AREA OF EFFECT PARSING // ═══════════════════════════════════════════════════════════════════════════ describe('parseAreaOfEffect', () => { it('parses cone "60ft cone"', () => { const aoe = parseAreaOfEffect('60ft cone'); expect(aoe?.size).toBe(60); expect(aoe?.shape).toBe('cone'); }); it('parses cube "15ft cube"', () => { const aoe = parseAreaOfEffect('15ft cube'); expect(aoe?.size).toBe(15); expect(aoe?.shape).toBe('cube'); }); it('parses sphere "20ft sphere"', () => { const aoe = parseAreaOfEffect('20ft sphere'); expect(aoe?.size).toBe(20); expect(aoe?.shape).toBe('sphere'); }); it('parses radius (sphere alias) "20ft radius"', () => { const aoe = parseAreaOfEffect('20ft radius'); expect(aoe?.shape).toBe('sphere'); }); it('parses line with dimensions "30x5 line"', () => { const aoe = parseAreaOfEffect('30x5 line'); expect(aoe?.size).toBe(30); expect(aoe?.shape).toBe('line'); expect(aoe?.secondarySize).toBe(5); }); it('parses cylinder with height "20ft cylinder 40ft high"', () => { const aoe = parseAreaOfEffect('20ft cylinder 40ft high'); expect(aoe?.size).toBe(20); expect(aoe?.shape).toBe('cylinder'); expect(aoe?.secondarySize).toBe(40); }); }); describe('formatAreaOfEffect', () => { it('formats cone', () => { expect(formatAreaOfEffect(parseAreaOfEffect('60ft cone')!)).toBe('60ft cone'); }); it('formats line with dimensions', () => { expect(formatAreaOfEffect(parseAreaOfEffect('30x5 line')!)).toBe('30x5ft line'); }); }); // ═══════════════════════════════════════════════════════════════════════════ // DICE UTILITIES // ═══════════════════════════════════════════════════════════════════════════ describe('rollDice', () => { it('rolls dice within expected range', () => { // Roll 100 times and check bounds for (let i = 0; i < 100; i++) { const roll = rollDice('2d6+3'); expect(roll).toBeGreaterThanOrEqual(5); expect(roll).toBeLessThanOrEqual(15); } }); it('handles simple modifier "+5"', () => { expect(rollDice('+5')).toBe(5); expect(rollDice('-3')).toBe(-3); }); it('uses custom RNG', () => { // Always roll max const maxRng = () => 0.9999; expect(rollDice('1d6', maxRng)).toBe(6); // Always roll min const minRng = () => 0; expect(rollDice('1d6', minRng)).toBe(1); }); }); describe('averageDice', () => { it('calculates correct averages', () => { expect(averageDice('1d6')).toBe(3); expect(averageDice('2d6')).toBe(7); expect(averageDice('2d6+3')).toBe(10); expect(averageDice('1d8+5')).toBe(9); }); }); // ═══════════════════════════════════════════════════════════════════════════ // ZOD SCHEMA INTEGRATION // ═══════════════════════════════════════════════════════════════════════════ describe('Zod Schemas', () => { it('PositionSchema transforms string to Position', () => { const result = PositionSchema.parse('10,5'); expect(result).toEqual({ x: 10, y: 5, z: 0 }); }); it('PositionSchema transforms object to Position', () => { const result = PositionSchema.parse({ x: 10, y: 5 }); expect(result).toEqual({ x: 10, y: 5, z: 0 }); }); it('PositionSchema rejects invalid string', () => { expect(() => PositionSchema.parse('invalid')).toThrow(); }); it('DamageSchema transforms string to DamageNotation', () => { const result = DamageSchema.parse('2d6+3 fire'); expect(result.count).toBe(2); expect(result.type).toBe('fire'); }); it('DurationSchema transforms string to Duration', () => { const result = DurationSchema.parse('1h'); expect(result.unit).toBe('hours'); expect(result.rounds).toBe(600); }); it('RangeSchema transforms string to Range', () => { const result = RangeSchema.parse('30/120'); expect(result.normal).toBe(30); expect(result.long).toBe(120); }); it('AreaOfEffectSchema transforms string to AreaOfEffect', () => { const result = AreaOfEffectSchema.parse('20ft cone'); expect(result.size).toBe(20); expect(result.shape).toBe('cone'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Mnehmos/rpg-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server