265 lines
8.5 KiB
TypeScript
265 lines
8.5 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { applyFilterRules } from '../src/core/filtering/filter-engine';
|
|
import { RawParticipant } from '../src/core/types/participant';
|
|
import { FilterRules, DEFAULT_FILTER_RULES } from '../src/core/types/giveaway';
|
|
import { computeConditionsHash } from '../src/core/randomizer/canonical';
|
|
import { verifyDrawResult, executeDeterministicDrawV1 } from '../src/core/randomizer/deterministic';
|
|
import { computeParticipantsSnapshotHash } from '../src/core/randomizer/canonical';
|
|
|
|
describe('Task 07: Duplicate Comments Rule & Backward Compatibility', () => {
|
|
// ─── 1. commentsCount merging accuracy (Fix || 1 bug) ─────────────────────────
|
|
it('duplicate participant entry with commentsCount: 0 does not artificially increment commentsCount', () => {
|
|
const rawEntries: RawParticipant[] = [
|
|
{
|
|
platformUserId: 'user_like_only',
|
|
firstName: 'Like',
|
|
lastName: 'Only',
|
|
source: 'LIKES',
|
|
liked: true,
|
|
commented: false,
|
|
commentsCount: 0,
|
|
reposted: false,
|
|
subscribed: true,
|
|
},
|
|
{
|
|
platformUserId: 'user_like_only',
|
|
firstName: 'Like',
|
|
lastName: 'Only',
|
|
source: 'LIKES',
|
|
liked: true,
|
|
commented: false,
|
|
commentsCount: 0,
|
|
reposted: false,
|
|
subscribed: true,
|
|
},
|
|
];
|
|
|
|
const result = applyFilterRules(rawEntries, DEFAULT_FILTER_RULES);
|
|
expect(result.allParticipants).toHaveLength(1);
|
|
expect(result.allParticipants[0].commentsCount).toBe(0);
|
|
});
|
|
|
|
it('merging like entry (0 comments) and comment entry (1 comment) accurately sums commentsCount to 1', () => {
|
|
const rawEntries: RawParticipant[] = [
|
|
{
|
|
platformUserId: 'user_mixed',
|
|
firstName: 'Mixed',
|
|
lastName: 'User',
|
|
source: 'LIKES',
|
|
liked: true,
|
|
commented: false,
|
|
commentsCount: 0,
|
|
reposted: false,
|
|
subscribed: true,
|
|
},
|
|
{
|
|
platformUserId: 'user_mixed',
|
|
firstName: 'Mixed',
|
|
lastName: 'User',
|
|
source: 'COMMENTS',
|
|
liked: false,
|
|
commented: true,
|
|
commentsCount: 1,
|
|
reposted: false,
|
|
subscribed: true,
|
|
},
|
|
];
|
|
|
|
const result = applyFilterRules(rawEntries, DEFAULT_FILTER_RULES);
|
|
expect(result.allParticipants).toHaveLength(1);
|
|
expect(result.allParticipants[0].liked).toBe(true);
|
|
expect(result.allParticipants[0].commented).toBe(true);
|
|
expect(result.allParticipants[0].commentsCount).toBe(1);
|
|
});
|
|
|
|
// ─── 2. Unconditional deduplication (1 user = 1 chance) ──────────────────────
|
|
it('unconditionally deduplicates participants so 1 user gets 1 chance regardless of comment volume', () => {
|
|
const rawEntries: RawParticipant[] = Array.from({ length: 10 }, (_, i) => ({
|
|
platformUserId: 'spammer_100',
|
|
firstName: 'Spammer',
|
|
lastName: 'User',
|
|
source: 'COMMENTS',
|
|
liked: true,
|
|
commented: true,
|
|
commentsCount: 1,
|
|
reposted: false,
|
|
subscribed: true,
|
|
}));
|
|
|
|
const result = applyFilterRules(rawEntries, {
|
|
...DEFAULT_FILTER_RULES,
|
|
requireComment: true,
|
|
});
|
|
|
|
expect(result.allParticipants).toHaveLength(1);
|
|
expect(result.eligibleParticipants).toHaveLength(1);
|
|
expect(result.allParticipants[0].commentsCount).toBe(10);
|
|
expect(result.allParticipants[0].eligible).toBe(true);
|
|
});
|
|
|
|
// ─── 3. Legacy snapshot verification compatibility ────────────────────────────
|
|
it('legacy snapshot with excludeDuplicateComments: true retains verified: true in verifyDrawResult', () => {
|
|
// 1. Simulate legacy filterRulesSnapshot with excludeDuplicateComments field
|
|
const legacyFilterRules = {
|
|
requireLike: true,
|
|
requireComment: false,
|
|
requireRepost: false,
|
|
requireSubscription: false,
|
|
excludeAdmins: false,
|
|
excludeBlacklistedIds: [],
|
|
excludeDuplicateComments: true,
|
|
minEligibleParticipants: 1,
|
|
};
|
|
|
|
const conditionsHash = computeConditionsHash(legacyFilterRules);
|
|
|
|
const eligibleParticipants = [
|
|
{
|
|
platformUserId: '101',
|
|
firstName: 'Alice',
|
|
lastName: 'A',
|
|
source: 'LIKES' as const,
|
|
liked: true,
|
|
commented: false,
|
|
commentsCount: 0,
|
|
reposted: false,
|
|
subscribed: true,
|
|
eligible: true,
|
|
exclusionReason: null,
|
|
},
|
|
{
|
|
platformUserId: '102',
|
|
firstName: 'Bob',
|
|
lastName: 'B',
|
|
source: 'LIKES' as const,
|
|
liked: true,
|
|
commented: false,
|
|
commentsCount: 0,
|
|
reposted: false,
|
|
subscribed: true,
|
|
eligible: true,
|
|
exclusionReason: null,
|
|
},
|
|
];
|
|
|
|
const participantsSnapshotHash = computeParticipantsSnapshotHash(eligibleParticipants);
|
|
|
|
const legacySnapshot = {
|
|
id: 'snap_legacy_001',
|
|
giveawayId: 'gw_legacy_001',
|
|
version: 1,
|
|
createdAt: '2026-08-15T12:00:00.000Z',
|
|
eligibleParticipants,
|
|
filterRulesSnapshot: legacyFilterRules,
|
|
participantCount: 2,
|
|
participantsSnapshotHash,
|
|
conditionsHash,
|
|
};
|
|
|
|
const seed = 'legacy_test_seed_1234567890abcdef1234567890abcdef';
|
|
|
|
// Execute draw with legacy snapshot
|
|
const drawResult = executeDeterministicDrawV1({
|
|
giveawayId: 'gw_legacy_001',
|
|
snapshot: legacySnapshot,
|
|
totalLoadedCount: 2,
|
|
winnersCount: 1,
|
|
reserveWinnersCount: 0,
|
|
seed,
|
|
});
|
|
|
|
// Independent verifyDrawResult replay
|
|
const verification = verifyDrawResult({
|
|
giveawayId: 'gw_legacy_001',
|
|
drawId: drawResult.drawId,
|
|
drawnAt: drawResult.drawnAt,
|
|
snapshot: legacySnapshot,
|
|
seed,
|
|
claimedWinnersCount: 1,
|
|
claimedReserveCount: 0,
|
|
claimedWinnerIds: drawResult.winnerIds,
|
|
claimedReserveWinnerIds: drawResult.reserveWinnerIds,
|
|
claimedDeterministicProofHash: drawResult.deterministicProofHash,
|
|
claimedAuditEventHash: drawResult.auditEventHash,
|
|
algorithmVersion: drawResult.algorithmVersion,
|
|
});
|
|
|
|
expect(verification.conditionsIntegrity).toBe(true);
|
|
expect(verification.participantsSnapshotIntegrity).toBe(true);
|
|
expect(verification.verified).toBe(true);
|
|
});
|
|
|
|
// ─── 4. New snapshot verification compatibility ───────────────────────────────
|
|
it('new snapshot without excludeDuplicateComments computes clean hash and verifies successfully', () => {
|
|
const newFilterRules: FilterRules = {
|
|
requireLike: true,
|
|
requireComment: true,
|
|
requireRepost: false,
|
|
requireSubscription: false,
|
|
excludeAdmins: false,
|
|
excludeBlacklistedIds: ['999'],
|
|
minEligibleParticipants: 1,
|
|
};
|
|
|
|
const conditionsHash = computeConditionsHash(newFilterRules);
|
|
|
|
const eligibleParticipants = [
|
|
{
|
|
platformUserId: '201',
|
|
firstName: 'Charlie',
|
|
lastName: 'C',
|
|
source: 'COMMENTS' as const,
|
|
liked: true,
|
|
commented: true,
|
|
commentsCount: 2,
|
|
reposted: false,
|
|
subscribed: true,
|
|
eligible: true,
|
|
exclusionReason: null,
|
|
},
|
|
];
|
|
|
|
const participantsSnapshotHash = computeParticipantsSnapshotHash(eligibleParticipants);
|
|
|
|
const newSnapshot = {
|
|
id: 'snap_new_001',
|
|
giveawayId: 'gw_new_001',
|
|
version: 1,
|
|
createdAt: '2026-08-21T12:00:00.000Z',
|
|
eligibleParticipants,
|
|
filterRulesSnapshot: newFilterRules,
|
|
participantCount: 1,
|
|
participantsSnapshotHash,
|
|
conditionsHash,
|
|
};
|
|
|
|
const seed = 'new_test_seed_1234567890abcdef1234567890abcdef';
|
|
|
|
const drawResult = executeDeterministicDrawV1({
|
|
giveawayId: 'gw_new_001',
|
|
snapshot: newSnapshot,
|
|
totalLoadedCount: 1,
|
|
winnersCount: 1,
|
|
reserveWinnersCount: 0,
|
|
seed,
|
|
});
|
|
|
|
const verification = verifyDrawResult({
|
|
giveawayId: 'gw_new_001',
|
|
drawId: drawResult.drawId,
|
|
drawnAt: drawResult.drawnAt,
|
|
snapshot: newSnapshot,
|
|
seed,
|
|
claimedWinnersCount: 1,
|
|
claimedReserveCount: 0,
|
|
claimedWinnerIds: drawResult.winnerIds,
|
|
claimedReserveWinnerIds: drawResult.reserveWinnerIds,
|
|
claimedDeterministicProofHash: drawResult.deterministicProofHash,
|
|
claimedAuditEventHash: drawResult.auditEventHash,
|
|
algorithmVersion: drawResult.algorithmVersion,
|
|
});
|
|
|
|
expect(verification.conditionsIntegrity).toBe(true);
|
|
expect(verification.verified).toBe(true);
|
|
});
|
|
});
|