runspec-node 0.11.1 → 0.12.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/dist/loader.js +6 -0
- package/dist/loader.js.map +1 -1
- package/dist/logging_setup.d.ts +20 -2
- package/dist/logging_setup.d.ts.map +1 -1
- package/dist/logging_setup.js +209 -11
- package/dist/logging_setup.js.map +1 -1
- package/dist/models.d.ts +1 -0
- package/dist/models.d.ts.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +26 -0
- package/dist/parser.js.map +1 -1
- package/dist/serve.js +18 -2
- package/dist/serve.js.map +1 -1
- package/package.json +4 -4
- package/src/loader.ts +6 -0
- package/src/logging_setup.ts +239 -11
- package/src/models.ts +1 -0
- package/src/parser.ts +27 -0
- package/src/serve.ts +19 -2
- package/tests/test_loader.test.ts +18 -4
- package/tests/test_logging_setup.test.ts +10 -3
- package/tests/test_run_summary.test.ts +204 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import {
|
|
5
|
+
configureLogging,
|
|
6
|
+
getLogger,
|
|
7
|
+
emitRunSummary,
|
|
8
|
+
_resetForTest,
|
|
9
|
+
RUN_SUMMARY_LOGGER,
|
|
10
|
+
} from '../src/logging_setup';
|
|
11
|
+
|
|
12
|
+
function tmpDir(): string {
|
|
13
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'runspec-summary-test-'));
|
|
14
|
+
fs.writeFileSync(path.join(d, 'package.json'), '{"name":"test","version":"0.0.0"}');
|
|
15
|
+
return d;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function makeCfg(dir: string, overrides: Record<string, unknown> = {}): Parameters<typeof configureLogging>[0] {
|
|
19
|
+
const { debug, noSummary, autonomy, agent, commandPath, ...logOverrides } = overrides as {
|
|
20
|
+
debug?: boolean; noSummary?: boolean; autonomy?: string; agent?: boolean; commandPath?: string[];
|
|
21
|
+
} & Record<string, unknown>;
|
|
22
|
+
return {
|
|
23
|
+
logCfg: { rotate: 'midnight', keep: 7, summary: true, ...logOverrides },
|
|
24
|
+
runnableName: 'myscript',
|
|
25
|
+
configPath: path.join(dir, 'runspec.toml'),
|
|
26
|
+
debug,
|
|
27
|
+
noSummary,
|
|
28
|
+
autonomy,
|
|
29
|
+
agent,
|
|
30
|
+
commandPath,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function captureStderr() {
|
|
35
|
+
const lines: string[] = [];
|
|
36
|
+
const spy = jest.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
|
|
37
|
+
lines.push(String(chunk));
|
|
38
|
+
return true;
|
|
39
|
+
});
|
|
40
|
+
return { lines, restore() { spy.mockRestore(); } };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
_resetForTest();
|
|
45
|
+
delete process.env['RUNSPEC_NO_SUMMARY'];
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterAll(() => {
|
|
49
|
+
// Final cleanup so jest's own process-exit doesn't trigger a summary
|
|
50
|
+
// emit against a stale state pointing at a tmp dir.
|
|
51
|
+
_resetForTest();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ── counter ──────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
test('counter increments per level', () => {
|
|
57
|
+
const dir = tmpDir();
|
|
58
|
+
configureLogging(makeCfg(dir));
|
|
59
|
+
const log = getLogger('test.counter');
|
|
60
|
+
log.info('one');
|
|
61
|
+
log.info('two');
|
|
62
|
+
log.warning('careful');
|
|
63
|
+
log.error('broke');
|
|
64
|
+
const cap = captureStderr();
|
|
65
|
+
emitRunSummary();
|
|
66
|
+
cap.restore();
|
|
67
|
+
const content = fs.readFileSync(path.join(dir, 'logs', 'myscript.log'), 'utf-8');
|
|
68
|
+
const summary = content.trim().split('\n').map(l => JSON.parse(l)).find(o => o.logger === RUN_SUMMARY_LOGGER);
|
|
69
|
+
expect(summary.extra.events).toEqual({ DEBUG: 0, INFO: 2, WARNING: 1, ERROR: 1, CRITICAL: 0 });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('counter ignores the summary logger itself', () => {
|
|
73
|
+
const dir = tmpDir();
|
|
74
|
+
configureLogging(makeCfg(dir));
|
|
75
|
+
// Calling the summary logger directly must not inflate counts.
|
|
76
|
+
getLogger(RUN_SUMMARY_LOGGER).info('should not count');
|
|
77
|
+
const cap = captureStderr();
|
|
78
|
+
emitRunSummary();
|
|
79
|
+
cap.restore();
|
|
80
|
+
const content = fs.readFileSync(path.join(dir, 'logs', 'myscript.log'), 'utf-8');
|
|
81
|
+
const lines = content.trim().split('\n').map(l => JSON.parse(l));
|
|
82
|
+
const summary = lines.find(o => o.logger === RUN_SUMMARY_LOGGER && o.extra?.event === 'run_summary');
|
|
83
|
+
expect(summary.extra.events).toEqual({ DEBUG: 0, INFO: 0, WARNING: 0, ERROR: 0, CRITICAL: 0 });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ── emit ─────────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
test('summary writes one record to the audit file', () => {
|
|
89
|
+
const dir = tmpDir();
|
|
90
|
+
configureLogging(makeCfg(dir, { autonomy: 'confirm', agent: false }));
|
|
91
|
+
getLogger('test.emit').info('did work');
|
|
92
|
+
getLogger('test.emit').warning('a warning');
|
|
93
|
+
const cap = captureStderr();
|
|
94
|
+
emitRunSummary();
|
|
95
|
+
cap.restore();
|
|
96
|
+
const content = fs.readFileSync(path.join(dir, 'logs', 'myscript.log'), 'utf-8');
|
|
97
|
+
const summaries = content.trim().split('\n').map(l => JSON.parse(l)).filter(o => o.logger === RUN_SUMMARY_LOGGER);
|
|
98
|
+
expect(summaries).toHaveLength(1);
|
|
99
|
+
const s = summaries[0];
|
|
100
|
+
expect(s.message).toBe('run completed');
|
|
101
|
+
expect(s.extra.event).toBe('run_summary');
|
|
102
|
+
expect(s.extra.runnable).toBe('myscript');
|
|
103
|
+
expect(s.extra.events.INFO).toBe(1);
|
|
104
|
+
expect(s.extra.events.WARNING).toBe(1);
|
|
105
|
+
expect(s.extra.exit_code).toBe(0);
|
|
106
|
+
expect(s.extra.exception).toBeNull();
|
|
107
|
+
expect(typeof s.extra.duration_ms).toBe('number');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('summary writes a one-line summary to stderr', () => {
|
|
111
|
+
const dir = tmpDir();
|
|
112
|
+
configureLogging(makeCfg(dir));
|
|
113
|
+
getLogger('test.stderr').info('ran');
|
|
114
|
+
const cap = captureStderr();
|
|
115
|
+
emitRunSummary();
|
|
116
|
+
cap.restore();
|
|
117
|
+
const joined = cap.lines.join('');
|
|
118
|
+
expect(joined).toContain('runspec: myscript completed');
|
|
119
|
+
expect(joined).toContain('events');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('summary is not echoed to stdout or stderr via console handlers', () => {
|
|
123
|
+
const dir = tmpDir();
|
|
124
|
+
const stdoutLines: string[] = [];
|
|
125
|
+
const stderrLines: string[] = [];
|
|
126
|
+
const o = jest.spyOn(process.stdout, 'write').mockImplementation((chunk) => { stdoutLines.push(String(chunk)); return true; });
|
|
127
|
+
const e = jest.spyOn(process.stderr, 'write').mockImplementation((chunk) => { stderrLines.push(String(chunk)); return true; });
|
|
128
|
+
configureLogging(makeCfg(dir));
|
|
129
|
+
getLogger('test.console').info('hi');
|
|
130
|
+
emitRunSummary();
|
|
131
|
+
o.mockRestore();
|
|
132
|
+
e.mockRestore();
|
|
133
|
+
// The JSON form of the summary record must not appear on either stream.
|
|
134
|
+
expect(stdoutLines.join('')).not.toContain('"logger":"runspec.runsummary"');
|
|
135
|
+
expect(stderrLines.join('').match(/"logger":"runspec.runsummary"/)).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('summary emit is idempotent', () => {
|
|
139
|
+
const dir = tmpDir();
|
|
140
|
+
configureLogging(makeCfg(dir));
|
|
141
|
+
const cap = captureStderr();
|
|
142
|
+
emitRunSummary();
|
|
143
|
+
emitRunSummary();
|
|
144
|
+
cap.restore();
|
|
145
|
+
const content = fs.readFileSync(path.join(dir, 'logs', 'myscript.log'), 'utf-8');
|
|
146
|
+
const summaries = content.trim().split('\n').map(l => JSON.parse(l)).filter(o => o.logger === RUN_SUMMARY_LOGGER);
|
|
147
|
+
expect(summaries).toHaveLength(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ── disable switches ─────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
test('noSummary option disables summary', () => {
|
|
153
|
+
const dir = tmpDir();
|
|
154
|
+
configureLogging(makeCfg(dir, { noSummary: true }));
|
|
155
|
+
const cap = captureStderr();
|
|
156
|
+
emitRunSummary(); // no-op because state is null
|
|
157
|
+
cap.restore();
|
|
158
|
+
expect(cap.lines.join('')).not.toContain('runspec: ');
|
|
159
|
+
expect(fs.existsSync(path.join(dir, 'logs', 'myscript.log'))).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('summary=false in config disables summary', () => {
|
|
163
|
+
const dir = tmpDir();
|
|
164
|
+
configureLogging(makeCfg(dir, { summary: false }));
|
|
165
|
+
const cap = captureStderr();
|
|
166
|
+
emitRunSummary();
|
|
167
|
+
cap.restore();
|
|
168
|
+
expect(cap.lines.join('')).not.toContain('runspec: ');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('RUNSPEC_NO_SUMMARY=1 disables summary', () => {
|
|
172
|
+
process.env['RUNSPEC_NO_SUMMARY'] = '1';
|
|
173
|
+
const dir = tmpDir();
|
|
174
|
+
configureLogging(makeCfg(dir));
|
|
175
|
+
const cap = captureStderr();
|
|
176
|
+
emitRunSummary();
|
|
177
|
+
cap.restore();
|
|
178
|
+
expect(cap.lines.join('')).not.toContain('runspec: ');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ── stderr line shape ────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
test('success line uses singular "warning" for 1 and plural "errors" for 0', () => {
|
|
184
|
+
const dir = tmpDir();
|
|
185
|
+
configureLogging(makeCfg(dir));
|
|
186
|
+
getLogger('t').warning('one');
|
|
187
|
+
const cap = captureStderr();
|
|
188
|
+
emitRunSummary();
|
|
189
|
+
cap.restore();
|
|
190
|
+
const joined = cap.lines.join('');
|
|
191
|
+
expect(joined).toContain('1 warning,');
|
|
192
|
+
expect(joined).toContain('0 errors)');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('success line uses plural for 2 warnings', () => {
|
|
196
|
+
const dir = tmpDir();
|
|
197
|
+
configureLogging(makeCfg(dir));
|
|
198
|
+
getLogger('t').warning('a');
|
|
199
|
+
getLogger('t').warning('b');
|
|
200
|
+
const cap = captureStderr();
|
|
201
|
+
emitRunSummary();
|
|
202
|
+
cap.restore();
|
|
203
|
+
expect(cap.lines.join('')).toContain('2 warnings,');
|
|
204
|
+
});
|