refactor!: simplify runtime coordination #7
Backend verify / commits (pull_request) Successful in 29s
Backend verify / security (pull_request) Successful in 8s
Backend verify / verify (pull_request) Failing after 9m36s

Keep financial ownership, atomic publication and refresh CAS.
Failed refresh leaves cookies untouched; logout still clears them.

Remove full-history candle prefetch and special metadata retries.
Use native Pino request context instead of parallel middleware.

BREAKING CHANGE: remove investment stateVersion, queued sync state,
and tbank-sync status progress/resources. Consumers must observe
resources through sync=observe with the updated frontend contract.
This commit is contained in:
2026-10-03 11:38:08 +03:00
parent 0f27da9fd1
commit d698ed9425
48 changed files with 185 additions and 925 deletions
+27 -17
View File
@@ -1,12 +1,13 @@
import { INestApplication } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { Test, TestingModule } from '@nestjs/testing';
import { PinoLogger } from 'nestjs-pino';
import { PARAMS_PROVIDER_TOKEN } from 'nestjs-pino';
import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from '../src/app.module';
import { StructuredLogger } from '../src/common/logging/structured-logger.service';
import { loggingOptions } from '../src/common/logging/logging.module';
import { configureApp } from '../src/main';
import { AuthService } from '../src/modules/auth/application/auth.service';
import { PrismaService } from '../src/prisma/prisma.service';
@@ -16,10 +17,22 @@ type Problem = { code: string };
describe('Authentication (e2e)', () => {
let app: INestApplication<App>;
const logs: Record<string, unknown>[] = [];
beforeEach(async () => {
logs.length = 0;
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).overrideProvider(PARAMS_PROVIDER_TOKEN).useFactory({
inject: [ConfigService],
factory: (config: Parameters<typeof loggingOptions>[0]) => ({
pinoHttp: {
...loggingOptions(config),
level: 'info',
stream: { write: (line: string) => logs.push(JSON.parse(line) as Record<string, unknown>) },
},
assignResponse: true,
}),
}).compile();
app = moduleFixture.createNestApplication();
@@ -187,10 +200,12 @@ describe('Authentication (e2e)', () => {
]);
const winner = responses.find((response) => response.status === 200);
if (!winner) throw new Error('Expected one successful rotation');
expect(responses.find((response) => response.status === 401)?.headers['set-cookie']).toBeUndefined();
await request(app.getHttpServer())
.post('/api/v1/auth/refresh')
.set('Cookie', cookie)
.expect(401);
.expect(401)
.expect((response) => expect(response.headers['set-cookie']).toBeUndefined());
await request(app.getHttpServer())
.post('/api/v1/auth/refresh')
.set('Cookie', winner.headers['set-cookie'][0])
@@ -304,8 +319,6 @@ describe('Authentication (e2e)', () => {
});
it('exposes a server request ID and logs one completion event', async () => {
const logger = app.get(StructuredLogger);
const event = jest.spyOn(logger, 'event');
const response = await request(app.getHttpServer())
.get('/api/v1/auth/me')
.set('Origin', 'http://localhost:5173')
@@ -315,16 +328,12 @@ describe('Authentication (e2e)', () => {
expect(response.headers['access-control-expose-headers']).toContain(
'X-Request-ID',
);
expect(event).toHaveBeenCalledTimes(1);
expect(event).toHaveBeenCalledWith(
'warn',
'http.request.completed',
expect.objectContaining({ status: 401, code: 'UNAUTHORIZED' }),
);
expect(logs.filter((entry) => entry.requestId === response.headers['x-request-id'])).toEqual([
expect.objectContaining({ level: 40, message: 'http.request.completed', status: 401, code: 'UNAUTHORIZED' }),
]);
});
it('logs an unhandled error with safe public context and correlation', async () => {
const error = jest.spyOn(PinoLogger.prototype, 'error');
const clientRequestId = 'a2dc6fbb-5a48-4c3c-9e3e-4fbf4d638a43';
jest
.spyOn(app.get(AuthService), 'login')
@@ -342,17 +351,18 @@ describe('Authentication (e2e)', () => {
expect(body).toEqual({ status: 500, code: 'INTERNAL_SERVER_ERROR' }),
);
expect(error).toHaveBeenCalledTimes(1);
expect(error).toHaveBeenCalledWith(
const completed = logs.filter((entry) => entry.requestId === response.headers['x-request-id']);
expect(completed).toEqual([
expect.objectContaining({
requestId: response.headers['x-request-id'],
clientRequestId,
status: 500,
code: 'INTERNAL_SERVER_ERROR',
level: 50,
message: 'http.request.completed',
}),
'http.request.completed',
);
expect(JSON.stringify(error.mock.calls)).not.toContain(
]);
expect(JSON.stringify(logs)).not.toContain(
'SYNTHETIC_AUDIT_SECRET',
);
});
+10 -43
View File
@@ -41,7 +41,6 @@ describe('atomic shared candle reconciliation (SQLite)', () => {
lastSuccessfulAt: null,
resourceKey: 'detail/uid',
dataRevision: '1',
stateVersion: 'detail-v1',
sync: { id: null, state: 'idle', errorCode: null },
},
},
@@ -273,7 +272,7 @@ describe('atomic shared candle reconciliation (SQLite)', () => {
).toBe(abandoned?.work.attemptId);
await reopened.read('owner', input);
await settled();
expect(upstream.getCandles.mock.calls.length).toBeGreaterThan(1);
expect(upstream.getCandles).toHaveBeenCalledTimes(1);
expect(
(await repository.work('owner', 'revision', 'candles:uid:month'))
?.attemptId,
@@ -483,52 +482,20 @@ describe('atomic shared candle reconciliation (SQLite)', () => {
await prisma.tbankCandleCoverage.count({ where: { isComplete: false } }),
).toBe(1);
});
it('prefetches only current supported portfolio instruments and lets screen work run between history windows', async () => {
await prisma.tbankInstrument.create({
data: {
provider: 'tbank',
uid: 'opened',
instrumentType: 'share',
metadata: {},
fetchedAt: new Date(),
},
});
it('loads only the requested period and reuses its saved coverage', async () => {
const service = createService();
let release: (value: unknown[]) => void = () => {};
const blocked = new Promise<unknown[]>((resolve) => {
release = resolve;
});
upstream.getCandles.mockReturnValueOnce(blocked);
await service.prefetch('owner', 'revision', [
{ instrumentUid: 'uid', type: 'STOCK' },
{ instrumentUid: 'uid', type: 'STOCK' },
{ instrumentUid: 'not-supported', type: 'FUND' },
{ instrumentUid: null, type: 'BOND' },
]);
for (let i = 0; i < 100 && !upstream.getCandles.mock.calls.length; i++) {
await delay(5);
}
expect((upstream.getCandles.mock.calls as unknown[][])[0]?.[1]).toBe('uid');
await service.read('owner', {
uid: 'opened',
const input = {
uid: 'uid',
instrumentType: 'share',
period: 'month',
});
release([]);
} as const;
await service.read('owner', input);
await settled();
expect((upstream.getCandles.mock.calls as unknown[][])[1]?.[1]).toBe(
'opened',
);
expect(
new Set(upstream.getCandles.mock.calls.map((call: unknown[]) => call[1])),
).toEqual(new Set(['opened', 'uid']));
expect(upstream.getCandles).toHaveBeenCalledTimes(1);
expect(await repository.work('owner', 'revision', 'candles:uid:all')).toBeNull();
const count = upstream.getCandles.mock.calls.length;
await service.prefetch('owner', 'revision', [
{ instrumentUid: 'uid', type: 'STOCK' },
]);
await service.read('owner', input);
await settled();
expect(upstream.getCandles).toHaveBeenCalledTimes(count);
expect(
(await repository.work('owner', 'revision', 'candles:opened:all'))?.state,
).toBe('complete');
});
});
+4 -5
View File
@@ -263,7 +263,7 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
expect(upstream.getOperationsByCursor).not.toHaveBeenCalled();
},
);
it('keeps status versions stable between observational reads of a moving period', async () => {
it('keeps data revisions stable while observational reads expose current coverage', async () => {
const observe = () =>
request(app.getHttpServer())
.get('/api/v1/brokerage-accounts/all/operations?sync=observe&limit=100')
@@ -276,7 +276,7 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
expect(second.meta.coverage?.requested).not.toEqual(
first.meta.coverage?.requested,
);
expect(second.meta.stateVersion).toBe(first.meta.stateVersion);
expect(second.meta.dataRevision).toBe(first.meta.dataRevision);
await prisma.tbankOperationCoverage.updateMany({
data: {
to: new Date(Date.now() - 60_000),
@@ -285,10 +285,10 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
});
const changed = (await observe().expect(200))
.body as BrokerageOperationsResourceDto;
expect(changed.meta.stateVersion).not.toBe(second.meta.stateVersion);
expect(changed.meta.coverage?.asOf).not.toBe(second.meta.coverage?.asOf);
expect(upstream.getOperationsByCursor).not.toHaveBeenCalled();
});
it('reads shared metadata from SQLite and changes stateVersion without invalidating a row cursor', async () => {
it('reads shared metadata from SQLite without invalidating a row cursor', async () => {
const before = (await get('limit=1&sync=observe').expect(200))
.body as BrokerageOperationsResourceDto;
assert(before.data?.nextCursor);
@@ -323,7 +323,6 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
const after = (await get('limit=1&sync=observe').expect(200))
.body as BrokerageOperationsResourceDto;
expect(after.data?.items[0].logoName).toBe('saved.png');
expect(after.meta.stateVersion).not.toBe(before.meta.stateVersion);
expect(after.meta.dataRevision).toBe(before.meta.dataRevision);
await get(
`limit=1&sync=observe&cursor=${encodeURIComponent(before.data.nextCursor)}`,
-6
View File
@@ -134,12 +134,6 @@ describe('T-Bank persisted read budget', () => {
'bond',
instruments.find((item) => item.uid === uid),
),
getLastPrice: (_token: string, uid: string) =>
counted('lastPrice', {
instrumentUid: uid,
price: { units: '100' },
time: { seconds: Date.UTC(2026, 7, 31) / 1000 },
}),
getLastPrices: (_token: string, uids: string[]) =>
counted(
'lastPrices',
+6 -10
View File
@@ -103,10 +103,11 @@ describe('screen sync HTTP and SQLite', () => {
release([]);
const done = await settled(ids[0]);
expect(done.state).toBe('succeeded');
expect(done.resources[0]).toMatchObject({
resourceKey: 'accounts',
availability: 'complete',
});
const accounts = await request(app.getHttpServer())
.get('/api/v1/brokerage-accounts?sync=observe')
.set('Authorization', auth())
.expect(200);
expect(accounts.body).toMatchObject({ data: [], meta: { availability: 'complete' } });
expect(JSON.stringify(done)).not.toMatch(
/revision|attemptId|encryptedToken|synthetic/,
);
@@ -297,20 +298,17 @@ describe('screen sync HTTP and SQLite', () => {
.set('Authorization', auth())
.expect(200);
const first = before.body as {
meta: { dataRevision: string; stateVersion: string };
meta: { dataRevision: string };
};
const last = after.body as {
data: { items: unknown[] };
meta: {
dataRevision: string;
stateVersion: string;
sync: { id: string };
};
};
expect(last.data.items).toEqual([]);
expect(last.meta.dataRevision).toBe(first.meta.dataRevision);
expect(last.meta.stateVersion).not.toBe(first.meta.stateVersion);
expect(done.resources[0].stateVersion).toBe(last.meta.stateVersion);
await status(last.meta.sync.id).expect(200);
expect(upstream.getOperationsByCursor).toHaveBeenCalledTimes(1);
});
@@ -389,7 +387,6 @@ describe('screen sync HTTP and SQLite', () => {
try {
expect((await status(second).expect(200)).body).toMatchObject({
state: 'running',
progress: { completed: 1, total: 2, discovering: false },
});
await status(second, 'other').expect(404);
} finally {
@@ -401,7 +398,6 @@ describe('screen sync HTTP and SQLite', () => {
expect(await settled(second)).toMatchObject({
state: outcome,
errorCode: outcome === 'failed' ? 'TBANK_UNAVAILABLE' : null,
progress: { completed: 2, total: 2, discovering: false },
});
await prisma.tbankConnection.delete({ where: { userId: 'owner' } });
await status(second).expect(404);