141 lines
7.5 KiB
JavaScript
141 lines
7.5 KiB
JavaScript
/**
|
||
* 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');
|