@jintianxiayu/cache-decorator 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -11
- package/README.md +364 -270
- package/dist/core/cache-error.d.ts +75 -0
- package/dist/core/cache-error.d.ts.map +1 -0
- package/dist/core/cache-error.js +168 -0
- package/dist/core/cache-error.js.map +1 -0
- package/dist/core/cache-logger.d.ts +3 -2
- package/dist/core/cache-logger.d.ts.map +1 -1
- package/dist/core/cache-logger.js +2 -0
- package/dist/core/cache-logger.js.map +1 -1
- package/dist/decorators/cache-evict.d.ts.map +1 -1
- package/dist/decorators/cache-evict.js +15 -10
- package/dist/decorators/cache-evict.js.map +1 -1
- package/dist/decorators/cache.d.ts +6 -0
- package/dist/decorators/cache.d.ts.map +1 -1
- package/dist/decorators/cache.js +113 -36
- package/dist/decorators/cache.js.map +1 -1
- package/jest.config.js +11 -11
- package/package.json +1 -1
- package/src/adapters/ioredis-cache-client.ts +89 -89
- package/src/adapters/node-redis-cache-client.ts +95 -95
- package/src/adapters/redis-key-prefix.ts +38 -38
- package/src/core/cache-error.ts +233 -0
- package/src/core/cache-logger.ts +116 -105
- package/src/core/key-builder.ts +33 -33
- package/src/core/native-cache.ts +55 -55
- package/src/core/pending-cache.ts +29 -29
- package/src/core/redis-cache-client.ts +160 -160
- package/src/core/redis-cache.ts +104 -104
- package/src/decorators/cache-evict.ts +137 -129
- package/src/decorators/cache.ts +323 -203
- package/src/index.ts +11 -11
- package/test/cache-evict-logging.test.ts +398 -362
- package/test/cache-logger.integration.test.ts +159 -129
- package/test/cache-logger.test.ts +156 -153
- package/test/cache-logging.test.ts +929 -544
- package/test/cache.test.ts +1017 -255
- package/test/fixtures/cache-logger-child.mjs +108 -108
- package/test/fixtures/cache-provider-failure-child.mjs +70 -0
- package/test/helpers/legacy-redis-cache.ts +41 -22
- package/test/helpers/package-consumer.ts +231 -231
- package/test/helpers/redis-fixture.ts +142 -142
- package/test/ioredis-cache-client.test.ts +143 -143
- package/test/legacy-redis-cache.test.ts +51 -0
- package/test/native-cache.test.ts +77 -77
- package/test/node-redis-cache-client.test.ts +149 -149
- package/test/pending-cache.test.ts +69 -69
- package/test/redis-cache-client-lifecycle.test.ts +112 -112
- package/test/redis-cache-client-types.test.ts +184 -184
- package/test/redis-cache-client.integration.test.ts +355 -327
- package/test/redis-cache-decorator.test.ts +601 -201
- package/test/redis-cache-provider.test.ts +269 -269
- package/test/type-contract/contract.ts +119 -64
- package/test/type-contract/tsconfig.json +12 -12
- package/tsconfig.json +8 -8
|
@@ -1,129 +1,159 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { join, resolve } from 'node:path';
|
|
4
|
-
import { runCommand } from './helpers/package-consumer';
|
|
5
|
-
|
|
6
|
-
jest.setTimeout(30_000);
|
|
7
|
-
|
|
8
|
-
interface CacheLogMetadata {
|
|
9
|
-
readonly event: string;
|
|
10
|
-
readonly cacheName: string;
|
|
11
|
-
readonly methodName: string;
|
|
12
|
-
readonly providerName: string;
|
|
13
|
-
readonly entryType?: string;
|
|
14
|
-
readonly operation?: string;
|
|
15
|
-
readonly error?: {
|
|
16
|
-
readonly name: string;
|
|
17
|
-
readonly message: string;
|
|
18
|
-
readonly password: string;
|
|
19
|
-
readonly email: string;
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface JsonLogEvent {
|
|
24
|
-
readonly level: string;
|
|
25
|
-
readonly name: string;
|
|
26
|
-
readonly message: string;
|
|
27
|
-
readonly traceId: string;
|
|
28
|
-
readonly meta: CacheLogMetadata;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface ChildResult {
|
|
32
|
-
readonly code: number | null;
|
|
33
|
-
readonly stdout: string;
|
|
34
|
-
readonly stderr: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const packageRoot = resolve(__dirname, '..');
|
|
38
|
-
const loggerRoot = resolve(packageRoot, '../logger');
|
|
39
|
-
const fixture = join(__dirname, 'fixtures', 'cache-logger-child.mjs');
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
*
|
|
44
|
-
* @
|
|
45
|
-
* @
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
let
|
|
56
|
-
|
|
57
|
-
child.
|
|
58
|
-
child.
|
|
59
|
-
child.
|
|
60
|
-
child.
|
|
61
|
-
child.once('
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
expect(output
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { runCommand } from './helpers/package-consumer';
|
|
5
|
+
|
|
6
|
+
jest.setTimeout(30_000);
|
|
7
|
+
|
|
8
|
+
interface CacheLogMetadata {
|
|
9
|
+
readonly event: string;
|
|
10
|
+
readonly cacheName: string;
|
|
11
|
+
readonly methodName: string;
|
|
12
|
+
readonly providerName: string;
|
|
13
|
+
readonly entryType?: string;
|
|
14
|
+
readonly operation?: string;
|
|
15
|
+
readonly error?: {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly message: string;
|
|
18
|
+
readonly password: string;
|
|
19
|
+
readonly email: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface JsonLogEvent {
|
|
24
|
+
readonly level: string;
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly message: string;
|
|
27
|
+
readonly traceId: string;
|
|
28
|
+
readonly meta: CacheLogMetadata;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ChildResult {
|
|
32
|
+
readonly code: number | null;
|
|
33
|
+
readonly stdout: string;
|
|
34
|
+
readonly stderr: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const packageRoot = resolve(__dirname, '..');
|
|
38
|
+
const loggerRoot = resolve(packageRoot, '../logger');
|
|
39
|
+
const fixture = join(__dirname, 'fixtures', 'cache-logger-child.mjs');
|
|
40
|
+
const providerFailureFixture = join(__dirname, 'fixtures', 'cache-provider-failure-child.mjs');
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 在隔离进程运行真实 Logger fixture,确保关闭流程完整刷新结构化输出。
|
|
44
|
+
* @param profileLevel cache 命名 profile 的筛选级别。
|
|
45
|
+
* @returns 子进程退出码和完整标准流。
|
|
46
|
+
* @throws 子进程无法启动时拒绝。
|
|
47
|
+
*/
|
|
48
|
+
function runFixture(profileLevel: 'debug' | 'error'): Promise<ChildResult> {
|
|
49
|
+
return new Promise((resolveResult, reject) => {
|
|
50
|
+
const child = spawn(process.execPath, [fixture, profileLevel], {
|
|
51
|
+
cwd: packageRoot,
|
|
52
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
53
|
+
windowsHide: true,
|
|
54
|
+
});
|
|
55
|
+
let stdout = '';
|
|
56
|
+
let stderr = '';
|
|
57
|
+
child.stdout.setEncoding('utf8');
|
|
58
|
+
child.stderr.setEncoding('utf8');
|
|
59
|
+
child.stdout.on('data', (chunk: string) => (stdout += chunk));
|
|
60
|
+
child.stderr.on('data', (chunk: string) => (stderr += chunk));
|
|
61
|
+
child.once('error', reject);
|
|
62
|
+
child.once('exit', (code) => resolveResult({ code, stdout, stderr }));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 在严格未处理 rejection 模式运行 Provider 异步失败 fixture。
|
|
68
|
+
* @returns 子进程退出码和完整标准流。
|
|
69
|
+
* @throws 子进程无法启动时拒绝。
|
|
70
|
+
*/
|
|
71
|
+
function runProviderFailureFixture(): Promise<ChildResult> {
|
|
72
|
+
return new Promise((resolveResult, reject) => {
|
|
73
|
+
const child = spawn(process.execPath, ['--unhandled-rejections=strict', providerFailureFixture], {
|
|
74
|
+
cwd: packageRoot,
|
|
75
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
76
|
+
windowsHide: true,
|
|
77
|
+
});
|
|
78
|
+
let stdout = '';
|
|
79
|
+
let stderr = '';
|
|
80
|
+
child.stdout.setEncoding('utf8');
|
|
81
|
+
child.stderr.setEncoding('utf8');
|
|
82
|
+
child.stdout.on('data', (chunk: string) => (stdout += chunk));
|
|
83
|
+
child.stderr.on('data', (chunk: string) => (stderr += chunk));
|
|
84
|
+
child.once('error', reject);
|
|
85
|
+
child.once('exit', (code) => resolveResult({ code, stdout, stderr }));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseEvents(output: string): JsonLogEvent[] {
|
|
90
|
+
return output
|
|
91
|
+
.split(/\r?\n/)
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.map((line) => JSON.parse(line) as JsonLogEvent);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
beforeAll(() => {
|
|
97
|
+
const compiler = require.resolve('typescript/bin/tsc');
|
|
98
|
+
runCommand({ executable: process.execPath, args: [compiler, '-p', loggerRoot], cwd: loggerRoot });
|
|
99
|
+
runCommand({ executable: process.execPath, args: [compiler, '-p', packageRoot], cwd: packageRoot });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('cache-operation-logging/M03 cache-operation-logging/M04 使用真实命名 Logger 脱敏并关联 traceId', async () => {
|
|
103
|
+
const result = await runFixture('debug');
|
|
104
|
+
expect(result.code).toBe(0);
|
|
105
|
+
expect(result.stderr).toBe('');
|
|
106
|
+
expect(result.stdout).not.toContain('provider-password-secret');
|
|
107
|
+
expect(result.stdout).not.toContain('provider@example.com');
|
|
108
|
+
expect(result.stdout).not.toContain('business-value-secret');
|
|
109
|
+
|
|
110
|
+
const output = parseEvents(result.stdout);
|
|
111
|
+
expect(output.map(({ level, meta }) => `${level}:${meta.event}`)).toEqual([
|
|
112
|
+
'debug:cache.miss',
|
|
113
|
+
'debug:cache.write_dispatched',
|
|
114
|
+
'error:cache.operation_failed',
|
|
115
|
+
]);
|
|
116
|
+
expect(output.every(({ name }) => name === '@jintianxiayu/cache-decorator')).toBe(true);
|
|
117
|
+
expect(output.every(({ traceId }) => traceId === 'cache-fixture-trace')).toBe(true);
|
|
118
|
+
expect(output.every(({ meta }) => !('traceId' in meta))).toBe(true);
|
|
119
|
+
expect(output[2]?.meta).toMatchObject({
|
|
120
|
+
event: 'cache.operation_failed',
|
|
121
|
+
cacheName: 'fixture-failure-users',
|
|
122
|
+
methodName: 'getUser',
|
|
123
|
+
providerName: 'failing',
|
|
124
|
+
operation: 'read',
|
|
125
|
+
error: {
|
|
126
|
+
name: 'Error',
|
|
127
|
+
message: 'redis unavailable',
|
|
128
|
+
password: '********',
|
|
129
|
+
email: 'pr***@example.com',
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('cache-operation-logging/Logger 配置筛选输出且复用应用提供的唯一 Logger runtime', async () => {
|
|
135
|
+
const result = await runFixture('error');
|
|
136
|
+
expect(result.code).toBe(0);
|
|
137
|
+
expect(result.stderr).toBe('');
|
|
138
|
+
const output = parseEvents(result.stdout);
|
|
139
|
+
expect(output).toHaveLength(1);
|
|
140
|
+
expect(output[0]).toMatchObject({
|
|
141
|
+
level: 'error',
|
|
142
|
+
name: '@jintianxiayu/cache-decorator',
|
|
143
|
+
traceId: 'cache-fixture-trace',
|
|
144
|
+
meta: { event: 'cache.operation_failed', operation: 'read' },
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('异步缓存写入和单 key 删除拒绝不会触发未处理 rejection', async () => {
|
|
149
|
+
const result = await runProviderFailureFixture();
|
|
150
|
+
expect(result.code).toBe(0);
|
|
151
|
+
expect(result.stderr).toBe('');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('cache decorator 源码不读取或写入 LoggerContext', () => {
|
|
155
|
+
const sources = ['src/core/cache-logger.ts', 'src/decorators/cache.ts', 'src/decorators/cache-evict.ts']
|
|
156
|
+
.map((file) => readFileSync(join(packageRoot, file), 'utf8'))
|
|
157
|
+
.join('\n');
|
|
158
|
+
expect(sources).not.toContain('LoggerContext');
|
|
159
|
+
});
|
|
@@ -1,153 +1,156 @@
|
|
|
1
|
-
import type { LoggerInterface } from '@jintianxiayu/logger';
|
|
2
|
-
|
|
3
|
-
type CacheLoggerModule = typeof import('../src/core/cache-logger');
|
|
4
|
-
|
|
5
|
-
interface CacheLoggerHarness {
|
|
6
|
-
readonly cacheLogger: CacheLoggerModule;
|
|
7
|
-
readonly getLogger: jest.Mock<LoggerInterface, [string]>;
|
|
8
|
-
readonly logger: jest.Mocked<LoggerInterface>;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const logContext = {
|
|
12
|
-
cacheName: 'users',
|
|
13
|
-
methodName: 'findUser',
|
|
14
|
-
providerName: 'default',
|
|
15
|
-
} as const;
|
|
16
|
-
|
|
17
|
-
function createMockLogger(): jest.Mocked<LoggerInterface> {
|
|
18
|
-
return {
|
|
19
|
-
debug: jest.fn(),
|
|
20
|
-
info: jest.fn(),
|
|
21
|
-
warn: jest.fn(),
|
|
22
|
-
error: jest.fn(),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* 为每个用例重新加载 cache Logger 模块,隔离模块级 Logger 缓存并允许注入获取失败。
|
|
28
|
-
* @param getLoggerImplementation 可选的 LoggerFactory.getLogger 替代实现。
|
|
29
|
-
* @returns 独立模块、Logger 获取 mock 和四个 level 方法 mock。
|
|
30
|
-
* @throws 隔离模块未能同步加载时抛出。
|
|
31
|
-
*/
|
|
32
|
-
function loadCacheLogger(getLoggerImplementation?: (name: string) => LoggerInterface): CacheLoggerHarness {
|
|
33
|
-
jest.resetModules();
|
|
34
|
-
const logger = createMockLogger();
|
|
35
|
-
const getLogger = jest.fn<LoggerInterface, [string]>(getLoggerImplementation ?? (() => logger));
|
|
36
|
-
jest.doMock('@jintianxiayu/logger', () => ({ LoggerFactory: { getLogger } }));
|
|
37
|
-
let cacheLogger: CacheLoggerModule | undefined;
|
|
38
|
-
jest.isolateModules(() => {
|
|
39
|
-
cacheLogger = jest.requireActual<CacheLoggerModule>('../src/core/cache-logger');
|
|
40
|
-
});
|
|
41
|
-
if (!cacheLogger) {
|
|
42
|
-
throw new Error('Cache logger module was not loaded');
|
|
43
|
-
}
|
|
44
|
-
return { cacheLogger, getLogger, logger };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
afterEach(() => {
|
|
48
|
-
jest.resetModules();
|
|
49
|
-
jest.dontMock('@jintianxiayu/logger');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('cache-operation-logging/L01 导入和 decorator 求值不初始化 Logger', () => {
|
|
53
|
-
const { getLogger } = loadCacheLogger();
|
|
54
|
-
const { Cache } = jest.requireActual<typeof import('../src/decorators/cache')>('../src/decorators/cache');
|
|
55
|
-
const { CacheEvict } = jest.requireActual<typeof import('../src/decorators/cache-evict')>(
|
|
56
|
-
'../src/decorators/cache-evict'
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
Cache('users');
|
|
60
|
-
CacheEvict('users');
|
|
61
|
-
|
|
62
|
-
expect(getLogger).not.toHaveBeenCalled();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('cache-operation-logging/L02 首次运行期事件获取并复用应用命名 Logger', () => {
|
|
66
|
-
const { cacheLogger, getLogger, logger } = loadCacheLogger();
|
|
67
|
-
|
|
68
|
-
cacheLogger.logCacheEvent('cache.miss', logContext);
|
|
69
|
-
cacheLogger.logCacheEvent('cache.hit', { ...logContext, entryType: 'value' });
|
|
70
|
-
|
|
71
|
-
expect(getLogger).toHaveBeenCalledTimes(1);
|
|
72
|
-
expect(getLogger).toHaveBeenCalledWith('@jintianxiayu/cache-decorator');
|
|
73
|
-
expect(logger.debug).toHaveBeenCalledTimes(2);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('cache-operation-logging/正常缓存决策使用 debug', () => {
|
|
77
|
-
const { cacheLogger, logger } = loadCacheLogger();
|
|
78
|
-
const events = [
|
|
79
|
-
'cache.pending_hit',
|
|
80
|
-
'cache.hit',
|
|
81
|
-
'cache.miss',
|
|
82
|
-
'cache.write_dispatched',
|
|
83
|
-
'cache.
|
|
84
|
-
'cache.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
expect(logger.debug
|
|
93
|
-
expect(logger.
|
|
94
|
-
expect(logger.
|
|
95
|
-
expect(logger.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
cacheLogger.logCacheEvent('cache.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
cacheLogger
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
1
|
+
import type { LoggerInterface } from '@jintianxiayu/logger';
|
|
2
|
+
|
|
3
|
+
type CacheLoggerModule = typeof import('../src/core/cache-logger');
|
|
4
|
+
|
|
5
|
+
interface CacheLoggerHarness {
|
|
6
|
+
readonly cacheLogger: CacheLoggerModule;
|
|
7
|
+
readonly getLogger: jest.Mock<LoggerInterface, [string]>;
|
|
8
|
+
readonly logger: jest.Mocked<LoggerInterface>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const logContext = {
|
|
12
|
+
cacheName: 'users',
|
|
13
|
+
methodName: 'findUser',
|
|
14
|
+
providerName: 'default',
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
function createMockLogger(): jest.Mocked<LoggerInterface> {
|
|
18
|
+
return {
|
|
19
|
+
debug: jest.fn(),
|
|
20
|
+
info: jest.fn(),
|
|
21
|
+
warn: jest.fn(),
|
|
22
|
+
error: jest.fn(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 为每个用例重新加载 cache Logger 模块,隔离模块级 Logger 缓存并允许注入获取失败。
|
|
28
|
+
* @param getLoggerImplementation 可选的 LoggerFactory.getLogger 替代实现。
|
|
29
|
+
* @returns 独立模块、Logger 获取 mock 和四个 level 方法 mock。
|
|
30
|
+
* @throws 隔离模块未能同步加载时抛出。
|
|
31
|
+
*/
|
|
32
|
+
function loadCacheLogger(getLoggerImplementation?: (name: string) => LoggerInterface): CacheLoggerHarness {
|
|
33
|
+
jest.resetModules();
|
|
34
|
+
const logger = createMockLogger();
|
|
35
|
+
const getLogger = jest.fn<LoggerInterface, [string]>(getLoggerImplementation ?? (() => logger));
|
|
36
|
+
jest.doMock('@jintianxiayu/logger', () => ({ LoggerFactory: { getLogger } }));
|
|
37
|
+
let cacheLogger: CacheLoggerModule | undefined;
|
|
38
|
+
jest.isolateModules(() => {
|
|
39
|
+
cacheLogger = jest.requireActual<CacheLoggerModule>('../src/core/cache-logger');
|
|
40
|
+
});
|
|
41
|
+
if (!cacheLogger) {
|
|
42
|
+
throw new Error('Cache logger module was not loaded');
|
|
43
|
+
}
|
|
44
|
+
return { cacheLogger, getLogger, logger };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
jest.resetModules();
|
|
49
|
+
jest.dontMock('@jintianxiayu/logger');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('cache-operation-logging/L01 导入和 decorator 求值不初始化 Logger', () => {
|
|
53
|
+
const { getLogger } = loadCacheLogger();
|
|
54
|
+
const { Cache } = jest.requireActual<typeof import('../src/decorators/cache')>('../src/decorators/cache');
|
|
55
|
+
const { CacheEvict } = jest.requireActual<typeof import('../src/decorators/cache-evict')>(
|
|
56
|
+
'../src/decorators/cache-evict'
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
Cache('users');
|
|
60
|
+
CacheEvict('users');
|
|
61
|
+
|
|
62
|
+
expect(getLogger).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('cache-operation-logging/L02 首次运行期事件获取并复用应用命名 Logger', () => {
|
|
66
|
+
const { cacheLogger, getLogger, logger } = loadCacheLogger();
|
|
67
|
+
|
|
68
|
+
cacheLogger.logCacheEvent('cache.miss', logContext);
|
|
69
|
+
cacheLogger.logCacheEvent('cache.hit', { ...logContext, entryType: 'value' });
|
|
70
|
+
|
|
71
|
+
expect(getLogger).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(getLogger).toHaveBeenCalledWith('@jintianxiayu/cache-decorator');
|
|
73
|
+
expect(logger.debug).toHaveBeenCalledTimes(2);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('cache-operation-logging/正常缓存决策使用 debug', () => {
|
|
77
|
+
const { cacheLogger, logger } = loadCacheLogger();
|
|
78
|
+
const events = [
|
|
79
|
+
'cache.pending_hit',
|
|
80
|
+
'cache.hit',
|
|
81
|
+
'cache.miss',
|
|
82
|
+
'cache.write_dispatched',
|
|
83
|
+
'cache.error_cache_skipped',
|
|
84
|
+
'cache.evict_dispatched',
|
|
85
|
+
'cache.evict_completed',
|
|
86
|
+
] as const;
|
|
87
|
+
|
|
88
|
+
for (const event of events) {
|
|
89
|
+
cacheLogger.logCacheEvent(event, logContext);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
expect(logger.debug).toHaveBeenCalledTimes(events.length);
|
|
93
|
+
expect(logger.debug.mock.calls.map((call) => (call[1] as { event: string }).event)).toEqual(events);
|
|
94
|
+
expect(logger.info).not.toHaveBeenCalled();
|
|
95
|
+
expect(logger.warn).not.toHaveBeenCalled();
|
|
96
|
+
expect(logger.error).not.toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('cache-operation-logging/可恢复回退与淘汰跳过使用 warn', () => {
|
|
100
|
+
const { cacheLogger, logger } = loadCacheLogger();
|
|
101
|
+
|
|
102
|
+
cacheLogger.logCacheEvent('cache.key_fallback', { ...logContext, reason: 'resolver_error' });
|
|
103
|
+
cacheLogger.logCacheEvent('cache.error_cache_failed', { ...logContext, phase: 'encode' });
|
|
104
|
+
cacheLogger.logCacheEvent('cache.evict_skipped', { ...logContext, reason: 'business_error' });
|
|
105
|
+
|
|
106
|
+
expect(logger.warn).toHaveBeenCalledTimes(3);
|
|
107
|
+
expect(logger.warn.mock.calls.map((call) => (call[1] as { event: string }).event)).toEqual([
|
|
108
|
+
'cache.key_fallback',
|
|
109
|
+
'cache.error_cache_failed',
|
|
110
|
+
'cache.evict_skipped',
|
|
111
|
+
]);
|
|
112
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
113
|
+
expect(logger.info).not.toHaveBeenCalled();
|
|
114
|
+
expect(logger.error).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('cache-operation-logging/缓存基础设施失败使用 error', () => {
|
|
118
|
+
const { cacheLogger, logger } = loadCacheLogger();
|
|
119
|
+
const providerError = new Error('redis unavailable');
|
|
120
|
+
|
|
121
|
+
cacheLogger.logCacheEvent('cache.operation_failed', {
|
|
122
|
+
...logContext,
|
|
123
|
+
operation: 'read',
|
|
124
|
+
error: providerError,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(logger.error).toHaveBeenCalledWith(
|
|
128
|
+
'Cache operation failed',
|
|
129
|
+
expect.objectContaining({ event: 'cache.operation_failed', operation: 'read', error: providerError })
|
|
130
|
+
);
|
|
131
|
+
expect(logger.debug).not.toHaveBeenCalled();
|
|
132
|
+
expect(logger.info).not.toHaveBeenCalled();
|
|
133
|
+
expect(logger.warn).not.toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('Logger 获取或写入同步失败时不向缓存调用方抛出', () => {
|
|
137
|
+
const getLoggerError = new Error('invalid logger config');
|
|
138
|
+
const failedGet = loadCacheLogger(() => {
|
|
139
|
+
throw getLoggerError;
|
|
140
|
+
});
|
|
141
|
+
expect(() => failedGet.cacheLogger.logCacheEvent('cache.miss', logContext)).not.toThrow();
|
|
142
|
+
|
|
143
|
+
const failedWrite = loadCacheLogger();
|
|
144
|
+
failedWrite.logger.debug.mockImplementationOnce(() => {
|
|
145
|
+
throw new Error('transport failed');
|
|
146
|
+
});
|
|
147
|
+
expect(() => failedWrite.cacheLogger.logCacheEvent('cache.miss', logContext)).not.toThrow();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('Provider 日志标签将缺省和空名称稳定映射为 default', () => {
|
|
151
|
+
const { cacheLogger } = loadCacheLogger();
|
|
152
|
+
|
|
153
|
+
expect(cacheLogger.cacheProviderLabel(undefined)).toBe('default');
|
|
154
|
+
expect(cacheLogger.cacheProviderLabel('')).toBe('default');
|
|
155
|
+
expect(cacheLogger.cacheProviderLabel('redis-primary')).toBe('redis-primary');
|
|
156
|
+
});
|