/** * GSO Test Runner (no external dependencies) * Run: node tests/run.js */ // Mock mongoose so MarketSystem can be required without a live DB const Module = require('module'); const origLoad = Module._load; Module._load = function(name, ...args) { if (name === 'mongoose') { const fakeSchema = function(def) { this.def = def; }; fakeSchema.prototype.index = function() {}; fakeSchema.Types = { Mixed: 'Mixed' }; return { Schema: fakeSchema, model: () => class {} }; } return origLoad.call(this, name, ...args); }; let passed = 0, failed = 0; function test(name, fn) { try { fn(); console.log(` ✅ ${name}`); passed++; } catch(e) { console.log(` ❌ ${name}\n ${e.message}`); failed++; } } function expect(val) { return { toBe: (x) => { if (val !== x) throw new Error(`Expected ${JSON.stringify(x)}, got ${JSON.stringify(val)}`); }, toBeTruthy: () => { if (!val) throw new Error(`Expected truthy, got ${val}`); }, toBeGreaterThan:(x) => { if (!(val > x)) throw new Error(`Expected > ${x}, got ${val}`); }, toBeLessThan: (x) => { if (!(val < x)) throw new Error(`Expected < ${x}, got ${val}`); }, }; } // ─── CraftingSystem ────────────────────────────────────────────────────────── console.log('\n📦 CraftingSystem'); const CraftingSystem = require('../systems/CraftingSystem'); const mockLoader = { getRecipe: (id) => id === 'test_recipe' ? { recipe: { inputs: { iron_ore: 2 }, output: { iron_ingot: 1 }, craft_time_seconds: 30 }, craft: { xp: 10 } } : null, getAllRecipes: () => [], getRecipesByType: () => [], getCraftingTabs: () => [], }; const cs = new CraftingSystem(mockLoader); test('canCraft with sufficient items', () => { const r = cs.checkMaterials('test_recipe', { items: [{id:'iron_ore', quantity:3}] }); if (!r.canCraft) throw new Error('Expected canCraft=true'); }); test('fails with insufficient items', () => { const r = cs.checkMaterials('test_recipe', { items: [{id:'iron_ore', quantity:1}] }); if (r.canCraft) throw new Error('Expected canCraft=false'); if (r.missing[0].need !== 2) throw new Error(`Expected need=2, got ${r.missing[0].need}`); }); test('unknown recipe -> canCraft=false', () => { const r = cs.checkMaterials('nope', { items: [] }); if (r.canCraft) throw new Error('Should be false'); }); test('_calcSkillLevel: 0 XP = level 1', () => expect(cs._calcSkillLevel(0)).toBe(1)); test('_calcSkillLevel: 200 XP = level 2', () => expect(cs._calcSkillLevel(200)).toBe(2)); test('_calcSkillLevel: caps at 50', () => expect(cs._calcSkillLevel(99999)).toBe(50)); test('_removeItems: reduces quantity', () => { const inv = { items: [{id:'iron_ore', quantity:5}] }; cs._removeItems(inv, 'iron_ore', 3); expect(inv.items[0].quantity).toBe(2); }); test('_removeItems: removes item fully', () => { const inv = { items: [{id:'iron_ore', quantity:2}] }; cs._removeItems(inv, 'iron_ore', 2); expect(inv.items.length).toBe(0); }); // ─── MarketSystem ──────────────────────────────────────────────────────────── console.log('\n💰 MarketSystem'); const { MarketSystem } = require('../systems/MarketSystem'); const ms = new MarketSystem(); test('has 5 tradeable resources', () => expect(Object.keys(ms.getTradeableResources()).length).toBe(5)); test('darkMatter minPrice = 10', () => expect(ms.getTradeableResources().darkMatter.minPrice).toBe(10)); test('metal maxPrice = 100', () => expect(ms.getTradeableResources().metal.maxPrice).toBe(100)); // ─── XP Progression ────────────────────────────────────────────────────────── console.log('\n⭐ XP Progression (GDD §3.2: 500 × L^1.65)'); const xpReq = (L) => Math.round(500 * Math.pow(L, 1.65)); test('Level 1 = 500 XP', () => expect(xpReq(1)).toBe(500)); test('Level 5 > Level 2', () => expect(xpReq(5)).toBeGreaterThan(xpReq(2))); test('Level 10 > 10,000', () => expect(xpReq(10)).toBeGreaterThan(10000)); test('Level 50 > 300,000', () => expect(xpReq(50)).toBeGreaterThan(300000)); // ─── Alliance Research Tree ─────────────────────────────────────────────────── console.log('\n🛡 Alliance Research Tree'); const tree = require('../data/gso/alliance/research_tree.json'); test('3 tiers', () => expect(tree.tiers.length).toBe(3)); test('Tier 1 has 3 techs', () => expect(tree.tiers[0].techs.length).toBe(3)); test('Tier 2 has prerequisites', () => { const ok = tree.tiers[1].techs.some(t => t.prereq?.length > 0); if (!ok) throw new Error('No tier 2 tech has prerequisites'); }); test('All techs have id/name/cost/effect', () => { for (const tier of tree.tiers) for (const t of tier.techs) if (!t.id||!t.name||!t.cost||!t.effect) throw new Error(`Tech missing fields: ${JSON.stringify(t)}`); }); // ─── Locales ────────────────────────────────────────────────────────────────── console.log('\n🌍 Locales'); const fs2 = require('fs'), path2 = require('path'); for (const lang of ['en','de','fr','es','ja','zh','ko','pt']) { test(`${lang}.json has nav/resources/actions`, () => { const locPath = path2.join(__dirname,'../../Client/locales',`${lang}.json`); const loc = JSON.parse(fs2.readFileSync(locPath,'utf8')); if (!loc.nav?.dashboard) throw new Error('Missing nav.dashboard'); if (!loc.resources?.metal) throw new Error('Missing resources.metal'); if (!loc.actions?.build) throw new Error('Missing actions.build'); }); } // ─── Recipe time keys ───────────────────────────────────────────────────────── console.log('\n⏱ Recipe Time Coverage'); const TIME_KEYS = ['craft_time_seconds','alloy_time_seconds','smelt_time_seconds','process_time_seconds','cook_time_seconds','harvest_time_seconds']; test('alloy recipes have alloy_time_seconds', () => { const dir = path2.join(__dirname,'../data/gso/recipes/alloys'); for (const f of fs2.readdirSync(dir).filter(f=>f.endsWith('.json'))) { const rec = JSON.parse(fs2.readFileSync(path2.join(dir,f))); if (!TIME_KEYS.some(k=>rec.recipe?.[k]!==undefined)) throw new Error(`${f} missing time key`); } }); test('smelting recipes have smelt_time_seconds', () => { const dir = path2.join(__dirname,'../data/gso/recipes/smelting'); for (const f of fs2.readdirSync(dir).filter(f=>f.endsWith('.json'))) { const rec = JSON.parse(fs2.readFileSync(path2.join(dir,f))); if (!rec.recipe?.smelt_time_seconds) throw new Error(`${f} missing smelt_time_seconds`); } }); // ─── Summary ────────────────────────────────────────────────────────────────── console.log(`\n${'─'.repeat(50)}`); console.log(`Results: ${passed} passed, ${failed} failed`); if (failed > 0) { console.log('❌ Some tests failed'); process.exit(1); } else console.log('✅ All tests passed');