From 0848ba01930162429e3f563d8209acbe87e3c4ce Mon Sep 17 00:00:00 2001 From: Yggdrasil Date: Wed, 14 Oct 2026 02:38:26 -0400 Subject: [PATCH 1/3] fix(api): GET /milestones + /maintenance-pools load the relations the UI renders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both list() were bare find() — no relations, so the frontend's adaptMilestone/adaptMaintenancePool got empty rows (no repository, sponsor, escrow, issues). Resolves #591 --- src/maintenance-pool/maintenance-pool.service.ts | 6 +++++- src/milestones/milestones.service.ts | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/maintenance-pool/maintenance-pool.service.ts b/src/maintenance-pool/maintenance-pool.service.ts index 13415a62..86ac588b 100644 --- a/src/maintenance-pool/maintenance-pool.service.ts +++ b/src/maintenance-pool/maintenance-pool.service.ts @@ -196,6 +196,10 @@ export class MaintenancePoolService { } async list(): Promise { - return this.poolRepo.find(); + // Load repository/sponsor/escrow — the frontend's adaptMaintenancePool + // needs them; a bare find() returned rows with no relations (#591). + return this.poolRepo.find({ + relations: ['repository', 'createdBy', 'escrow'], + }); } } diff --git a/src/milestones/milestones.service.ts b/src/milestones/milestones.service.ts index c2b7ee8b..fe21ca75 100644 --- a/src/milestones/milestones.service.ts +++ b/src/milestones/milestones.service.ts @@ -214,7 +214,12 @@ export class MilestonesService { } async list(): Promise { - return this.milestoneRepo.find(); + // Load the relations the frontend's adaptMilestone renders — repository + // (org/repo display), sponsor, escrow (fund status), issues (the linked + // issues). A bare find() returned empty rows (#591). + return this.milestoneRepo.find({ + relations: ['repository', 'sponsor', 'escrow', 'issues'], + }); } allocateBudget(id: string) { From c6094bec3d46a57b0be5fb756ee2b1ceb8c83261 Mon Sep 17 00:00:00 2001 From: yggdrasilworld Date: Sun, 18 Oct 2026 21:50:18 -0400 Subject: [PATCH 2/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)); } } From 9195bd4c7596d32506b08f507eacda134400311b Mon Sep 17 00:00:00 2001 From: yggdrasilworld Date: Sun, 18 Oct 2026 21:57:42 -0400 Subject: [PATCH 3/3] test(milestones): align the specs with caller-bound + transactional signatures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fund(id, funderAddress, callerUserId) and create(dto, callerUserId) gained the authenticated-caller argument — pass it at the call sites - releasePartial now receives the outer transaction manager (5th arg) - OPEN -> IN_PROGRESS resolves to InvalidMilestoneTransitionError on this branch (the accepting-distributions check is the sibling's #578) - milestones.controller.spec gains the IdempotencyKey repository mock --- src/milestones/milestones.controller.spec.ts | 32 ++++++++++++++++---- src/milestones/milestones.service.spec.ts | 13 +++++--- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/milestones/milestones.controller.spec.ts b/src/milestones/milestones.controller.spec.ts index 8d87497d..06a90603 100644 --- a/src/milestones/milestones.controller.spec.ts +++ b/src/milestones/milestones.controller.spec.ts @@ -1,3 +1,4 @@ +import type { Request } from 'express'; import { Test, TestingModule } from '@nestjs/testing'; import { plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; @@ -5,8 +6,10 @@ import { MilestonesController } from './milestones.controller'; import { MilestonesService } from './milestones.service'; import { CreateMilestoneDto } from './dto/create-milestone.dto'; import { AssetType, MilestoneStatus } from '../common/enums'; +import { getRepositoryToken } from '@nestjs/typeorm'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { RolesGuard } from '../auth/guards/roles.guard'; +import { IdempotencyKey } from '../common/entities/idempotency-key.entity'; describe('MilestonesController', () => { let controller: MilestonesController; @@ -33,7 +36,13 @@ describe('MilestonesController', () => { const module: TestingModule = await Test.createTestingModule({ controllers: [MilestonesController], - providers: [{ provide: MilestonesService, useValue: milestonesService }], + providers: [ + { provide: MilestonesService, useValue: milestonesService }, + { + provide: getRepositoryToken(IdempotencyKey), + useValue: { findOne: jest.fn(), save: jest.fn(), create: jest.fn() }, + }, + ], }) .overrideGuard(JwtAuthGuard) .useValue({ canActivate: () => true }) @@ -53,9 +62,14 @@ describe('MilestonesController', () => { asset: AssetType.USDC, }; - await controller.create(dto); + await controller.create(dto, { + user: { userId: 'sponsor-1' }, + } as unknown as Request); - expect(milestonesService.create).toHaveBeenCalledWith(dto); + expect(milestonesService.create).toHaveBeenCalledWith( + dto, + 'sponsor-1', + ); }); }); @@ -77,9 +91,15 @@ describe('MilestonesController', () => { describe('fund', () => { it('calls milestonesService.fund with id and funderAddress', async () => { - await controller.fund('m1', { funderAddress: 'GFUNDER' }); - - expect(milestonesService.fund).toHaveBeenCalledWith('m1', 'GFUNDER'); + await controller.fund('m1', { funderAddress: 'GFUNDER' }, { + user: { userId: 'sponsor-1' }, + } as unknown as Request); + + expect(milestonesService.fund).toHaveBeenCalledWith( + 'm1', + 'GFUNDER', + 'sponsor-1', + ); }); }); diff --git a/src/milestones/milestones.service.spec.ts b/src/milestones/milestones.service.spec.ts index 612bde8d..e851e8a9 100644 --- a/src/milestones/milestones.service.spec.ts +++ b/src/milestones/milestones.service.spec.ts @@ -6,6 +6,7 @@ import { MilestonesService } from './milestones.service'; import { EscrowService } from '../escrow/escrow.service'; import { Issue, Milestone } from '../common/entities'; import { AssetType, MilestoneStatus } from '../common/enums'; +import { InvalidMilestoneTransitionError } from './milestone-state-machine'; describe('MilestonesService', () => { let service: MilestonesService; @@ -75,7 +76,7 @@ describe('MilestonesService', () => { sponsorId: 'sponsor-1', }); - const milestone = await service.fund('m1', 'GFUNDER'); + const milestone = await service.fund('m1', 'GFUNDER', 'sponsor-1'); expect(milestone.status).toBe(MilestoneStatus.FUNDED); expect(milestone.escrowId).toBe('escrow-1'); @@ -90,7 +91,7 @@ describe('MilestonesService', () => { sponsorId: 'sponsor-1', }); - await service.fund('m1', 'GFUNDER'); + await service.fund('m1', 'GFUNDER', 'sponsor-1'); expect(escrowService.fund).toHaveBeenCalledWith( expect.objectContaining({ @@ -110,7 +111,7 @@ describe('MilestonesService', () => { sponsorId: null, }); - await service.fund('m2', 'GFUNDER'); + await service.fund('m2', 'GFUNDER', 'sponsor-1'); expect(escrowService.fund).toHaveBeenCalledWith( expect.objectContaining({ sponsorId: null }), @@ -123,7 +124,7 @@ describe('MilestonesService', () => { status: MilestoneStatus.FUNDED, }); - await expect(service.fund('m1', 'GFUNDER')).rejects.toThrow( + await expect(service.fund('m1', 'GFUNDER', 'sponsor-1')).rejects.toThrow( 'Milestone m1 is not OPEN (current: funded)', ); }); @@ -263,6 +264,7 @@ describe('MilestonesService', () => { '250.0000000', 'RECIPIENT_ADDR', 'user-1', + expect.anything(), ); expect(payment.id).toBe('payment-1'); expect(dataSource.transaction).toHaveBeenCalled(); @@ -341,7 +343,7 @@ describe('MilestonesService', () => { await expect( service.resolveIssue('m1', 'i1', 'RECIPIENT_ADDR'), - ).rejects.toThrow('Milestone m1 is not accepting distributions'); + ).rejects.toThrow(InvalidMilestoneTransitionError); }); it('rejects when the issue does not belong to the milestone (#114)', async () => { @@ -406,6 +408,7 @@ describe('MilestonesService', () => { '333.3333333', 'RECIPIENT_ADDR', undefined, + expect.anything(), ); }); });