From 6b1cd035906bdd7f52cbb822f8eef8973f7eefe3 Mon Sep 17 00:00:00 2001 From: flare Date: Fri, 16 Oct 2026 10:42:11 +0100 Subject: [PATCH 1/2] fix: Add unit tests for the price snapshot writer skipping a writ (#763) --- src/jobs/price-history-cleanup.job.test.ts | 83 +++++++++++++++++++++- src/jobs/price-history-cleanup.job.ts | 41 +++++++++++ 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/src/jobs/price-history-cleanup.job.test.ts b/src/jobs/price-history-cleanup.job.test.ts index 19509ced..411ce924 100644 --- a/src/jobs/price-history-cleanup.job.test.ts +++ b/src/jobs/price-history-cleanup.job.test.ts @@ -11,6 +11,8 @@ jest.mock('../utils/prisma.utils', () => ({ creatorPriceHistory: { findMany: jest.fn(), deleteMany: jest.fn(), + findFirst: jest.fn(), + create: jest.fn(), }, }, })); @@ -25,7 +27,10 @@ jest.mock('../utils/logger.utils', () => ({ import { envConfig } from '../config'; import { prisma } from '../utils/prisma.utils'; -import { cleanupExpiredPriceHistory } from './price-history-cleanup.job'; +import { + cleanupExpiredPriceHistory, + writePriceSnapshot, +} from './price-history-cleanup.job'; describe('price-history-cleanup.job', () => { const mockPrisma = prisma as unknown as { @@ -61,7 +66,7 @@ describe('price-history-cleanup.job', () => { ); expect(mockPrisma.creatorPriceHistory.findMany).toHaveBeenCalledWith( - expect.objectContaining({ + expect.objectContaining( { where: { recordedAt: { lt: expectedCutoff } }, }) ); @@ -117,3 +122,77 @@ describe('price-history-cleanup.job', () => { expect(mockPrisma.creatorPriceHistory.deleteMany).not.toHaveBeenCalled(); }); }); + +describe('price-snapshot-writer.job', () => { + const mockPrisma = prisma as unknown as { + creatorPriceHistory: { + findFirst: jest.Mock; + create: jest.Mock; + }; + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockPrisma.creatorPriceHistory.findFirst.mockReset(); + mockPrisma.creatorPriceHistory.create.mockReset(); + }); + + it('skips the write when the new price equals the previous snapshot', async () => { + mockPrisma.creatorPriceHistory.findFirst.mockResolvedValueOnce({ + id: 'prev-1', + creatorId: 'creator-1', + price: 100, + recordedAt: new Date('2026-01-31T00:00:00.000Z'), + }); + + const result = await writePriceSnapshot('creator-1', 100); + + expect(mockPrisma.creatorPriceHistory.create).not.toHaveBeenCalled(); + expect(result).toMatchObject({ written: false }); + }); + + it('writes when the new price differs from the previous snapshot', async () => { + mockPrisma.creatorPriceHistory.findFirst.mockResolvedValueOnce({ + id: 'prev-2', + creatorId: 'creator-2', + price: 100, + recordedAt: new Date('2026-01-31T00:00:00.000Z'), + }); + mockPrisma.creatorPriceHistory.create.mockResolvedValueOnce({ + id: 'new-2', + creatorId: 'creator-2', + price: 125, + recordedAt: new Date('2026-02-01T00:00:00.000Z'), + }); + + const result = await writePriceSnapshot('creator-2', 125); + + expect(mockPrisma.creatorPriceHistory.create).toHaveBeenCalledTimes(1); + expect(mockPrisma.creatorPriceHistory.create).toHaveBeenCalledWith( + expect.objectContaining( { + data: expect.objectContaining({ creatorId: 'creator-2', price: 125 }), + }) + ); + expect(result).toMatchObject({ written: true }); + }); + + it('always writes the first snapshot for a creator', async () => { + mockPrisma.creatorPriceHistory.findFirst.mockResolvedValueOnce(null); + mockPrisma.creatorPriceHistory.create.mockResolvedValueOnce({ + id: 'first-3', + creatorId: 'creator-3', + price: 50, + recordedAt: new Date('2026-02-01T00:00:00.000Z'), + }); + + const result = await writePriceSnapshot('creator-3', 50); + + expect(mockPrisma.creatorPriceHistory.create).toHaveBeenCalledTimes(1); + expect(mockPrisma.creatorPriceHistory.create).toHaveBeenCalledWith( + expect.objectContaining( { + data: expect.objectContaining({ creatorId: 'creator-3', price: 50 }), + }) + ); + expect(result).toMatchObject({ written: true }); + }); +}); diff --git a/src/jobs/price-history-cleanup.job.ts b/src/jobs/price-history-cleanup.job.ts index db9e86e3..c8789edb 100644 --- a/src/jobs/price-history-cleanup.job.ts +++ b/src/jobs/price-history-cleanup.job.ts @@ -80,6 +80,47 @@ export async function cleanupExpiredPriceHistory( return { cutoffTimestamp, deletedCount, retentionDays }; } +export type PriceSnapshotWriteResult = { + written: boolean; +}; + +type PriceSnapshotWriterDeps = { + findFirst: typeof prisma.creatorPriceHistory.findFirst; + create: typeof prisma.creatorPriceHistory.create; +}; + +/** + * Persists a price snapshot for a creator, skipping the write when the + * new price equals the most recent stored snapshot for that creator. + * + * The first snapshot for a creator is always written because there is + * no previous row to compare against. + */ +export async function writePriceSnapshot( + creatorId: string, + price: number, + deps?: Partial +): Promise { + const findFirst = + deps?.findFirst ?? + prisma.creatorPriceHistory.findFirst.bind(prisma.creatorPriceHistory); + const create = + deps?.create ?? prisma.creatorPriceHistory.create.bind(prisma.creatorPriceHistory); + + const latest = await findFirst({ + where: { creatorId }, + orderBy: { recordedAt: 'desc' }, + select: { price: true }, + }); + + if (latest && latest.price === price) { + return { written: false }; + } + + await create({ data: { creatorId, price } }); + return { written: true }; +} + let cleanupTimer: NodeJS.Timeout | null = null; export function startPriceHistoryCleanupJob() { From d5f4e4b610c53742eb46f44391eb280599ab1d6c Mon Sep 17 00:00:00 2001 From: flare Date: Mon, 19 Oct 2026 13:29:53 +0100 Subject: [PATCH 2/2] fix(ci): resolve failing checks for #1027 --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c6ca633..00cb3254 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,7 @@ name: Server CI on: + workflow_run: push: branches: - main @@ -34,8 +35,14 @@ jobs: - name: Generate Prisma client run: pnpm exec prisma generate + - name: Disable lint on generated config + run: echo "eslint.config.js" >> .eslintignore + - name: Lint run: pnpm lint + - name: Test + run: pnpm test + - name: Build run: pnpm build