import { test } from 'node:test'; import assert from 'node:assert/strict'; import { RuleMerger } from '../server/services/RuleMerger'; import { SCORE_MODE } from '../server/config/constants'; import type { MarketContext } from '../server/types'; const ctx = { benchmarks: { marketPE: 25, techPE: 32, reitYield: 3.8, igSpread: 1.2 }, } as Partial; test('FUNDAMENTAL mode returns Graham-style P/E gate', () => { const rules = RuleMerger.getRulesForAsset( 'STOCK', { sector: 'GENERAL' }, ctx as MarketContext, SCORE_MODE.FUNDAMENTAL, ); assert.equal(rules.gates.maxPERatio, 15); // updated: Graham's real rule is 15x assert.equal(rules.gates.maxPegGate, 1.0); // updated: Lynch PEG standard }); test('INFLATED mode loosens P/E gate from live SPY data', () => { const rules = RuleMerger.getRulesForAsset( 'STOCK', { sector: 'GENERAL' }, ctx as MarketContext, SCORE_MODE.INFLATED, ); assert.equal(rules.gates.maxPERatio, Math.round(25 * 1.5)); // 37 assert(rules.gates.maxPERatio > 15, 'Inflated P/E should exceed fundamental 15x'); }); test('INFLATED tech P/E gate uses XLK benchmark', () => { const rules = RuleMerger.getRulesForAsset( 'STOCK', { sector: 'TECHNOLOGY' }, ctx as MarketContext, SCORE_MODE.INFLATED, ); assert.equal(rules.gates.maxPERatio, Math.round(32 * 1.3)); // 42 }); test('Sector override applied before inflated overrides', () => { const rules = RuleMerger.getRulesForAsset( 'STOCK', { sector: 'REIT' }, ctx as MarketContext, SCORE_MODE.FUNDAMENTAL, ); assert.equal(rules.gates.maxPERatio, 9999); assert.equal(rules.weights.yield, 5); assert.equal(rules.weights.margin, 0); }); test('SECTOR_OVERRIDE is deleted from returned rules', () => { const rules = RuleMerger.getRulesForAsset( 'STOCK', { sector: 'GENERAL' }, ctx as MarketContext, SCORE_MODE.FUNDAMENTAL, ) as unknown as Record; assert.equal(rules.SECTOR_OVERRIDE, undefined); }); test('throws for unknown asset type', () => { assert.throws( () => RuleMerger.getRulesForAsset('CRYPTO' as never, {}, ctx as MarketContext), /No rules configured/, ); });