@reldens/skills
Version:
1,097 lines (866 loc) • 28.6 kB
Markdown
# TESTING PATTERNS AND BEST PRACTICES
## Lessons Learned from @reldens/skills Test Suite
## OVERVIEW
This document captures critical testing patterns, anti-patterns, and best practices learned from debugging and fixing the @reldens/skills test suite. These patterns apply to any event-driven system using EventsManagerSingleton.
## CRITICAL RULES
### Rule #1: NEVER Test Through Events Unless Testing the Event System Itself
**Why:** Event firing is asynchronous and creates race conditions that make tests non-deterministic.
**❌ ANTI-PATTERN:**
```javascript
it('should send level up message to client', async () => {
let sender = new Sender(classPath, mockClient);
sender.registerListeners(); // Registers event listener
// This fires LEVEL_UP event asynchronously
await classPath.levelUp();
// Race condition! Event listener may not have completed yet
assert.strictEqual(mockClient.sentMessages.length, 1); // ❌ May fail randomly
});
```
**✅ CORRECT PATTERN:**
```javascript
it('should send level up message to client', async () => {
let sender = new Sender(classPath, mockClient);
sender.registerListeners();
// Call the method directly, bypassing event system
await sender.sendLevelUpData(classPath);
// Deterministic - method has completed before assertion
assert.strictEqual(mockClient.sentMessages.length, 1); // ✓ Always works
});
```
**When to Test Through Events:**
- When explicitly testing event firing/listening behavior
- When testing event parameter passing
- When testing event sequence/order
**Example of Valid Event Testing:**
```javascript
it('should fire LEVEL_UP event with correct parameters', async () => {
let eventFired = false;
let receivedClassPath = null;
classPath.listenEvent(SkillsEvents.LEVEL_UP, (cp) => {
eventFired = true;
receivedClassPath = cp;
}, 'test-level-up-event');
await classPath.levelUp();
assert.strictEqual(eventFired, true);
assert.strictEqual(receivedClassPath, classPath);
});
```
### Rule #2: NEVER Use sleep() to Fix Race Conditions
**Why:** sleep() is a band-aid that masks the real problem. Tests should be deterministic, not time-dependent.
**❌ ANTI-PATTERN:**
```javascript
it('should update client after skill execution', async () => {
await skill.execute(target);
await TestHelpers.sleep(10); // ❌ Waiting for async event to complete
assert.strictEqual(mockClient.updated, true); // Flaky test
});
```
**✅ CORRECT PATTERN:**
```javascript
it('should update client after skill execution', async () => {
await sender.sendSkillExecutionData(skill, target); // Direct method call
assert.strictEqual(mockClient.updated, true); // Deterministic
});
```
**When sleep() IS Appropriate:**
```javascript
it('should restore canActivate after skillDelay', async () => {
let skill = new Skill({
owner: mockOwner,
skillDelay: 100 // Actual timer feature
});
skill.validate();
assert.strictEqual(skill.canActivate, false);
// ✓ Testing actual timer behavior
await TestHelpers.sleep(150);
assert.strictEqual(skill.canActivate, true);
});
```
**Summary:**
- ❌ Don't use sleep() to wait for async events
- ✓ Do use sleep() to test actual timer/delay features (skillDelay, castTime)
### Rule #3: ALWAYS Use Unique removeKeys
**Why:** EventsManagerSingleton maintains a GLOBAL removeKey registry that persists across tests even after `clearEventListeners()`.
**❌ ANTI-PATTERN:**
```javascript
// test-class-path.js
describe('ClassPath Events', () => {
afterEach(async () => {
await TestHelpers.clearEventListeners();
});
it('should fire ADD_SKILLS events', async () => {
classPath.listenEvent(SkillsEvents.ADD_SKILLS_BEFORE, callback, 'before-listener');
classPath.listenEvent(SkillsEvents.ADD_SKILLS_AFTER, callback, 'after-listener');
// Test passes
});
it('should fire REMOVE_SKILLS events', async () => {
// ❌ removeKey collision! 'before-listener' already exists in global registry
classPath.listenEvent(SkillsEvents.REMOVE_SKILLS_BEFORE, callback, 'before-listener');
classPath.listenEvent(SkillsEvents.REMOVE_SKILLS_AFTER, callback, 'after-listener');
// Listeners silently fail to register - test fails
});
});
```
**✅ CORRECT PATTERN:**
```javascript
describe('ClassPath Events', () => {
afterEach(async () => {
await TestHelpers.clearEventListeners();
});
it('should fire ADD_SKILLS events', async () => {
classPath.listenEvent(SkillsEvents.ADD_SKILLS_BEFORE, callback, 'add-before-listener');
classPath.listenEvent(SkillsEvents.ADD_SKILLS_AFTER, callback, 'add-after-listener');
// Test passes
});
it('should fire REMOVE_SKILLS events', async () => {
// ✓ Unique removeKeys
classPath.listenEvent(SkillsEvents.REMOVE_SKILLS_BEFORE, callback, 'remove-before-listener');
classPath.listenEvent(SkillsEvents.REMOVE_SKILLS_AFTER, callback, 'remove-after-listener');
// Listeners register correctly - test passes
});
});
```
**Best Practice - Use Descriptive, Namespaced removeKeys:**
```javascript
// Pattern: {test-suite}.{test-name}.{event-name}.{listener-purpose}
classPath.listenEvent(
SkillsEvents.LEVEL_UP,
callback,
'class-path.level-progression.level-up.verify-label-change'
);
```
### Rule #4: ALWAYS Clear Event Listeners in afterEach
**Why:** Event listeners persist across tests and can cause unexpected side effects.
**❌ ANTI-PATTERN:**
```javascript
describe('Skill Events', () => {
it('test 1', async () => {
skill.listenEvent(SkillsEvents.SKILL_AFTER_EXECUTE, callback, 'listener-1');
// Test completes, listener still registered
});
it('test 2', async () => {
await skill.execute(target);
// ❌ Listener from test 1 still fires! Unexpected callback execution
});
});
```
**✅ CORRECT PATTERN:**
```javascript
describe('Skill Events', () => {
afterEach(async () => {
await TestHelpers.clearEventListeners();
});
it('test 1', async () => {
skill.listenEvent(SkillsEvents.SKILL_AFTER_EXECUTE, callback, 'listener-1');
// Test completes
});
// afterEach clears listeners
it('test 2', async () => {
await skill.execute(target);
// ✓ No listeners from test 1
});
});
```
**TestHelpers.clearEventListeners() Implementation:**
```javascript
// tests/utils/test-helpers.js
static async clearEventListeners()
{
await EventsManagerSingleton.removeAllListeners();
}
```
**Important Note:**
- `clearEventListeners()` removes listener registrations
- It does NOT clear the removeKey registry
- This is why unique removeKeys are critical
---
### Rule #5: Test State Changes, Not Internal Implementation
**Why:** Tests should verify behavior and outcomes, not implementation details.
**❌ ANTI-PATTERN:**
```javascript
it('should call applyModifiers internally', async () => {
let applyModifiersCalled = false;
let originalMethod = skill.applyModifiers;
skill.applyModifiers = function(...args) {
applyModifiersCalled = true;
return originalMethod.apply(this, args);
};
await skill.execute(target);
assert.strictEqual(applyModifiersCalled, true); // ❌ Testing implementation
});
```
**✅ CORRECT PATTERN:**
```javascript
it('should apply target effects to target', async () => {
let initialValue = mockTarget.stats.hp;
let modifier = new Modifier({
key: 'hp-buff',
propertyKey: 'stats.hp',
operation: '+',
value: 20
});
let skill = new Effect({
owner: mockOwner,
targetEffects: [modifier]
});
await skill.execute(mockTarget);
// ✓ Testing observable behavior
assert.strictEqual(mockTarget.stats.hp, initialValue + 20);
});
```
### Rule #6: Use Fixtures for Consistent Test Data
**Why:** Reduces duplication, ensures consistency, makes tests easier to maintain.
**❌ ANTI-PATTERN:**
```javascript
it('test 1', async () => {
let skill = new Attack({
key: 'sword-attack',
owner: mockOwner,
hitDamage: 50,
attackProperties: ['atk'],
defenseProperties: ['def']
});
// Use skill
});
it('test 2', async () => {
// ❌ Duplicate skill creation with slightly different values
let skill = new Attack({
key: 'sword-attack',
owner: mockOwner,
hitDamage: 45, // Oops, different damage
attackProperties: ['atk'],
defenseProperties: ['def']
});
// Use skill
});
```
**✅ CORRECT PATTERN:**
```javascript
// tests/fixtures/skills/attack-skills.js
export const basicSwordAttack = {
key: 'sword-attack',
hitDamage: 50,
attackProperties: ['atk'],
defenseProperties: ['def'],
affectedProperty: 'hp'
};
// tests/unit/types/test-attack.js
import { basicSwordAttack } from '../../fixtures/skills/attack-skills.js';
it('test 1', async () => {
let skill = new Attack({
...basicSwordAttack,
owner: mockOwner
});
// Use skill
});
it('test 2', async () => {
let skill = new Attack({
...basicSwordAttack,
owner: mockOwner
});
// ✓ Consistent skill data
});
```
---
### Rule #7: Mock External Dependencies
**Why:** Unit tests should be isolated from external systems (network, database, physics engine).
**❌ ANTI-PATTERN:**
```javascript
it('should execute physical skill', async () => {
let physicsEngine = new RealPhysicsEngine(); // ❌ Real physics engine
let owner = new Player({physicsEngine});
let skill = new PhysicalAttack({owner});
await skill.execute(target); // Depends on real physics simulation
});
```
**✅ CORRECT PATTERN:**
```javascript
it('should execute physical skill', async () => {
let mockOwner = new MockOwner();
mockOwner.executePhysicalSkill = async (target, skill, executeOnHit) => {
// ✓ Mock physics behavior
await executeOnHit(target);
return true;
};
let skill = new PhysicalAttack({
owner: mockOwner,
objectWidth: 10,
objectHeight: 10
});
await skill.execute(mockTarget);
// Deterministic, fast, isolated
});
```
**Mock Classes in @reldens/skills:**
- `MockOwner` - Mock entity with position methods
- `MockTarget` - Mock target with stats
- `MockClient` - Mock network client with send/broadcast
### Rule #8: Test Edge Cases and Boundary Conditions
**Why:** Edge cases reveal bugs that normal cases don't.
**Examples:**
**Boundary Values:**
```javascript
it('should handle 0 damage', async () => {
let skill = new Attack({
owner: mockOwner,
hitDamage: 0
});
await skill.execute(mockTarget);
// Verify behavior with zero damage
});
it('should handle infinite range', async () => {
let skill = new Skill({
owner: mockOwner,
range: 0 // 0 = infinite range
});
assert.strictEqual(skill.owner.isInRange(skill, farAwayTarget, skill.range), true);
});
```
**Null/Undefined:**
```javascript
it('should handle undefined target', async () => {
let skill = new Effect({owner: mockOwner});
let result = await skill.execute(undefined);
assert.strictEqual(result.error, SKILL.TARGET_NOT_AVAILABLE);
});
```
**Empty Collections:**
```javascript
it('should handle empty ownerEffects', async () => {
let skill = new Skill({
owner: mockOwner,
ownerEffects: []
});
await skill.execute(mockTarget);
// Should not throw, should skip effects application
});
```
**Maximum Values:**
```javascript
it('should not allow damage below 0 when allowEffectBelowZero is false', async () => {
mockTarget.stats.hp = 50;
let skill = new Attack({
owner: mockOwner,
hitDamage: 100,
allowEffectBelowZero: false
});
await skill.execute(mockTarget);
assert.strictEqual(mockTarget.stats.hp, 0); // Clamped to 0
});
```
---
### Rule #9: Use Descriptive Test Names
**Why:** Test names should describe what is being tested and expected outcome.
**❌ ANTI-PATTERN:**
```javascript
it('test 1', async () => {
// What does this test?
});
it('attack works', async () => {
// Too vague
});
it('check dodge', async () => {
// Unclear expectation
});
```
**✅ CORRECT PATTERN:**
```javascript
it('should apply damage to target hp when attack succeeds', async () => {
// Clear: what, when, expected outcome
});
it('should return DODGED error when target dodge > aim * dodgeOverAimSuccess', async () => {
// Clear: condition and expected result
});
it('should apply critical damage multiplier when critical roll succeeds', async () => {
// Clear: feature and expected behavior
});
```
**Pattern:**
```
should [action/behavior] when [condition]
should [expected outcome] [optional: given specific input]
```
### Rule #10: One Assertion Per Logical Concept
**Why:** Multiple unrelated assertions make it hard to identify what failed.
**❌ ANTI-PATTERN:**
```javascript
it('should execute skill correctly', async () => {
await skill.execute(target);
assert.strictEqual(skill.usesCount, 1); // Tests usage tracking
assert.strictEqual(target.stats.hp, 80); // Tests damage application
assert.strictEqual(skill.canActivate, false); // Tests cooldown
assert.strictEqual(mockClient.messagesSent.length, 1); // Tests networking
// If this fails, which assertion failed? What broke?
});
```
**✅ CORRECT PATTERN:**
```javascript
describe('Skill execution', () => {
it('should increment usesCount after execution', async () => {
await skill.execute(target);
assert.strictEqual(skill.usesCount, 1);
});
it('should apply damage to target hp', async () => {
let initialHp = target.stats.hp;
await skill.execute(target);
assert.strictEqual(target.stats.hp, initialHp - skill.hitDamage);
});
it('should set canActivate to false during cooldown', async () => {
await skill.execute(target);
assert.strictEqual(skill.canActivate, false);
});
it('should send execution message to client', async () => {
await sender.sendSkillExecutionData(skill, target);
assert.strictEqual(mockClient.messagesSent.length, 1);
});
});
```
**Exception - Related Assertions:**
```javascript
it('should calculate damage correctly with attack and defense', async () => {
mockOwner.atk = 100;
mockTarget.def = 50;
let result = await skill.execute(mockTarget);
// These are all related to damage calculation
assert.ok(result.damages);
assert.ok(result.damages.hp);
assert.strictEqual(typeof result.damages.hp, 'number');
assert.ok(result.damages.hp > 0);
});
```
## COMMON TESTING ANTI-PATTERNS
### Anti-Pattern #1: Testing Private Methods
**❌ DON'T:**
```javascript
it('should call private _calculateDamage method', async () => {
let damage = skill._calculateDamage(target);
assert.ok(damage > 0);
});
```
**✅ DO:**
```javascript
it('should apply calculated damage to target', async () => {
let initialHp = target.stats.hp;
await skill.execute(target);
assert.ok(target.stats.hp < initialHp);
});
```
### Anti-Pattern #2: Over-Mocking
**❌ DON'T:**
```javascript
it('should execute skill', async () => {
// Mocking everything - not testing real behavior
skill.validate = () => true;
skill.runSkillLogic = () => ({success: true});
skill.fireEvent = () => {};
let result = await skill.execute(target);
assert.strictEqual(result.success, true); // Not testing anything real
});
```
**✅ DO:**
```javascript
it('should execute skill', async () => {
// Only mock external dependencies
let skill = new Attack({
owner: mockOwner, // Mock
hitDamage: 50
});
let result = await skill.execute(mockTarget); // Real execution
assert.ok(result.damages); // Real result
});
```
### Anti-Pattern #3: Testing Framework Code
**❌ DON'T:**
```javascript
it('should fire event through EventsManager', async () => {
let eventFired = false;
EventsManagerSingleton.on('test.event', () => {
eventFired = true;
});
await EventsManagerSingleton.emit('test.event');
assert.strictEqual(eventFired, true); // Testing EventsManager, not your code
});
```
**✅ DO:**
```javascript
it('should fire LEVEL_UP event when leveling up', async () => {
let eventFired = false;
classPath.listenEvent(SkillsEvents.LEVEL_UP, () => {
eventFired = true;
}, 'test-level-up');
await classPath.levelUp(); // Testing your code
assert.strictEqual(eventFired, true);
});
```
### Anti-Pattern #4: Assertion-less Tests
**❌ DON'T:**
```javascript
it('should execute skill without errors', async () => {
await skill.execute(target);
// No assertions - test passes even if skill is broken
});
```
**✅ DO:**
```javascript
it('should execute skill without errors', async () => {
let result = await skill.execute(target);
assert.ok(result); // At minimum, verify result exists
assert.strictEqual(result.error, undefined); // Verify no error
});
```
### Anti-Pattern #5: Dependent Tests
**❌ DON'T:**
```javascript
let sharedSkill;
it('test 1 - create skill', async () => {
sharedSkill = new Skill({owner: mockOwner});
});
it('test 2 - use skill', async () => {
// ❌ Depends on test 1 running first
await sharedSkill.execute(target);
});
```
**✅ DO:**
```javascript
it('test 1 - create skill', async () => {
let skill = new Skill({owner: mockOwner});
// Test skill creation
});
it('test 2 - use skill', async () => {
let skill = new Skill({owner: mockOwner}); // ✓ Independent
await skill.execute(target);
});
```
## PROVEN TESTING PATTERNS
### Pattern #1: Arrange-Act-Assert (AAA)
```javascript
it('should apply damage to target hp', async () => {
// ARRANGE - Set up test data
let initialHp = mockTarget.stats.hp;
let skill = new Attack({
owner: mockOwner,
hitDamage: 50,
affectedProperty: 'hp'
});
// ACT - Execute the behavior
await skill.execute(mockTarget);
// ASSERT - Verify the outcome
assert.strictEqual(mockTarget.stats.hp, initialHp - 50);
});
```
### Pattern #2: Setup and Teardown
```javascript
describe('Attack Skills', () => {
let mockOwner, mockTarget, skill;
beforeEach(() => {
// SETUP - Run before each test
mockOwner = new MockOwner();
mockTarget = new MockTarget();
skill = new Attack({
owner: mockOwner,
hitDamage: 50
});
});
afterEach(async () => {
// TEARDOWN - Clean up after each test
await TestHelpers.clearEventListeners();
});
it('test 1', async () => {
// Use fresh instances
});
it('test 2', async () => {
// Use fresh instances
});
});
```
---
### Pattern #3: Parameterized Tests
```javascript
describe('Damage calculation with various stats', () => {
const testCases = [
{atk: 100, def: 0, expectedDamage: 50},
{atk: 100, def: 50, expectedDamage: 25},
{atk: 100, def: 100, expectedDamage: 0},
{atk: 50, def: 100, expectedDamage: 0}
];
testCases.forEach(({atk, def, expectedDamage}) => {
it(`should deal ${expectedDamage} damage when atk=${atk} and def=${def}`, async () => {
mockOwner.atk = atk;
mockTarget.def = def;
let skill = new Attack({
owner: mockOwner,
hitDamage: 50,
attackProperties: ['atk'],
defenseProperties: ['def']
});
let result = await skill.execute(mockTarget);
assert.strictEqual(result.damages.hp, expectedDamage);
});
});
});
```
### Pattern #4: Test Doubles (Mock, Stub, Spy)
**Mock - Verify interaction:**
```javascript
it('should call executePhysicalSkill on owner', async () => {
let called = false;
let receivedTarget = null;
mockOwner.executePhysicalSkill = async (target, skill, callback) => {
called = true;
receivedTarget = target;
await callback(target);
};
let skill = new PhysicalAttack({
owner: mockOwner,
objectWidth: 10,
objectHeight: 10
});
await skill.execute(mockTarget);
assert.strictEqual(called, true);
assert.strictEqual(receivedTarget, mockTarget);
});
```
**Stub - Provide controlled response:**
```javascript
it('should handle out of range result', async () => {
mockOwner.isInRange = () => false; // Stub returns false
let skill = new Skill({
owner: mockOwner,
range: 100,
rangeAutomaticValidation: true
});
let result = skill.validate();
assert.strictEqual(result, false);
});
```
**Spy - Track calls:**
```javascript
it('should fire event multiple times', async () => {
let callCount = 0;
let receivedArgs = [];
skill.listenEvent(SkillsEvents.SKILL_AFTER_EXECUTE, (...args) => {
callCount++;
receivedArgs.push(args);
}, 'spy-listener');
await skill.execute(target1);
await skill.execute(target2);
assert.strictEqual(callCount, 2);
assert.strictEqual(receivedArgs.length, 2);
});
```
### Pattern #5: Error Testing
```javascript
it('should return error when target is out of range', async () => {
mockOwner.isInRange = () => false;
let skill = new Effect({
owner: mockOwner,
range: 100,
rangeAutomaticValidation: true
});
let result = await skill.execute(mockTarget);
assert.strictEqual(result.error, SKILL.OUT_OF_RANGE);
});
it('should return error when owner is casting', async () => {
mockOwner.isCasting = true;
let skill = new Skill({owner: mockOwner});
let result = await skill.execute(mockTarget);
assert.strictEqual(result.error, SKILL.CAN_NOT_ACTIVATE);
});
```
## BUG DISCOVERY PATTERNS
### Pattern: Proof Before Fix
**Process:**
1. Identify failing test
2. Read production code completely
3. Trace execution flow with debugger/logs
4. Find exact line causing issue
5. Understand why it's wrong
6. Provide proof (comparison to correct code, specification)
7. Apply fix
8. Verify fix resolves issue
**Example from Session:**
**Bug:** Skill.applyModifiers() applying critical to wrong value
**Proof Process:**
1. Test showed: Expected 100, got 120 (with 2x critical)
2. Read lib/skill.js:293-309 completely
3. Traced: modifier.value = 10, current = 80, critical = 2x
4. Found bug at line 307:
```javascript
newValue = this.applyCriticalValue(newValue); // Applies to (80+10)=90
```
5. Compared to Attack.js:101 (correct pattern):
```javascript
damage = damage + this.getCriticalDiff(damage); // Applies to damage only
```
6. Proof: Attack applies critical to damage value only, Effect was applying to sum
7. Applied fix using getCriticalDiff()
8. Test passed: 80 + (10*2) = 100 ✓
### Pattern: Explore Don't Guess
**❌ DON'T:**
```javascript
// Test is failing, let me try:
await TestHelpers.sleep(10); // Maybe timing issue?
// Still failing, let me try:
classPath.listenEvent(..., 'different-key'); // Maybe key issue?
// Still failing, let me try:
await classPath.levelUp(); await classPath.levelUp(); // Call twice?
// ❌ Guessing randomly
```
**✅ DO:**
```javascript
// Test is failing
// 1. Read the test completely
// 2. Read the production code completely
// 3. Add logging to trace execution
// 4. Find WHERE it fails
// 5. Find WHY it fails
// 6. Prove the root cause
// 7. Apply targeted fix
```
**Example from Session:**
**Issue:** REMOVE_SKILLS event listeners not firing
**Exploration:**
1. Read test completely - sees listeners registered, event fired, but flags stay false
2. Read ClassPath.listenEvent() - calls events.onWithKey()
3. Read EventsManagerSingleton - found removeKey registry
4. Read clearEventListeners() - clears listeners but NOT registry
5. Checked ADD_SKILLS test - used same removeKeys!
6. **Proof:** removeKey registry persists, causing collision
7. **Fix:** Use unique removeKeys
## TEST ORGANIZATION
### File Structure:
```
tests/
├── run-tests.js # Test runner
├── utils/
│ └── test-helpers.js # Shared utilities
├── fixtures/
│ ├── mocks/ # Mock classes
│ ├── skills/ # Skill test data
│ └── levels/ # Level test data
└── unit/
├── test-skill.js # Unit tests
├── test-levels-set.js
├── types/
│ ├── test-attack.js
│ └── test-effect.js
├── server/
│ └── test-sender.js
└── client/
└── test-receiver.js
```
### Test File Template:
```javascript
import {describe, it, beforeEach, afterEach} from 'node:test';
import assert from 'node:assert';
import {Skill} from '../../lib/skill.js';
import {MockOwner} from '../fixtures/mocks/mock-owner.js';
import {TestHelpers} from '../utils/test-helpers.js';
describe('Skill - Feature Description', () => {
let mockOwner, mockTarget, skill;
beforeEach(() => {
mockOwner = new MockOwner();
mockTarget = new MockTarget();
});
afterEach(async () => {
await TestHelpers.clearEventListeners();
});
describe('Sub-feature', () => {
it('should do X when Y', async () => {
// Arrange
// Act
// Assert
});
});
});
```
## DEBUGGING CHECKLIST
When a test fails:
- [ ] Read the complete test code
- [ ] Read the complete production code being tested
- [ ] Understand what the test expects
- [ ] Understand what the production code does
- [ ] Add logging to trace execution
- [ ] Identify the exact line that produces wrong result
- [ ] Understand WHY it's wrong
- [ ] Find proof (comparison, specification, other code)
- [ ] Apply targeted fix
- [ ] Verify fix works
- [ ] Check for similar issues in other code
- [ ] Update documentation if pattern discovered
**NEVER:**
- [ ] Guess randomly
- [ ] Try multiple fixes without understanding
- [ ] Use sleep() to mask race conditions
- [ ] Delete tests instead of fixing them
- [ ] Modify tests to match broken code
## CRITICAL LESSONS FROM SESSION
### Lesson #1: EventsManagerSingleton removeKey Registry Persists
**Discovery:** removeKey registry is NOT cleared by removeAllListeners()
**Impact:** Tests reusing removeKeys fail silently
**Solution:** Use unique removeKeys across all tests
**Pattern:**
```javascript
'{component}.{test-suite}.{specific-test}.{event-name}'
// Example: 'class-path.events.remove-skills.before-listener'
```
### Lesson #2: Critical Applies to Value Only, Not Sum
**Discovery:** Critical should apply to modifier/damage value, not to (current + value)
**Impact:** Critical damage/effects were doubled incorrectly
**Solution:** Use getCriticalDiff() to calculate bonus, add to result
**Pattern:**
```javascript
let newValue = modifier.getModifiedValue(); // current + value
newValue = newValue + this.getCriticalDiff(modifier.value); // + critical bonus
```
### Lesson #3: Test Methods Directly, Not Through Events
**Discovery:** Testing through events creates race conditions
**Impact:** Tests fail randomly due to timing
**Solution:** Call methods directly, only test events when testing event system
**Pattern:**
```javascript
// ❌ await classPath.levelUp(); assert(client.messagesSent);
// ✓ await sender.sendLevelUpData(classPath); assert(client.messagesSent);
```
### Lesson #4: sleep() is for Timers, Not Race Conditions
**Discovery:** sleep() masks async issues instead of fixing them
**Impact:** Tests become time-dependent and flaky
**Solution:** Fix test pattern (direct calls), only use sleep() for timer features
**Pattern:**
```javascript
// ❌ await action(); await sleep(10); assert(result);
// ✓ await directMethod(); assert(result);
// ✓ skill.validate(); await sleep(skillDelay + 10); assert(canActivate);
```
## REFERENCES
- Test Runner: `tests/run-tests.js`
- Test Helpers: `tests/utils/test-helpers.js`
- Mock Classes: `tests/fixtures/mocks/`
- Event System: `.claude/event-system-architecture.md`
- Skill Execution: `.claude/skill-execution-flow.md`