mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 22:46:32 +00:00
81 lines
3.7 KiB
TypeScript
81 lines
3.7 KiB
TypeScript
/**
|
|
* Storage-health-notifier request-context ratchet (Plan 3f Task 4, R3).
|
|
*
|
|
* Task 0's emitter trace (`task-0-report.md`) found one ambiguous path: the
|
|
* "Sync now" backfill sweep (`StorageJobsService.startBackfill`) detaches
|
|
* from its originating admin request (`void driver.backfill(...)`,
|
|
* fire-and-forget), so a `replica_failure` event it reports can reach
|
|
* `StorageHealthNotifierService`'s listener long after the triggering
|
|
* request's own EntityManager fork was meant to be done with. No emitter was
|
|
* found with NO request context at all, but per Task 0's own recommendation
|
|
* ("cheap insurance worth taking regardless"), the listener now forks its
|
|
* OWN fresh request context on every invocation via `withRequestContext` —
|
|
* the same shape `CronRegistrarService.register`'s wrapped tick uses.
|
|
*
|
|
* This test fires the event from a BARE context — plain top-level test code,
|
|
* no HTTP request, no `withRequestContext` wrapper around the emit itself —
|
|
* driving the REAL listener registration through a real `buildApp()` boot
|
|
* (not a bare method call on a hand-built double), and asserts the listener
|
|
* never logs `cannotUseGlobalContext` / "global EntityManager" — the same
|
|
* regression class `boot-sweeps-request-context.test.ts` guards for the
|
|
* seven cron boot sweeps.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
import type { INestApplication } from '@nestjs/common';
|
|
|
|
vi.mock('../../src/db/database', async () => {
|
|
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
|
return buildDbMock(createSnapshotTestDb());
|
|
});
|
|
vi.mock('../../src/config', () => ({
|
|
JWT_SECRET: 'test-jwt-secret-for-trek-testing-only',
|
|
ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2',
|
|
updateJwtSecret: () => {},
|
|
SESSION_DURATION: '24h',
|
|
SESSION_DURATION_MS: 86400000,
|
|
SESSION_DURATION_SECONDS: 86400,
|
|
DEFAULT_LANGUAGE: 'en',
|
|
}));
|
|
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn(), getOnlineUserIds: vi.fn(() => []) }));
|
|
|
|
import { db as testDb } from '../../src/db/database';
|
|
import { buildApp } from '../../src/bootstrap';
|
|
import { StorageEventsService } from '../../src/nest/storage/storage-events.service';
|
|
|
|
describe('StorageHealthNotifierService listener runs inside its own request context, outside any active one', () => {
|
|
let app: INestApplication;
|
|
|
|
beforeAll(async () => {
|
|
app = await buildApp();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
testDb.close();
|
|
});
|
|
|
|
it('STORAGE-HEALTH-CTX-001: emitting replica_failure from a bare (non-request) context never logs cannotUseGlobalContext', async () => {
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
try {
|
|
// The listener is already registered for real, from buildApp()'s own
|
|
// Nest lifecycle (onApplicationBootstrap runs during app.init()) — no
|
|
// hand-called registration here, per rule 12/13 ("drive the real
|
|
// listener registration, not a bare method call").
|
|
const events = app.get(StorageEventsService);
|
|
events.emitReplicaFailure({ backend: 'ctx-ratchet', key: 'k', op: 'put', error: 'timeout', at: Date.now() });
|
|
|
|
// Let the fire-and-forget async dispatch (withRequestContext(...).then(...))
|
|
// settle before asserting.
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
const suspicious = errSpy.mock.calls
|
|
.map((args) => args.map(String).join(' '))
|
|
.filter((line) => /cannotUseGlobalContext|global EntityManager/i.test(line));
|
|
expect(suspicious).toEqual([]);
|
|
} finally {
|
|
errSpy.mockRestore();
|
|
}
|
|
});
|
|
});
|