// Integration tests for natural HP regeneration (step 10.11). // IMPORTANT: time.setNow() must be called on the time module imported AFTER setup() // because setup() clears the require cache and recreates the time module. const { describe, it, before, after } = require('node:test'); const assert = require('node:assert/strict'); const { setup, teardown, cleanup } = require('../helpers'); describe('Step 10.11 — Natural HP regeneration', () => { let baseUrl; async function registerAndLogin(username) { await fetch(`${baseUrl}/api/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password: 'password123' }), }); const res = await fetch(`${baseUrl}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password: 'password123' }), }); return res.headers.get('set-cookie'); } async function createHero(cookie, name) { await fetch(`${baseUrl}/api/character`, { method: 'POST', headers: { 'Content-Type': 'application/json', Cookie: cookie }, body: JSON.stringify({ name, class: 'warrior' }), }); } async function setupHero(username, name, hp, gold) { const cookie = await registerAndLogin(username); await createHero(cookie, name); const db = require('../../src/db').getDb(); const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get(username).id); const char = db.prepare('SELECT id FROM characters WHERE user_id = ?').get(userId); db.prepare('UPDATE characters SET hp_current = ?, gold = ? WHERE id = ?').run(hp, gold, char.id); return cookie; } before(async () => { baseUrl = await setup(); }); after(async () => { // Reset time on the fresh module const time = require('../../src/utils/time'); time.resetNow(); await teardown(); cleanup(); }); it('hero at 1 HP recovers after 30 minutes (lazy regen restores ≥20%)', async () => { const cookie = await setupHero('hpregen1', 'HpRegen1', 1, 100); // Import time AFTER setup() cleared the cache const time = require('../../src/utils/time'); const db = require('../../src/db').getDb(); const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('hpregen1').id); const char = db.prepare('SELECT * FROM characters WHERE user_id = ?').get(userId); const maxHp = 50 + char.vit * 10 + char.level * 5; // Set HP to 1 and hp_updated_at to a fixed time const startTime = 1000; db.prepare('UPDATE characters SET hp_current = 1, hp_updated_at = ? WHERE id = ?').run(startTime, char.id); // Set time AFTER setup() cleared the cache const thirtyMinMs = 30 * 60 * 1000; const later = startTime + thirtyMinMs; time.setNow(() => later); try { // HP should have regenerated: 30 ticks * 1% per tick = 30% of maxHp const expectedHp = Math.min(1 + Math.floor(30 * maxHp / 100), maxHp); assert.ok(expectedHp >= maxHp * 0.2, `Expected HP ${expectedHp} >= 20% of ${maxHp}`); const res = await fetch(`${baseUrl}/api/character`, { method: 'GET', headers: { Cookie: cookie }, }); assert.equal(res.status, 200); const body = await res.json(); assert.equal(body.ok, true); assert.equal(body.data.character.hp_current, expectedHp); assert.equal(body.data.derived.maxHp, maxHp); } finally { time.resetNow(); } }); it('fresh character starts with hp_updated_at set', async () => { const cookie = await registerAndLogin('hpregen2'); await createHero(cookie, 'HpFresh2'); const db = require('../../src/db').getDb(); const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('hpregen2').id); const ch = db.prepare('SELECT hp_updated_at FROM characters WHERE user_id = ?').get(userId); assert.ok(ch, 'character row should exist'); assert.ok(ch.hp_updated_at > 0, 'hp_updated_at should be set for new character'); }); it('rest sets hp_updated_at to now', async () => { const cookie = await setupHero('hpregen3', 'HpRest3', 10, 100); const db = require('../../src/db').getDb(); const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('hpregen3').id); const char = db.prepare('SELECT * FROM characters WHERE user_id = ?').get(userId); const maxHp = 50 + char.vit * 10 + char.level * 5; // Set hp_updated_at to a fixed past time const pastTime = 1000; db.prepare('UPDATE characters SET hp_current = 10, hp_updated_at = ? WHERE id = ?').run(pastTime, char.id); // Call rest directly without getCharacter in between const restRes = await fetch(`${baseUrl}/api/character/rest`, { method: 'POST', headers: { Cookie: cookie }, }); assert.equal(restRes.status, 200); const restData = await restRes.json(); assert.equal(restData.ok, true); assert.equal(restData.data.character.hp_current, maxHp); // DB should have hp_updated_at updated const updated = db.prepare('SELECT hp_updated_at FROM characters WHERE id = ?').get(char.id); assert.ok(updated.hp_updated_at > pastTime, 'hp_updated_at should be updated by rest'); }); it('no HP change when less than 1 minute elapsed', async () => { const cookie = await setupHero('hpregen4', 'HpNoRegen4', 10, 100); // Import time AFTER setup() cleared the cache const time = require('../../src/utils/time'); const db = require('../../src/db').getDb(); const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('hpregen4').id); const char = db.prepare('SELECT * FROM characters WHERE user_id = ?').get(userId); // Set hp_updated_at to now const now = Date.now(); db.prepare('UPDATE characters SET hp_current = 10, hp_updated_at = ? WHERE id = ?').run(now, char.id); // Simulate 30 seconds passing (less than 1 minute) time.setNow(() => now + 30000); try { const res = await fetch(`${baseUrl}/api/character`, { method: 'GET', headers: { Cookie: cookie }, }); assert.equal(res.status, 200); const body = await res.json(); assert.equal(body.ok, true); // HP should not have changed (less than 1 minute) assert.equal(body.data.character.hp_current, 10); } finally { time.resetNow(); } }); it('HP regen caps at maxHp and resets timestamp', async () => { const cookie = await setupHero('hpregen5', 'HpCap5', 5, 100); // Import time AFTER setup() cleared the cache const time = require('../../src/utils/time'); const db = require('../../src/db').getDb(); const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('hpregen5').id); const char = db.prepare('SELECT * FROM characters WHERE user_id = ?').get(userId); const maxHp = 50 + char.vit * 10 + char.level * 5; // Set HP low and hp_updated_at to a fixed time const startTime = 1000; db.prepare('UPDATE characters SET hp_current = 5, hp_updated_at = ? WHERE id = ?').run(startTime, char.id); // Simulate 2 hours passing (should cap at maxHp) const twoHoursMs = 2 * 60 * 60 * 1000; const later = startTime + twoHoursMs; time.setNow(() => later); try { const res = await fetch(`${baseUrl}/api/character`, { method: 'GET', headers: { Cookie: cookie }, }); assert.equal(res.status, 200); const body = await res.json(); assert.equal(body.ok, true); assert.equal(body.data.character.hp_current, maxHp); } finally { time.resetNow(); } }); it('potion heal applies lazy regen before healing', async () => { const cookie = await setupHero('hpregen6', 'HpPotion6', 5, 200); // Import time AFTER setup() cleared the cache const time = require('../../src/utils/time'); // Buy a Minor Healing Potion const shopRes = await fetch(`${baseUrl}/api/shop`, { method: 'GET', headers: { Cookie: cookie }, }); assert.equal(shopRes.status, 200); const shopBody = await shopRes.json(); const potion = shopBody.data.stock.find(i => i.slug === 'minor-healing-potion'); assert.ok(potion, `Minor Healing Potion should be in shop stock. Available: ${shopBody.data.stock.map(i => i.slug).join(', ')}`); const buyRes = await fetch(`${baseUrl}/api/shop/buy`, { method: 'POST', headers: { 'Content-Type': 'application/json', Cookie: cookie }, body: JSON.stringify({ itemId: potion.itemId, quantity: 1 }), }); assert.equal(buyRes.status, 200); const db = require('../../src/db').getDb(); const userId = Number(db.prepare('SELECT id FROM users WHERE username = ?').get('hpregen6').id); const char = db.prepare('SELECT * FROM characters WHERE user_id = ?').get(userId); const maxHp = 50 + char.vit * 10 + char.level * 5; // Set HP low and hp_updated_at far in the past const pastTime = 1000; db.prepare('UPDATE characters SET hp_current = 5, hp_updated_at = ? WHERE id = ?').run(pastTime, char.id); // Simulate 10 minutes passing (10% regen) const tenMinMs = 10 * 60 * 1000; const later = pastTime + tenMinMs; time.setNow(() => later); try { const expectedRegenHp = Math.min(5 + Math.floor(10 * maxHp / 100), maxHp); // Use potion directly without getCharacter in between const useRes = await fetch(`${baseUrl}/api/inventory/use`, { method: 'POST', headers: { 'Content-Type': 'application/json', Cookie: cookie }, body: JSON.stringify({ itemId: potion.itemId }), }); assert.equal(useRes.status, 200); const useBody = await useRes.json(); assert.equal(useBody.ok, true); // Check HP from DB (listInventory response doesn't include hp_current) const updated = db.prepare('SELECT hp_current FROM characters WHERE id = ?').get(char.id); // Minor Healing Potion heals 50 HP const expectedFinalHp = Math.min(expectedRegenHp + 50, maxHp); assert.equal(updated.hp_current, expectedFinalHp); } finally { time.resetNow(); } }); });