Files
fanuzu/app/firebase/functions/test/hibernation.test.ts
T

115 lines
5.7 KiB
TypeScript

import { afterAll, beforeAll, expect, it, vi } from 'vitest';
import { getFirestore, Timestamp, Transaction } from 'firebase-admin/firestore';
import { hibernationPatch, hibernationSettings } from '../src/fandom/hibernation.js';
import { fnFpiAggregate } from '../src/fandom/fandom.js';
import { bootAdmin, shutdownAdmin } from './support.js';
const config = { enabled: true, wakeFpi: 20, wakeSeconds: 60 };
const settings = hibernationSettings(config)!;
const day = 86_400_000;
const at = (ms: number) => Timestamp.fromMillis(ms);
let originalConfig: FirebaseFirestore.DocumentData | undefined;
beforeAll(async () => {
bootAdmin();
originalConfig = (await getFirestore().doc('config/hibernation').get()).data();
});
afterAll(async () => {
const db = getFirestore();
const ref = db.doc('config/hibernation');
if (originalConfig) await ref.set(originalConfig);
else await ref.delete();
await db.recursiveDelete(db.doc('fandoms/test-hibernation'));
await shutdownAdmin();
});
it('진입 기본값만 원문에서 가져오고 복귀 설정 누락·잘못된 값은 거부한다', () => {
expect(settings.enterFpi).toBe(10);
expect(settings.enterSeconds).toBe(30 * 86400);
expect(hibernationSettings({})).toBeNull();
for (const invalid of [
{ enabled: true }, { ...config, wakeFpi: 10 },
{ ...config, wakeSeconds: -1 }, { ...config, enterFpi: '10' },
]) expect(() => hibernationSettings(invalid)).toThrow();
expect(hibernationPatch({}, Number.NaN, settings, at(0))).toEqual({});
});
it('30일 연속 미만·경계값·복귀 유지시간·재시도·정책 변경을 구분한다', () => {
let state = hibernationPatch({}, 9, settings, at(0));
state = { ...state, ...hibernationPatch(state, 9, settings, at(30 * day - 1)) };
expect(state.isHibernating).toBe(false);
const interrupted = hibernationPatch(state, 10, settings, at(30 * day));
expect(interrupted.hibernationLowSince).toBeNull();
expect(interrupted.isHibernating).toBe(false);
const changed = hibernationPatch(state, 9, { ...settings, enterFpi: 11 }, at(30 * day));
expect(changed.isHibernating).toBe(false);
state = { ...state, ...hibernationPatch(state, 9, settings, at(30 * day)) };
expect(state.isHibernating).toBe(true);
expect(hibernationPatch(state, 99, settings, at(0))).toEqual({});
const noActivity = hibernationPatch(state, 99, settings, at(31 * day));
expect(noActivity.isHibernating).toBe(true);
expect(noActivity.hibernationWakeSince).toBeNull();
const activity = { id: 'new-activity', at: at(31 * day) };
state = { ...state, ...hibernationPatch(state, 20, settings, at(31 * day), activity) };
expect(state.isHibernating).toBe(true);
state = { ...state, ...hibernationPatch(state, 19, settings, at(31 * day + 30_000), activity) };
expect(state.hibernationWakeSince).toBeNull();
state = { ...state, ...hibernationPatch(state, 20, settings, at(32 * day), activity) };
expect(state.isHibernating).toBe(true);
state = { ...state, ...hibernationPatch(state, 20, settings, at(32 * day + 60_000), activity) };
expect(state.isHibernating).toBe(false);
});
it('실제 FPI 집계가 휴면을 함께 확정하며 sFPI나 별도 점수를 차감하지 않는다', async () => {
const db = getFirestore();
const ref = db.doc('fandoms/test-hibernation');
await db.doc('config/hibernation').set(config);
await ref.set({
stage: 1, sfpi: 100, fpiScore: 99,
...hibernationPatch({}, 0, settings, at(Date.now() - 31 * day)),
});
const create = vi.spyOn(Transaction.prototype, 'create').mockImplementationOnce(() => {
throw new Error('history write failed');
});
try {
await expect(fnFpiAggregate.run({ scheduleTime: new Date().toISOString() }))
.rejects.toThrow('history write failed');
} finally {
create.mockRestore();
}
expect((await ref.get()).get('isHibernating')).toBe(false);
expect((await ref.get()).get('fpiScore')).toBe(99);
expect((await ref.collection('stateHistory').get()).size).toBe(0);
await fnFpiAggregate.run({ scheduleTime: new Date().toISOString() });
const result = await ref.get();
expect(result.get('isHibernating')).toBe(true);
expect(result.get('fpiScore')).toBe(0);
expect(result.get('sfpi')).toBe(100);
expect((await ref.collection('stateHistory').get()).size).toBe(1);
await fnFpiAggregate.run({ scheduleTime: new Date().toISOString() });
expect((await ref.get()).get('hibernationChangedAt').toMillis())
.toBe(result.get('hibernationChangedAt').toMillis());
await db.doc('config/hibernation').set({ ...config, wakeSeconds: 0 });
const event = ref.collection('fpiEvents').doc('wake-activity');
await event.set({ axis: 'contribution', value: 100, verified: true,
occurredAt: Timestamp.now(), reasonCode: 'ECLIPSE_RESTORE' });
await fnFpiAggregate.run({ scheduleTime: new Date().toISOString() });
expect((await ref.get()).get('isHibernating')).toBe(true);
await event.update({ reasonCode: 'QUEST_ACTIVITY' });
await fnFpiAggregate.run({ scheduleTime: new Date().toISOString() });
const awake = await ref.get();
expect(awake.get('isHibernating')).toBe(false);
const history = await ref.collection('stateHistory').orderBy('occurredAt').get();
expect(history.size).toBe(2);
expect(history.docs.map((entry) => entry.get('kind'))).toEqual(['hibernation', 'wake_up']);
expect(history.docs[0]!.get('fpiScore')).toBe(0);
expect(history.docs[1]!.get('sourceEventId')).toBe('wake-activity');
expect(history.docs[1]!.get('settings').wakeSeconds).toBe(0);
expect(awake.get('hibernationWakeEventId')).toBeUndefined();
await Promise.all([
fnFpiAggregate.run({ scheduleTime: new Date().toISOString() }),
fnFpiAggregate.run({ scheduleTime: new Date().toISOString() }),
]);
expect((await ref.collection('stateHistory').get()).size).toBe(2);
expect(awake.get('sfpi')).toBe(100);
});