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:
Justin Maier
2026-08-15 13:22:31 -06:00
parent 92f1728652
commit 17f994221e
3 changed files with 66 additions and 3 deletions
@@ -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
+38 -3
View File
@@ -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'),
};
+6
View File
@@ -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