Skip to main content
Glama
update_character.test.ts12 kB
/** * update_character Tests - TDD * Red -> Green -> Refactor */ import { describe, it, expect, beforeEach } from 'vitest'; import { handleToolCall } from '../../src/registry.js'; import { getTextContent } from '../helpers.js'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; // Helper to get the same data directory the code uses const getDataDir = () => { if (process.platform === 'win32') { return path.join(process.env.APPDATA || os.homedir(), 'rpg-lite-mcp'); } else { return path.join(os.homedir(), '.config', 'rpg-lite-mcp'); } }; describe('update_character', () => { let testCharacterId: string; beforeEach(async () => { // Create a test character before each test const result = await handleToolCall('create_character', { name: 'Test Hero', class: 'Fighter', level: 5, stats: { str: 16, dex: 14, con: 15, int: 10, wis: 12, cha: 8, }, ac: 18, speed: 30, }); const text = getTextContent(result); const match = text.match(/Character ID: ([a-z0-9-]+)/); testCharacterId = match![1]; }); describe('Basic Updates', () => { it('should update character HP', async () => { const result = await handleToolCall('update_character', { characterId: testCharacterId, hp: 25, }); const text = getTextContent(result); expect(result.isError).toBeUndefined(); expect(text).toContain('╔'); // ASCII box border expect(text).toContain('CHARACTER UPDATE'); expect(text).toContain('CHANGES'); expect(text).toContain('HP:'); expect(text).toContain('25'); expect(text).toContain('→'); // Arrow showing before → after }); it('should update character level', async () => { const result = await handleToolCall('update_character', { characterId: testCharacterId, level: 6, }); const text = getTextContent(result); expect(text).toContain('Level:'); expect(text).toContain('5'); // Before expect(text).toContain('6'); // After expect(text).toContain('→'); // Arrow format // Max HP should increase due to level up expect(text).toContain('Max HP:'); }); it('should update character name', async () => { const result = await handleToolCall('update_character', { characterId: testCharacterId, name: 'Updated Hero', }); const text = getTextContent(result); expect(text).toContain('Test Hero'); // Before expect(text).toContain('Updated Hero'); // After }); it('should update AC', async () => { const result = await handleToolCall('update_character', { characterId: testCharacterId, ac: 20, }); const text = getTextContent(result); expect(text).toContain('AC:'); expect(text).toContain('18'); // Before expect(text).toContain('20'); // After }); }); describe('Ability Score Updates', () => { it('should update a single ability score', async () => { const result = await handleToolCall('update_character', { characterId: testCharacterId, stats: { str: 18 }, }); const text = getTextContent(result); expect(text).toContain('STR'); expect(text).toContain('16'); // Before expect(text).toContain('18'); // After // Modifier should change from +3 to +4 expect(text).toContain('+3'); expect(text).toContain('+4'); }); it('should update multiple ability scores', async () => { const result = await handleToolCall('update_character', { characterId: testCharacterId, stats: { str: 18, dex: 16, con: 16 }, }); const text = getTextContent(result); expect(text).toContain('STR'); expect(text).toContain('DEX'); expect(text).toContain('CON'); }); it('should recalculate maxHp when CON changes', async () => { const result = await handleToolCall('update_character', { characterId: testCharacterId, stats: { con: 18 }, // +4 instead of +2, should add +10 HP (2 * 5 levels) }); const text = getTextContent(result); expect(text).toContain('CON'); expect(text).toContain('Max HP:'); // Should show increased maxHp }); }); describe('Multiple Field Updates', () => { it('should update multiple fields at once', async () => { const result = await handleToolCall('update_character', { characterId: testCharacterId, hp: 30, ac: 19, speed: 35, level: 6, }); const text = getTextContent(result); expect(text).toContain('HP:'); expect(text).toContain('AC:'); expect(text).toContain('Speed:'); expect(text).toContain('Level:'); }); it('should update class and recalculate derived stats', async () => { const result = await handleToolCall('update_character', { characterId: testCharacterId, class: 'Wizard', // Lower hit die (d6 vs d10) }); const text = getTextContent(result); expect(text).toContain('Fighter'); // Before expect(text).toContain('Wizard'); // After expect(text).toContain('Max HP:'); // Should recalculate }); }); describe('Optional Fields', () => { it('should update equipment list', async () => { const result = await handleToolCall('update_character', { characterId: testCharacterId, equipment: ['Longsword', 'Shield', 'Plate Armor'], }); const text = getTextContent(result); expect(text).toContain('Equipment:'); expect(text).toContain('Longsword'); expect(text).toContain('Shield'); }); it('should update spellcasting info', async () => { const result = await handleToolCall('update_character', { characterId: testCharacterId, spellcastingAbility: 'int', cantrips: ['Fire Bolt', 'Mage Hand'], knownSpells: ['Magic Missile', 'Shield'], }); const text = getTextContent(result); expect(text).toContain('Spellcasting:'); expect(text).toContain('INT'); }); it('should update resistances/immunities', async () => { const result = await handleToolCall('update_character', { characterId: testCharacterId, resistances: ['fire', 'cold'], immunities: ['poison'], }); const text = getTextContent(result); expect(text).toContain('Resistances:'); expect(text).toContain('fire'); expect(text).toContain('Immunities:'); expect(text).toContain('poison'); }); }); describe('Validation', () => { it('should reject HP greater than maxHp', async () => { // First get current maxHp const getResult = await handleToolCall('get_character', { characterId: testCharacterId, }); const getText = getTextContent(getResult); const hpMatch = getText.match(/HP: \[.*?\] (\d+)\/(\d+)/); const maxHp = parseInt(hpMatch![2]); const result = await handleToolCall('update_character', { characterId: testCharacterId, hp: maxHp + 10, // Over max }); expect(result.isError).toBe(true); const text = getTextContent(result); expect(text).toContain('Error'); expect(text).toContain('HP'); }); it('should treat negative numbers as damage deltas', async () => { // Get initial HP const getResult = await handleToolCall('get_character', { characterId: testCharacterId, }); const getText = getTextContent(getResult); const hpMatch = getText.match(/HP: \[.*?\] (\d+)\/(\d+)/); const initialHp = parseInt(hpMatch![1]); // Apply damage using negative number (delta) const result = await handleToolCall('update_character', { characterId: testCharacterId, hp: -5, }); expect(result.isError).toBeUndefined(); // Should succeed (delta notation) const text = getTextContent(result); expect(text).toContain('HP:'); // HP should be reduced by 5 const afterMatch = text.match(/HP:.*?→.*?(\d+)\/(\d+)/); expect(parseInt(afterMatch![1])).toBe(initialHp - 5); }); it('should reject level out of range', async () => { const result = await handleToolCall('update_character', { characterId: testCharacterId, level: 25, // Over 20 }); expect(result.isError).toBe(true); const text = getTextContent(result); expect(text).toContain('Level'); }); it('should reject ability scores out of range', async () => { const result = await handleToolCall('update_character', { characterId: testCharacterId, stats: { str: 35 }, // Over 30 }); expect(result.isError).toBe(true); }); it('should reject non-existent character', async () => { const result = await handleToolCall('update_character', { characterId: 'non-existent-id', hp: 30, }); expect(result.isError).toBe(true); const text = getTextContent(result); expect(text).toContain('not found'); }); }); describe('Persistence', () => { it('should persist changes to disk', async () => { await handleToolCall('update_character', { characterId: testCharacterId, name: 'Persisted Hero', hp: 42, }); // Read the file directly from AppData const dataDir = path.join(getDataDir(), 'characters'); const filePath = path.join(dataDir, `${testCharacterId}.json`); const fileContent = fs.readFileSync(filePath, 'utf-8'); const character = JSON.parse(fileContent); expect(character.name).toBe('Persisted Hero'); expect(character.hp).toBe(42); }); it('should retrieve updated character with get_character', async () => { await handleToolCall('update_character', { characterId: testCharacterId, ac: 22, }); const result = await handleToolCall('get_character', { characterId: testCharacterId, }); const text = getTextContent(result); expect(text).toContain('22'); // Updated AC }); }); describe('Output Format', () => { it('should show before/after comparison in ASCII', async () => { const result = await handleToolCall('update_character', { characterId: testCharacterId, hp: 35, level: 6, }); const text = getTextContent(result); expect(text).toContain('╔'); // ASCII box border expect(text).toContain('CHARACTER UPDATE'); expect(text).toContain('CHANGES'); expect(text).toContain('→'); // Arrow showing changes expect(text).toContain('║'); // Box border (HEAVY style) }); it('should only show changed fields in comparison', async () => { const result = await handleToolCall('update_character', { characterId: testCharacterId, hp: 20, }); const text = getTextContent(result); // Should show HP change expect(text).toContain('HP:'); // Should not clutter with unchanged fields (test by checking reasonable length) expect(text.length).toBeLessThan(2000); // Compact output }); it('should handle updates with no actual changes gracefully', async () => { // Get current HP first const getResult = await handleToolCall('get_character', { characterId: testCharacterId, }); const getText = getTextContent(getResult); const hpMatch = getText.match(/HP: \[.*?\] (\d+)\/(\d+)/); const currentHp = parseInt(hpMatch![1]); const result = await handleToolCall('update_character', { characterId: testCharacterId, hp: currentHp, // Same value }); const text = getTextContent(result); expect(result.isError).toBeUndefined(); expect(text).toContain('CHARACTER UPDATE'); }); }); });

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/ChatRPG'

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