mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
feat(test-mocks): make withSysReadDeadline a seam on the canonical redis mock
`setup.ts` spread `~/server/redis/sys-read-deadline` into the canonical
`~/server/redis/client` factory, so every migrated file ran the REAL wall-clock
deadline wrapper and a test had no way to inject a sysRedis read timeout. Nine
files across two slices could not migrate: converting them would have left their
timeout legs green and asserting nothing.
The seam's registered default is the real implementation, not a pass-through.
REDIS_SYS_READ_TIMEOUT_MS is in TEST_ENV_DEFAULTS at 2000, so the wrapper is
armed today; a pass-through default would disarm a live guard in every file in
the worker to give nine files a lever.
Resolved by a lazy import. A static import of sys-read-deadline evaluates its
`import { env }` before setup.ts's hoisted `vi.mock('~/env/server')` factory has
initialised, and the file collects zero tests with "Cannot access
'__vi_import_4__' before initialization".
Verified: the guard fails without the seam (1 failed / 11 passed) and passes with
it (12/12); 6 fake-timer files that touch sysRedis are 178/178 both halves; 34
caller-derived files are 396/396 both halves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UtTG4QQR29eWf7kjM6HiLU
This commit is contained in:
@@ -40,6 +40,28 @@ describe('shared-module mocks', () => {
|
||||
expect(redisClient.withSysReadDeadline).toBeTypeOf('function');
|
||||
});
|
||||
|
||||
it('exposes withSysReadDeadline as a seam whose default is the REAL deadline', async () => {
|
||||
const redisClient = await import('~/server/redis/client');
|
||||
expect(redisClient.withSysReadDeadline).toBe(redisMock.withSysReadDeadline);
|
||||
|
||||
// The default has to still time out, or promoting it to a seam would silently disarm a
|
||||
// live guard in every file in the worker. A hanging promise is the only input that tells
|
||||
// the real implementation apart from a pass-through — every resolved promise looks the
|
||||
// same through both.
|
||||
await expect(redisClient.withSysReadDeadline(new Promise(() => undefined), 5)).rejects.toThrow(
|
||||
'sysRedis read timed out after 5ms'
|
||||
);
|
||||
|
||||
// …and the positive control for that assertion: the same call resolves when the promise
|
||||
// beats the deadline. Without this, a seam that rejected everything would also pass.
|
||||
await expect(redisClient.withSysReadDeadline(Promise.resolve('v'), 5000)).resolves.toBe('v');
|
||||
|
||||
// The lever the nine blocked files need: replace it per file, and the reset restores the
|
||||
// real implementation for the next one.
|
||||
redisMock.withSysReadDeadline.mockRejectedValueOnce(new Error('injected'));
|
||||
await expect(redisClient.withSysReadDeadline(Promise.resolve('v'))).rejects.toThrow('injected');
|
||||
});
|
||||
|
||||
it('never evaluates the real db/redis shims, so no client is ever constructed', () => {
|
||||
// 🔴 The registration spreads the PACKAGE, not `importOriginal` of the app shim. The
|
||||
// shims construct Prisma/Redis clients at module scope, so spreading them forced real
|
||||
|
||||
@@ -51,13 +51,48 @@ const DEFAULTS: Record<string, () => Promise<unknown>> = {
|
||||
ping: () => Promise.resolve('PONG'),
|
||||
};
|
||||
|
||||
/**
|
||||
* `withSysReadDeadline` is a SEAM, not a command: a test injects a sysRedis read timeout by
|
||||
* replacing it, which is the only lever it has — the deadline is a wall-clock race the mocked
|
||||
* client can never lose on its own.
|
||||
*
|
||||
* 🔴 Its default is the REAL implementation, deliberately. `setup.ts` spread the real module
|
||||
* into the canonical factory, so every migrated file has been running the real wrapper; a
|
||||
* pass-through default would silently disarm a live guard in every file in the worker to give
|
||||
* nine files a lever. Wrapping the real function is the only version of this that adds a seam
|
||||
* and changes nothing else.
|
||||
*
|
||||
* 🔴 Resolved by a LAZY import, not a top-level one. This module is loaded from `setup.ts`, which
|
||||
* also registers the `~/env/server` mock — and `vi.mock` is hoisted above it. A static
|
||||
* `import … from '~/server/redis/sys-read-deadline'` therefore evaluates that module (and its
|
||||
* `import { env }`) before the env mock's factory has initialised, and the whole file collects
|
||||
* ZERO tests with `Cannot access '__vi_import_4__' before initialization`.
|
||||
*/
|
||||
let realModule: typeof import('~/server/redis/sys-read-deadline') | undefined;
|
||||
|
||||
const SEAMS: Record<string, (...args: any[]) => any> = {
|
||||
withSysReadDeadline: async (...args: any[]) => {
|
||||
realModule ??= await import('~/server/redis/sys-read-deadline');
|
||||
return realModule.withSysReadDeadline(...(args as [Promise<unknown>, number?]));
|
||||
},
|
||||
};
|
||||
|
||||
registerDefaults((path) => {
|
||||
const root = path.slice(0, path.indexOf('.'));
|
||||
if (!ROOTS.includes(root)) return undefined;
|
||||
if (SEAMS[path]) return SEAMS[path];
|
||||
const dot = path.indexOf('.');
|
||||
// A root path with no dot is not a command; `slice(0, -1)` would silently truncate it into
|
||||
// one that never matches, which is how a resolver returns undefined for the wrong reason.
|
||||
if (dot === -1) return undefined;
|
||||
if (!ROOTS.includes(path.slice(0, dot))) return undefined;
|
||||
return DEFAULTS[path.slice(path.lastIndexOf('.') + 1)];
|
||||
});
|
||||
|
||||
export const redisMock: { redis: HybridNode; sysRedis: HybridNode } = {
|
||||
export const redisMock: {
|
||||
redis: HybridNode;
|
||||
sysRedis: HybridNode;
|
||||
withSysReadDeadline: HybridNode;
|
||||
} = {
|
||||
redis: hybridNode('redis'),
|
||||
sysRedis: hybridNode('sysRedis'),
|
||||
withSysReadDeadline: hybridNode('withSysReadDeadline'),
|
||||
};
|
||||
|
||||
@@ -36,11 +36,17 @@ vi.mock('~/server/db/client', async () => ({
|
||||
dbWrite: dbMock.dbWrite,
|
||||
}));
|
||||
|
||||
// The `sys-read-deadline` spread stays for anything else that module exports; the seam is
|
||||
// overridden after it, so a test can inject a sysRedis read timeout the way it injects a
|
||||
// `sysRedis.get`. Its default IS that real implementation, so this adds a lever and changes
|
||||
// no behaviour — see redis.mock.ts. Without it a converted test keeps the real wall-clock
|
||||
// race, which the mocked client can never lose, and its timeout leg passes asserting nothing.
|
||||
vi.mock('~/server/redis/client', async () => ({
|
||||
...(await import('@civitai/redis/client')),
|
||||
...(await import('~/server/redis/sys-read-deadline')),
|
||||
redis: redisMock.redis,
|
||||
sysRedis: redisMock.sysRedis,
|
||||
withSysReadDeadline: redisMock.withSysReadDeadline,
|
||||
}));
|
||||
|
||||
// Mock @civitai/client to avoid ESM resolution issues
|
||||
|
||||
Reference in New Issue
Block a user