115 lines
5.7 KiB
TypeScript
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);
|
|
});
|