dnd-character-deploy/tests/currency.test.ts
2026-10-08 01:54:05 +00:00

8 lines
4.3 KiB
TypeScript

import 'fake-indexeddb/auto';import {test} from 'node:test';import assert from 'node:assert/strict';import {newDb} from 'pg-mem';
import {newCharacter,characterSchema,currencySchema,copperValue,changeCurrency,coinLimit} from '../src/model.ts';import {Storage,Conflict} from '../server/storage.mjs';import {CharacterDB,saveCharacter,loadCharacters} from '../src/db.ts';import {Campaigns} from '../server/campaigns.mjs';
import {characterMcp} from '../server/mcp.mjs';import {Client} from '@modelcontextprotocol/sdk/client/index.js';import {InMemoryTransport} from '@modelcontextprotocol/sdk/inMemory.js';
test('coin tally uses integer copper and rejects fractional counts, overdrafts and overflow',()=>{const c=newCharacter();changeCurrency(c,'gp',12);changeCurrency(c,'sp',3);changeCurrency(c,'cp',7);assert.equal(copperValue(c.currency!),1237);changeCurrency(c,'sp',-2);assert.deepEqual(c.currency,{gp:12,sp:1,cp:7});const before=structuredClone(c.currency);assert.throws(()=>changeCurrency(c,'cp',-8));assert.throws(()=>changeCurrency(c,'gp',0.5));assert.throws(()=>changeCurrency(c,'gp',coinLimit));assert.deepEqual(c.currency,before);assert.throws(()=>currencySchema.parse({gp:-1,sp:0,cp:0}));assert.throws(()=>currencySchema.parse({gp:0,sp:1.1,cp:0}));assert.equal(copperValue({gp:coinLimit,sp:coinLimit,cp:coinLimit}),coinLimit*111);delete c.currency;assert.equal(characterSchema.parse(c).currency,undefined);changeCurrency(c,'cp',1);assert.deepEqual(c.currency,{gp:0,sp:0,cp:1})});
test('currency survives backup and device persistence without mixing accounts or rewriting notes',async()=>{const database=new CharacterDB('currency-'+crypto.randomUUID()),c=newCharacter();c.currency={gp:14,sp:2,cp:9};c.inventory='Old note: 15 gold';try{await saveCharacter('alice',characterSchema.parse(JSON.parse(JSON.stringify(c))),database);await saveCharacter('bob',{...c,currency:{gp:1,sp:0,cp:0}},database);assert.deepEqual((await loadCharacters('alice',database))[0].currency,c.currency);assert.equal((await loadCharacters('alice',database))[0].inventory,c.inventory);assert.equal((await loadCharacters('bob',database))[0].currency?.gp,1)}finally{await database.delete()}});
test('old-client omission preserves purse; explicit zero clears under account and revision guards',async()=>{const pg=newDb().adapters.createPg(),s=new Storage(new pg.Pool());await s.init();const c=newCharacter();c.currency={gp:15,sp:3,cp:1};await s.put('alice',c.id,c,0);const old={...c,name:'Old client'};delete old.currency;delete old.inventoryItems;await s.put('alice',c.id,old,1);assert.deepEqual((await s.get('alice',c.id)).character.currency,c.currency);await assert.rejects(s.put('alice',c.id,old,1),Conflict);await assert.rejects(s.put('bob',c.id,old,2),Conflict);await s.put('alice',c.id,{...old,currency:{gp:0,sp:0,cp:0}},2);assert.deepEqual((await s.get('alice',c.id)).character.currency,{gp:0,sp:0,cp:0})});
test('MCP currency roundtrip preserves purse on HP edits and legacy replacement; party view omits it',async()=>{const pg=newDb().adapters.createPg(),s=new Storage(new pg.Pool());await s.init();const c=newCharacter();c.currency={gp:4,sp:5,cp:6};await s.put('alice',c.id,c,0);const campaigns=new Campaigns(s);await campaigns.init();const campaign=await campaigns.create('alice','Alice',{name:'Coins'});await campaigns.mutate('alice',campaign.id,1,'assign',{character_id:c.id});assert.equal((await campaigns.view('alice',campaign.id)).party[0].character.currency,undefined);const server=characterMcp({storage:s,sub:'alice',scopes:['characters:read','characters:write']}),client=new Client({name:'coin-test',version:'1'}),[a,b]=InMemoryTransport.createLinkedPair();await server.connect(a);await client.connect(b);try{const got:any=await client.callTool({name:'get_character',arguments:{id:c.id}});assert.deepEqual(got.structuredContent.sheet.character.currency,c.currency);assert.equal((await client.callTool({name:'change_hit_points',arguments:{id:c.id,expected_revision:1,amount:-1}})).isError,undefined);assert.deepEqual((await s.get('alice',c.id)).character.currency,c.currency);const old=(await s.get('alice',c.id)).character;delete old.currency;assert.equal((await client.callTool({name:'update_character',arguments:{character:old,expected_revision:2}})).isError,undefined);assert.deepEqual((await s.get('alice',c.id)).character.currency,c.currency)}finally{await client.close()}});