test: prove reads finish before source synchronization
Conventional commits / commits (pull_request) Successful in 31s
Backend security / security (pull_request) Successful in 9s
Backend verify / verify (pull_request) Successful in 10m47s
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Failing after 12m14s

Block synthetic upstream with an explicit barrier instead of comparing
total HTTP batch time with a two-second timer. Keep the 500 ms server
p95 gate and RPC assertions unchanged.
This commit was merged in pull request #34.
This commit is contained in:
2026-09-26 21:45:50 +03:00
parent d6e6c4ace5
commit e4232138d0
+21 -4
View File
@@ -70,10 +70,12 @@ describe('T-Bank persisted read budget', () => {
}).sort((a, b) => Number(b.date.seconds) - Number(a.date.seconds)); }).sort((a, b) => Number(b.date.seconds) - Number(a.date.seconds));
const rpc: Record<string, number> = {}; const rpc: Record<string, number> = {};
let upstreamMode: 'ready' | 'delayed' | 'failed' = 'ready'; let upstreamMode: 'ready' | 'delayed' | 'failed' = 'ready';
let sourceGate: Promise<void> | undefined;
let releaseSource = () => {};
const serverTimes: number[] = []; const serverTimes: number[] = [];
async function counted<T>(name: string, value: T): Promise<T> { async function counted<T>(name: string, value: T): Promise<T> {
rpc[name] = (rpc[name] ?? 0) + 1; rpc[name] = (rpc[name] ?? 0) + 1;
if (upstreamMode === 'delayed') await delay(2000); if (upstreamMode === 'delayed') await sourceGate;
if (upstreamMode === 'failed') if (upstreamMode === 'failed')
return Promise.reject(new Error('Synthetic outage')); return Promise.reject(new Error('Synthetic outage'));
return Promise.resolve(value); return Promise.resolve(value);
@@ -516,7 +518,8 @@ describe('T-Bank persisted read budget', () => {
JSON.stringify(output, null, 2), JSON.stringify(output, null, 2),
); );
console.log(JSON.stringify(output)); console.log(JSON.stringify(output));
// Delayed dependency cannot hold a data response or multiply one stale pass. // Hold the source until HTTP reads finish: proves independence without a
// wall-clock race against the runner's throughput. The p95 gate stays below.
await fresh(); await fresh();
await app.get(PrismaService).tbankPortfolio.updateMany({ await app.get(PrismaService).tbankPortfolio.updateMany({
where: { accountId: 'baseline-0' }, where: { accountId: 'baseline-0' },
@@ -524,31 +527,45 @@ describe('T-Bank persisted read budget', () => {
}); });
const beforeStale = { ...rpc }; const beforeStale = { ...rpc };
upstreamMode = 'delayed'; upstreamMode = 'delayed';
sourceGate = new Promise<void>((resolve) => {
releaseSource = resolve;
});
const startStale = performance.now(); const startStale = performance.now();
try {
await Promise.all( await Promise.all(
Array.from({ length: 20 }, () => Array.from({ length: 20 }, () =>
request(app.getHttpServer()) request(app.getHttpServer())
.get('/api/v1/brokerage-accounts/baseline-0/portfolio') .get('/api/v1/brokerage-accounts/baseline-0/portfolio')
.set('Authorization', `Bearer ${token}`) .set('Authorization', `Bearer ${token}`)
.timeout(10_000)
.expect(200), .expect(200),
), ),
); );
} finally {
releaseSource();
}
const staleResponsesMs = performance.now() - startStale; const staleResponsesMs = performance.now() - startStale;
expect(staleResponsesMs).toBeLessThan(2000);
await settled(); await settled();
expect(rpc.portfolio - beforeStale.portfolio).toBe(1); expect(rpc.portfolio - beforeStale.portfolio).toBe(1);
expect(rpc.positions - beforeStale.positions).toBe(1); expect(rpc.positions - beforeStale.positions).toBe(1);
await app await app
.get(PrismaService) .get(PrismaService)
.tbankPortfolio.deleteMany({ where: { accountId: 'baseline-0' } }); .tbankPortfolio.deleteMany({ where: { accountId: 'baseline-0' } });
sourceGate = new Promise<void>((resolve) => {
releaseSource = resolve;
});
const coldStart = performance.now(); const coldStart = performance.now();
try {
const missing = await request(app.getHttpServer()) const missing = await request(app.getHttpServer())
.get('/api/v1/brokerage-accounts/baseline-0/portfolio') .get('/api/v1/brokerage-accounts/baseline-0/portfolio')
.set('Authorization', `Bearer ${token}`) .set('Authorization', `Bearer ${token}`)
.timeout(10_000)
.expect(200); .expect(200);
expect((missing.body as { data: unknown }).data).toBeNull(); expect((missing.body as { data: unknown }).data).toBeNull();
} finally {
releaseSource();
}
const coldImportMs = performance.now() - coldStart; const coldImportMs = performance.now() - coldStart;
expect(coldImportMs).toBeLessThan(2000);
await settled(); await settled();
Object.assign(output, { Object.assign(output, {
staleResponsesMs, staleResponsesMs,