From 9abf745f515fefa8b5b5d28308a32d7450af7bbe Mon Sep 17 00:00:00 2001 From: Yggdrasil Date: Wed, 14 Oct 2026 01:49:37 -0400 Subject: [PATCH 1/3] fix(reputation): history() returns newest-first with a total MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit history() ordered ASC and returned a bare array — the oldest snapshots, so the newest only surfaced on the last page and the client had no total to paginate against. Now DESC + findAndCount → {items, total}. Resolves #585 --- src/reputation/reputation.service.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/reputation/reputation.service.ts b/src/reputation/reputation.service.ts index 0533cd2f..e06675a7 100644 --- a/src/reputation/reputation.service.ts +++ b/src/reputation/reputation.service.ts @@ -57,17 +57,21 @@ export class ReputationService { async history( userId: string, options: ReputationHistoryOptions = {}, - ): Promise { + ): Promise<{ items: ReputationSnapshot[]; total: number }> { const limit = Math.min( Math.max(options.limit ?? REPUTATION_HISTORY_DEFAULT_LIMIT, 1), REPUTATION_HISTORY_MAX_LIMIT, ); const offset = Math.max(options.offset ?? 0, 0); - return this.snapshotRepo.find({ + // DESC — newest first. ASC returned the OLDEST `limit` snapshots, so the + // newest only appeared on the last page and a client needed the total to + // reach them (#585). findAndCount also returns the real total. + const [items, total] = await this.snapshotRepo.findAndCount({ where: { userId }, - order: { computedAt: 'ASC' }, + order: { computedAt: 'DESC' }, take: limit, skip: offset, }); + return { items, total }; } } From 96263e34802cebb1268b3b839eceb1b0367dc76a Mon Sep 17 00:00:00 2001 From: yggdrasilworld Date: Sun, 18 Oct 2026 21:25:39 -0400 Subject: [PATCH 2/3] test(reputation): align the history spec with findAndCount (#585) history() switched to findAndCount so callers get the real total; the spec still mocked/asserted find. --- src/reputation/reputation.service.spec.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/reputation/reputation.service.spec.ts b/src/reputation/reputation.service.spec.ts index 55570dde..ab407b06 100644 --- a/src/reputation/reputation.service.spec.ts +++ b/src/reputation/reputation.service.spec.ts @@ -23,6 +23,7 @@ describe('ReputationService', () => { save: jest.fn((entity) => Promise.resolve({ id: 'snap-1', ...entity })), findOne: jest.fn(), find: jest.fn(), + findAndCount: jest.fn(), }; beforeEach(async () => { @@ -94,14 +95,14 @@ describe('ReputationService', () => { }); it('paginates history with a default/max limit', async () => { - mockSnapshotRepo.find.mockResolvedValue([]); + mockSnapshotRepo.findAndCount.mockResolvedValue([[], 0]); await service.history('user-1'); - expect(mockSnapshotRepo.find).toHaveBeenCalledWith( + expect(mockSnapshotRepo.findAndCount).toHaveBeenCalledWith( expect.objectContaining({ take: 50, skip: 0 }), ); await service.history('user-1', { limit: 1000, offset: 10 }); - expect(mockSnapshotRepo.find).toHaveBeenCalledWith( + expect(mockSnapshotRepo.findAndCount).toHaveBeenCalledWith( expect.objectContaining({ take: 100, skip: 10 }), ); }); From ba932341ccbd9085970ebca173b46df8667d9da0 Mon Sep 17 00:00:00 2001 From: yggdrasilworld Date: Sun, 18 Oct 2026 21:50:12 -0400 Subject: [PATCH 3/3] fix: repair merge glue that left the branch uncompilable The shared base carried half-written merge artifacts: an orphaned invokeOnLockedEscrow block in escrow.service.ts, duplicated returns plus a dead parsePagination in sponsors.controller.ts, and missing describe/it closers in github-webhooks.service.spec.ts. Aligned each with the intended form so the branch typechecks and its specs load. --- src/escrow/escrow.service.ts | 10 ++-------- src/github/github-webhooks.service.spec.ts | 3 +++ src/sponsors/sponsors.controller.ts | 21 --------------------- 3 files changed, 5 insertions(+), 29 deletions(-) diff --git a/src/escrow/escrow.service.ts b/src/escrow/escrow.service.ts index 6d2dd289..c252993f 100644 --- a/src/escrow/escrow.service.ts +++ b/src/escrow/escrow.service.ts @@ -584,20 +584,14 @@ export class EscrowService { recipients: Array<[string, number]>, manager?: EntityManager, ): Promise { - return this.invokeOnLockedEscrow(escrow, operation, () => - this.soroban.invoke( - 'release', - // `release(issue_id: u64, recipients)` — u64-typed on-chain (#301). - [u64(this.onChainKeyFor(escrow)), recipients], - this.contractOpts(escrow), - ), return this.invokeOnLockedEscrow( escrow, operation, () => this.soroban.invoke( 'release', - [this.onChainKeyFor(escrow), recipients], + // `release(issue_id: u64, recipients)` — u64-typed on-chain (#301). + [u64(this.onChainKeyFor(escrow)), recipients], this.contractOpts(escrow), ), manager, diff --git a/src/github/github-webhooks.service.spec.ts b/src/github/github-webhooks.service.spec.ts index 45bb6fe8..a35ac3be 100644 --- a/src/github/github-webhooks.service.spec.ts +++ b/src/github/github-webhooks.service.spec.ts @@ -869,6 +869,9 @@ describe('GithubWebhooksService', () => { expect(bountiesService.markInReview).not.toHaveBeenCalled(); expect(bountiesService.markMergedAndRelease).not.toHaveBeenCalled(); + }); + }); + // #308: a redelivered X-GitHub-Delivery used to hit the unique constraint // on webhook_events.deliveryId and escape handleEvent as a 500, so every // redelivery of that event failed forever. diff --git a/src/sponsors/sponsors.controller.ts b/src/sponsors/sponsors.controller.ts index b7a48cca..c097bf1e 100644 --- a/src/sponsors/sponsors.controller.ts +++ b/src/sponsors/sponsors.controller.ts @@ -26,25 +26,6 @@ function validatePage(limit?: number, offset?: number) { throw new BadRequestException('offset must be >= 0'); } return { limit, offset }; -/** - * Parse optional limit/offset query params, rejecting NaN, negatives, - * non-integers, and limits above MAX_PAGE_LIMIT with a 400 (#280). - */ -function parsePagination(limit?: string, offset?: string) { - const parse = (name: string, raw: string | undefined, min: number, max: number) => { - if (raw === undefined || raw === '') return undefined; - const n = Number(raw); - if (!Number.isInteger(n) || n < min || n > max) { - throw new BadRequestException( - `${name} must be an integer between ${min} and ${max}`, - ); - } - return n; - }; - return { - limit: parse('limit', limit, 1, MAX_PAGE_LIMIT), - offset: parse('offset', offset, 0, Number.MAX_SAFE_INTEGER), - }; } @ApiTags('sponsors') @@ -73,7 +54,6 @@ export class SponsorsController { ) { this.assertOwnsSponsor(user, id); return this.sponsorsService.dashboard(id, validatePage(limit, offset)); - return this.sponsorsService.dashboard(id, parsePagination(limit, offset)); } @ApiBearerAuth() @@ -91,6 +71,5 @@ export class SponsorsController { id, validatePage(limit, offset), ); - return this.sponsorsService.milestoneProgress(id, parsePagination(limit, offset)); } }