@elisra-devops/docgen-skins 0.27.0 → 0.29.0
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/.github/workflows/ci.yml +2 -0
- package/bin/index.js +15 -3
- package/bin/index.js.map +1 -1
- package/bin/models/json/table/JSONTableCell.js +4 -3
- package/bin/models/json/table/JSONTableCell.js.map +1 -1
- package/bin/services/logSink.d.ts +25 -0
- package/bin/services/logSink.js +16 -0
- package/bin/services/logSink.js.map +1 -0
- package/bin/services/logger.d.ts +6 -0
- package/bin/services/logger.js +212 -16
- package/bin/services/logger.js.map +1 -1
- package/bin/services/runContext.d.ts +7 -0
- package/bin/services/runContext.js +12 -0
- package/bin/services/runContext.js.map +1 -0
- package/bin/test/integration/skins.integration.test.js +74 -0
- package/bin/test/integration/skins.integration.test.js.map +1 -1
- package/bin/test/logger.test.d.ts +1 -0
- package/bin/test/logger.test.js +272 -0
- package/bin/test/logger.test.js.map +1 -0
- package/bin/test/runContext.test.d.ts +1 -0
- package/bin/test/runContext.test.js +96 -0
- package/bin/test/runContext.test.js.map +1 -0
- package/eslint.config.mjs +40 -0
- package/package.json +8 -3
- package/src/index.ts +15 -5
- package/src/models/json/table/JSONTableCell.ts +4 -3
- package/src/services/logSink.ts +44 -0
- package/src/services/logger.ts +220 -17
- package/src/services/runContext.ts +22 -0
- package/src/test/integration/skins.integration.test.ts +82 -0
- package/src/test/logger.test.ts +268 -0
- package/src/test/runContext.test.ts +48 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import * as winston from 'winston';
|
|
2
|
+
import Transport from 'winston-transport';
|
|
3
|
+
import { redact, DiagnosticsTransport } from '../services/logger';
|
|
4
|
+
import { installLogSink, LogSink, DiagnosticEvent } from '../services/logSink';
|
|
5
|
+
import { runContextStore } from '../services/runContext';
|
|
6
|
+
|
|
7
|
+
const applyRedact = (info: Record<string, unknown>) => (redact() as any).transform({ ...info });
|
|
8
|
+
|
|
9
|
+
describe('logger redact format', () => {
|
|
10
|
+
test('scrubs a top-level token/password/secret regardless of key casing', () => {
|
|
11
|
+
const out = applyRedact({ level: 'info', message: 'x', token: 'abc', Password: 'p', SECRET: 's' });
|
|
12
|
+
expect(out.token).toBe('[REDACTED]');
|
|
13
|
+
expect(out.Password).toBe('[REDACTED]');
|
|
14
|
+
expect(out.SECRET).toBe('[REDACTED]');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('scrubs nested config.auth.password and config.headers.Authorization — the AxiosError.toJSON() leak', () => {
|
|
18
|
+
const axiosLikeError = {
|
|
19
|
+
message: 'Request failed',
|
|
20
|
+
config: {
|
|
21
|
+
auth: { username: '', password: 'super-secret-pat' },
|
|
22
|
+
headers: { Authorization: 'Bearer abc.def.ghi' },
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
const out: any = applyRedact({ level: 'error', message: 'upstream failed', err: axiosLikeError });
|
|
26
|
+
expect(out.err.config.auth.password).toBe('[REDACTED]');
|
|
27
|
+
expect(out.err.config.headers.Authorization).toBe('[REDACTED]');
|
|
28
|
+
expect(out.err.message).toBe('Request failed');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('leaves non-sensitive fields untouched', () => {
|
|
32
|
+
const out = applyRedact({ level: 'info', message: 'ok', project: 'Cube-ADCS', docType: 'SVD' });
|
|
33
|
+
expect(out.project).toBe('Cube-ADCS');
|
|
34
|
+
expect(out.docType).toBe('SVD');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('never throws on a circular object', () => {
|
|
38
|
+
const circular: any = { name: 'x' };
|
|
39
|
+
circular.self = circular;
|
|
40
|
+
expect(() => applyRedact({ level: 'info', message: 'x', circular })).not.toThrow();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('never throws and passes info through on null/undefined meta', () => {
|
|
44
|
+
expect(() => applyRedact({ level: 'info', message: 'x', meta: null })).not.toThrow();
|
|
45
|
+
expect(() => applyRedact({ level: 'info', message: 'x', meta: undefined })).not.toThrow();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// A capturing transport + the exact JSON format chain logger.ts uses (errors -> timestamp ->
|
|
50
|
+
// redact -> splat -> json), so these tests exercise the real pipeline, not just redact() alone.
|
|
51
|
+
class CaptureTransport extends Transport {
|
|
52
|
+
lines: Record<string, unknown>[] = [];
|
|
53
|
+
log(info: Record<string, unknown>, callback: () => void) {
|
|
54
|
+
this.lines.push(JSON.parse((info as any)[Symbol.for('message')] ?? JSON.stringify(info)));
|
|
55
|
+
callback();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function makeTestLogger() {
|
|
59
|
+
const capture = new CaptureTransport();
|
|
60
|
+
const logger = winston.createLogger({
|
|
61
|
+
level: 'silly',
|
|
62
|
+
format: winston.format.combine(
|
|
63
|
+
winston.format.errors({ stack: true }),
|
|
64
|
+
winston.format.timestamp(),
|
|
65
|
+
redact(),
|
|
66
|
+
winston.format.splat(),
|
|
67
|
+
winston.format.json()
|
|
68
|
+
),
|
|
69
|
+
transports: [capture],
|
|
70
|
+
});
|
|
71
|
+
return { logger, capture };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe('logger pipeline — errors({stack:true})', () => {
|
|
75
|
+
test('logger.error(err) (single-arg Error) still produces a non-empty message and a stack', () => {
|
|
76
|
+
// This is the regression errors({stack:true}) exists to prevent: without it, winston's
|
|
77
|
+
// single-arg path makes `info` *be* the Error, and message/stack are non-enumerable, so
|
|
78
|
+
// json() would emit {"level":"error","timestamp":"…"} — content-free.
|
|
79
|
+
const { logger, capture } = makeTestLogger();
|
|
80
|
+
logger.error(new Error('boom'));
|
|
81
|
+
expect(capture.lines).toHaveLength(1);
|
|
82
|
+
expect(capture.lines[0].message).toBe('boom');
|
|
83
|
+
expect(typeof capture.lines[0].stack).toBe('string');
|
|
84
|
+
expect((capture.lines[0].stack as string).length).toBeGreaterThan(0);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('logger pipeline — hostile inputs never throw', () => {
|
|
89
|
+
test.each([
|
|
90
|
+
['null', null],
|
|
91
|
+
['undefined', undefined],
|
|
92
|
+
['NaN', NaN],
|
|
93
|
+
['a circular object', (() => { const c: any = { a: 1 }; c.self = c; return c; })()],
|
|
94
|
+
['an object with a throwing getter', { get boom() { throw new Error('nope'); } }],
|
|
95
|
+
['a BigInt', BigInt(9007199254740993)],
|
|
96
|
+
['a Symbol', Symbol('x')],
|
|
97
|
+
['an Error with no message', new Error()],
|
|
98
|
+
['a 10MB string', 'x'.repeat(10 * 1024 * 1024)],
|
|
99
|
+
])('logger.error(%s) does not throw', (_label, value) => {
|
|
100
|
+
const { logger } = makeTestLogger();
|
|
101
|
+
expect(() => logger.error(value as any)).not.toThrow();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('DiagnosticsTransport (Phase 6a)', () => {
|
|
106
|
+
function makeDiagnosticsLogger() {
|
|
107
|
+
const capture = new CaptureTransport();
|
|
108
|
+
const logger = winston.createLogger({
|
|
109
|
+
level: 'silly',
|
|
110
|
+
format: winston.format.combine(
|
|
111
|
+
winston.format.errors({ stack: true }),
|
|
112
|
+
winston.format.timestamp(),
|
|
113
|
+
redact(),
|
|
114
|
+
winston.format.splat(),
|
|
115
|
+
winston.format.json()
|
|
116
|
+
),
|
|
117
|
+
transports: [capture, new DiagnosticsTransport()],
|
|
118
|
+
});
|
|
119
|
+
return { logger, capture };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
test('pushes warn/error records to whatever sink is installed', () => {
|
|
123
|
+
const events: DiagnosticEvent[] = [];
|
|
124
|
+
const sink: LogSink = { push: (e) => events.push(e) };
|
|
125
|
+
installLogSink(sink);
|
|
126
|
+
const { logger } = makeDiagnosticsLogger();
|
|
127
|
+
logger.warn('a stable warning');
|
|
128
|
+
logger.error('a stable error', Object.assign(new Error('boom'), { code: 'ECONN' }));
|
|
129
|
+
expect(events).toHaveLength(2);
|
|
130
|
+
expect(events[0].level).toBe('warn');
|
|
131
|
+
expect(events[1].err?.code).toBe('ECONN');
|
|
132
|
+
expect(events[1].err?.stack).toEqual(expect.any(String));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('does not push info/debug records', () => {
|
|
136
|
+
const events: DiagnosticEvent[] = [];
|
|
137
|
+
installLogSink({ push: (e) => events.push(e) });
|
|
138
|
+
const { logger } = makeDiagnosticsLogger();
|
|
139
|
+
logger.info('not interesting to the dashboard');
|
|
140
|
+
expect(events).toHaveLength(0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('a throwing sink cannot break the log call', () => {
|
|
144
|
+
installLogSink({
|
|
145
|
+
push: () => {
|
|
146
|
+
throw new Error('sink is down');
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
const { logger } = makeDiagnosticsLogger();
|
|
150
|
+
expect(() => logger.error('still must not throw')).not.toThrow();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test.each([
|
|
154
|
+
['null', null],
|
|
155
|
+
['undefined', undefined],
|
|
156
|
+
['NaN', NaN],
|
|
157
|
+
['a circular object', (() => { const c: any = { a: 1 }; c.self = c; return c; })()],
|
|
158
|
+
['an object with a throwing getter', { get boom() { throw new Error('nope'); } }],
|
|
159
|
+
['a BigInt', BigInt(9007199254740993)],
|
|
160
|
+
['a Symbol', Symbol('x')],
|
|
161
|
+
['an Error with no message', new Error()],
|
|
162
|
+
['a 10MB string', 'x'.repeat(10 * 1024 * 1024)],
|
|
163
|
+
])('logger.error(%s) with a sink installed does not throw', (_label, value) => {
|
|
164
|
+
installLogSink({ push: () => undefined });
|
|
165
|
+
const { logger } = makeDiagnosticsLogger();
|
|
166
|
+
expect(() => logger.error(value as any)).not.toThrow();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('textFormat safety (regression)', () => {
|
|
171
|
+
// The hostile-input matrix above exercises makeTestLogger()'s json() pipeline — it never
|
|
172
|
+
// touched the real singleton logger's text-format branch (LOG_FORMAT's default), which is
|
|
173
|
+
// where a Symbol message crashed: a template literal's implicit ToString throws on a Symbol,
|
|
174
|
+
// and this formatter sits with no try/catch around it.
|
|
175
|
+
test('logger.error(Symbol) does not throw through the real default-exported logger', () => {
|
|
176
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
177
|
+
const logger = require('../services/logger').default;
|
|
178
|
+
expect(() => logger.error(Symbol('x'))).not.toThrow();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('DiagnosticsTransport capture policy (Phase 6b)', () => {
|
|
183
|
+
function makeDiagnosticsLogger() {
|
|
184
|
+
const capture = new CaptureTransport();
|
|
185
|
+
const logger = winston.createLogger({
|
|
186
|
+
level: 'silly',
|
|
187
|
+
format: winston.format.combine(
|
|
188
|
+
winston.format.errors({ stack: true }),
|
|
189
|
+
winston.format.timestamp(),
|
|
190
|
+
redact(),
|
|
191
|
+
winston.format.splat(),
|
|
192
|
+
winston.format.json()
|
|
193
|
+
),
|
|
194
|
+
transports: [capture, new DiagnosticsTransport()],
|
|
195
|
+
});
|
|
196
|
+
return { logger, capture };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
test('normal mode (no captureMode) does not persist debug/info', () => {
|
|
200
|
+
const events: DiagnosticEvent[] = [];
|
|
201
|
+
installLogSink({ push: (e) => events.push(e) });
|
|
202
|
+
const { logger } = makeDiagnosticsLogger();
|
|
203
|
+
runContextStore.run({ runId: 'run-1' }, () => {
|
|
204
|
+
logger.debug('a debug line');
|
|
205
|
+
logger.info('an info line');
|
|
206
|
+
});
|
|
207
|
+
expect(events).toHaveLength(0);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('verbose mode persists debug and info', () => {
|
|
211
|
+
const events: DiagnosticEvent[] = [];
|
|
212
|
+
installLogSink({ push: (e) => events.push(e) });
|
|
213
|
+
const { logger } = makeDiagnosticsLogger();
|
|
214
|
+
runContextStore.run({ runId: 'run-2', captureMode: 'verbose' }, () => {
|
|
215
|
+
logger.debug('a debug line');
|
|
216
|
+
logger.info('an info line');
|
|
217
|
+
});
|
|
218
|
+
expect(events).toHaveLength(2);
|
|
219
|
+
expect(events[0].retainPending).toBeUndefined();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('warn/error persist regardless of capture mode', () => {
|
|
223
|
+
const events: DiagnosticEvent[] = [];
|
|
224
|
+
installLogSink({ push: (e) => events.push(e) });
|
|
225
|
+
const { logger } = makeDiagnosticsLogger();
|
|
226
|
+
runContextStore.run({ runId: 'run-3' }, () => {
|
|
227
|
+
logger.warn('a warning');
|
|
228
|
+
logger.error('an error');
|
|
229
|
+
});
|
|
230
|
+
expect(events).toHaveLength(2);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('retain-on-failure persists debug/info tagged retainPending: true', () => {
|
|
234
|
+
const events: DiagnosticEvent[] = [];
|
|
235
|
+
installLogSink({ push: (e) => events.push(e) });
|
|
236
|
+
const { logger } = makeDiagnosticsLogger();
|
|
237
|
+
runContextStore.run({ runId: 'run-4', captureMode: 'retain-on-failure' }, () => {
|
|
238
|
+
logger.debug('a debug line');
|
|
239
|
+
});
|
|
240
|
+
expect(events).toHaveLength(1);
|
|
241
|
+
expect(events[0].retainPending).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('retain-on-failure does not tag warn/error as retainPending', () => {
|
|
245
|
+
const events: DiagnosticEvent[] = [];
|
|
246
|
+
installLogSink({ push: (e) => events.push(e) });
|
|
247
|
+
const { logger } = makeDiagnosticsLogger();
|
|
248
|
+
runContextStore.run({ runId: 'run-5', captureMode: 'retain-on-failure' }, () => {
|
|
249
|
+
logger.error('an error');
|
|
250
|
+
});
|
|
251
|
+
expect(events).toHaveLength(1);
|
|
252
|
+
expect(events[0].retainPending).toBeUndefined();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('a concurrent normal-mode run is unaffected by a sibling verbose run', () => {
|
|
256
|
+
const events: DiagnosticEvent[] = [];
|
|
257
|
+
installLogSink({ push: (e) => events.push(e) });
|
|
258
|
+
const { logger } = makeDiagnosticsLogger();
|
|
259
|
+
runContextStore.run({ runId: 'verbose-run', captureMode: 'verbose' }, () => {
|
|
260
|
+
logger.debug('verbose debug');
|
|
261
|
+
});
|
|
262
|
+
runContextStore.run({ runId: 'normal-run' }, () => {
|
|
263
|
+
logger.debug('normal debug');
|
|
264
|
+
});
|
|
265
|
+
expect(events).toHaveLength(1);
|
|
266
|
+
expect(events[0].message).toBe('verbose debug');
|
|
267
|
+
});
|
|
268
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as winston from 'winston';
|
|
2
|
+
import Transport from 'winston-transport';
|
|
3
|
+
import { withRunContext } from '../services/logger';
|
|
4
|
+
import { runContextStore } from '../services/runContext';
|
|
5
|
+
|
|
6
|
+
class CaptureTransport extends Transport {
|
|
7
|
+
lines: Record<string, unknown>[] = [];
|
|
8
|
+
log(info: Record<string, unknown>, callback: () => void) {
|
|
9
|
+
this.lines.push(JSON.parse((info as any)[Symbol.for('message')] ?? JSON.stringify(info)));
|
|
10
|
+
callback();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function makeTestLogger() {
|
|
14
|
+
const capture = new CaptureTransport();
|
|
15
|
+
const logger = winston.createLogger({
|
|
16
|
+
level: 'silly',
|
|
17
|
+
format: winston.format.combine(withRunContext(), winston.format.json()),
|
|
18
|
+
transports: [capture],
|
|
19
|
+
});
|
|
20
|
+
return { logger, capture };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('withRunContext', () => {
|
|
24
|
+
test('is a no-op when the store was never populated', () => {
|
|
25
|
+
const { logger, capture } = makeTestLogger();
|
|
26
|
+
logger.info('outside any run');
|
|
27
|
+
expect(capture.lines[0].runId).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('stamps runId onto every log emitted inside store.run(...)', () => {
|
|
31
|
+
const { logger, capture } = makeTestLogger();
|
|
32
|
+
runContextStore.run({ runId: 'run-123' }, () => {
|
|
33
|
+
logger.info('inside the run');
|
|
34
|
+
});
|
|
35
|
+
logger.info('outside again');
|
|
36
|
+
expect(capture.lines[0].runId).toBe('run-123');
|
|
37
|
+
expect(capture.lines[1].runId).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('async work started inside the run keeps the runId across the await boundary', async () => {
|
|
41
|
+
const { logger, capture } = makeTestLogger();
|
|
42
|
+
await runContextStore.run({ runId: 'run-async' }, async () => {
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
44
|
+
logger.info('after await');
|
|
45
|
+
});
|
|
46
|
+
expect(capture.lines[0].runId).toBe('run-async');
|
|
47
|
+
});
|
|
48
|
+
});
|