From d5e6c3eb7b2113d032e71ad7b31881635b911173 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Thu, 15 Oct 2026 07:46:10 +0300 Subject: [PATCH] test: replace late portfolio sleep with started and done barriers (WealthLedger/backend#63) --- test/tbank-local-reads.e2e-spec.ts | 23 ++++++-- test/tbank-portfolio-publish.e2e-spec.ts | 73 ++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 test/tbank-portfolio-publish.e2e-spec.ts diff --git a/test/tbank-local-reads.e2e-spec.ts b/test/tbank-local-reads.e2e-spec.ts index a90401a..ffadb48 100644 --- a/test/tbank-local-reads.e2e-spec.ts +++ b/test/tbank-local-reads.e2e-spec.ts @@ -3,6 +3,7 @@ import type { Server } from 'node:http'; import { INestApplication } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { Test } from '@nestjs/testing'; +import assert from 'node:assert/strict'; import { setTimeout as delay } from 'node:timers/promises'; import request from 'supertest'; @@ -501,16 +502,30 @@ describe('local accounts and portfolio HTTP (SQLite)', () => { await put().expect(200); await seedMembership(); const blocked = deferred(); - upstream.getPortfolio.mockReturnValueOnce(blocked.promise); - await get('brokerage-accounts/account/portfolio').expect(200); + const started = deferred(); + upstream.getPortfolio.mockImplementationOnce(() => { + started.resolve(); + return blocked.promise; + }); + // Starts the real portfolio producer, the same path the hourly walk uses. + const late = await portfolioService.request( + 'owner', + (await prisma.tbankConnection.findUniqueOrThrow({ where: { userId: 'owner' } })).revision, + 'account', + true, + ); + assert(late); + // The late upstream call must have begun before the connection is replaced. + await started.promise; await put().expect(200); await seedMembership(); blocked.resolve({ positions: [] }); - await delay(20); + // Barrier: the fenced producer's completion, not a timer. + await late.done; + expect(await prisma.tbankPortfolio.count()).toBe(0); const replaced = await get( 'brokerage-accounts/account/portfolio?sync=observe', ).expect(200); expect((replaced.body as PortfolioResourceDto).data).toBeNull(); - expect(await prisma.tbankPortfolio.count()).toBe(0); }); }); diff --git a/test/tbank-portfolio-publish.e2e-spec.ts b/test/tbank-portfolio-publish.e2e-spec.ts new file mode 100644 index 0000000..4c877e7 --- /dev/null +++ b/test/tbank-portfolio-publish.e2e-spec.ts @@ -0,0 +1,73 @@ +import { ConfigService } from '@nestjs/config'; +import assert from 'node:assert/strict'; + +import { PortfolioRepository } from '../src/modules/investments/portfolio/infrastructure/portfolio.repository'; +import { PrismaService } from '../src/prisma/prisma.service'; + +// Real SQLite transaction: a running attempt of the current revision publishes its snapshot. +describe('portfolio publication (SQLite)', () => { + const prisma = new PrismaService( + new ConfigService({ DATABASE_URL: process.env.DATABASE_URL }), + ); + const repository = new PortfolioRepository(prisma); + const snapshot = { + lastSuccessfulAt: '2026-01-01T00:00:00.000Z', + isStale: false, + totalsByCurrency: [], + positions: [], + cashBalances: [], + structure: {}, + } as never; + const accountId = 'account'; + + beforeAll(async () => { + await prisma.$connect(); + await prisma.user.create({ + data: { id: 'owner', email: 'owner@example.invalid', passwordHash: 'synthetic' }, + }); + }); + afterAll(async () => prisma.$disconnect()); + beforeEach(async () => { + await prisma.tbankConnection.deleteMany(); + await prisma.tbankConnection.create({ + data: { + userId: 'owner', + revision: 'current', + encryptedToken: 'synthetic', + connectedAt: new Date(), + accounts: { + create: { + accountId, + type: 'brokerage', + name: 'Synthetic', + upstreamOrder: 0, + }, + }, + }, + }); + }); + + it('publishes the same snapshot for the running attempt of the current revision', async () => { + await prisma.tbankSyncWork.create({ + data: { + id: 'live', + userId: 'owner', + revision: 'current', + scopeKey: `portfolio:${accountId}`, + intent: {}, + state: 'running', + attemptId: 'live-attempt', + processRunId: 'run', + priority: 1, + progress: {}, + }, + }); + const published = await repository.publish( + { id: 'live', attemptId: 'live-attempt', userId: 'owner', revision: 'current' }, + accountId, + snapshot, + ); + assert.equal(published, true); + expect(await prisma.tbankPortfolio.count()).toBe(1); + }); +});