deepdebug-local-agent 1.0.21 → 1.0.23
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/Dockerfile +55 -20
- package/README.md +81 -0
- package/cloudbuild-agent-staging.yaml +40 -0
- package/docs/legacy-connectors.md +268 -0
- package/package.json +4 -1
- package/scripts/sync-fixtures.mjs +246 -0
- package/src/adapters/as400/as400-parser.js +12 -0
- package/src/adapters/as400/index.js +134 -0
- package/src/adapters/cobol/cobol-parser.js +37 -0
- package/src/adapters/cobol/index.js +151 -0
- package/src/adapters/ibmmq/ibmmq-parser.js +23 -0
- package/src/adapters/ibmmq/index.js +137 -0
- package/src/adapters/index.js +79 -0
- package/src/adapters/oracle-forms/index.js +145 -0
- package/src/adapters/oracle-forms/oracle-forms-parser.js +11 -0
- package/src/adapters/sap/index.js +146 -0
- package/src/adapters/sap/sap-parser.js +41 -0
- package/src/detectors/framework-detector.js +33 -0
- package/src/detectors/language-detector.js +26 -1
- package/src/exec-utils.js +182 -0
- package/src/server.js +329 -16
- package/test/adapters/as400/as400.test.js +121 -0
- package/test/adapters/cobol/cobol.test.js +217 -0
- package/test/adapters/ibmmq/ibmmq.test.js +122 -0
- package/test/adapters/oracle-forms/oracle-forms.test.js +180 -0
- package/test/adapters/registry.test.js +101 -0
- package/test/adapters/sap/sap.test.js +142 -0
- package/src/local-agent-source.tar.gz +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
|
+
import { As400LogAdapter } from '../../../src/adapters/as400/index.js';
|
|
6
|
+
import { AS400_MSGID_REGEX } from '../../../src/adapters/as400/as400-parser.js';
|
|
7
|
+
|
|
8
|
+
// Stub ssh2.Client — supports .connect(), .exec(callback), .on('close'/'error'/'ready'), .end()
|
|
9
|
+
class StubSshClient extends EventEmitter {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
this.connectCalls = [];
|
|
13
|
+
this.execCalls = [];
|
|
14
|
+
this._channels = [];
|
|
15
|
+
this._cannedOutput = '';
|
|
16
|
+
}
|
|
17
|
+
connect(opts) {
|
|
18
|
+
this.connectCalls.push(opts);
|
|
19
|
+
// emit 'ready' on next tick
|
|
20
|
+
process.nextTick(() => this.emit('ready'));
|
|
21
|
+
}
|
|
22
|
+
exec(cmd, cb) {
|
|
23
|
+
this.execCalls.push(cmd);
|
|
24
|
+
const stream = new EventEmitter();
|
|
25
|
+
stream.stderr = new EventEmitter();
|
|
26
|
+
process.nextTick(() => {
|
|
27
|
+
cb(null, stream);
|
|
28
|
+
// Push canned output then close.
|
|
29
|
+
stream.emit('data', Buffer.from(this._cannedOutput || ''));
|
|
30
|
+
stream.emit('close', 0);
|
|
31
|
+
});
|
|
32
|
+
this._channels.push(stream);
|
|
33
|
+
}
|
|
34
|
+
end() {
|
|
35
|
+
process.nextTick(() => this.emit('close'));
|
|
36
|
+
}
|
|
37
|
+
setCannedOutput(text) { this._cannedOutput = text; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('As400LogAdapter', () => {
|
|
41
|
+
test('constructor sets keepalive defaults (T-2-02)', () => {
|
|
42
|
+
const adapter = new As400LogAdapter({ host: '1.2.3.4', username: 'u', password: 'p' });
|
|
43
|
+
assert.equal(adapter._sshOpts.keepaliveInterval, 30000);
|
|
44
|
+
assert.equal(adapter._sshOpts.keepaliveCountMax, 3);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('emits 6-key event for CPF/MCH/SQL lines (AC-2)', async () => {
|
|
48
|
+
const stub = new StubSshClient();
|
|
49
|
+
stub.setCannedOutput([
|
|
50
|
+
'CPF1234 ERROR Job DAILY01 ended abnormally',
|
|
51
|
+
'INFO This line should not match',
|
|
52
|
+
'MCH3601 Pointer not set for location *N',
|
|
53
|
+
''
|
|
54
|
+
].join('\n'));
|
|
55
|
+
const adapter = new As400LogAdapter({
|
|
56
|
+
host: '1.2.3.4', username: 'u', password: 'p',
|
|
57
|
+
pollInterval: 100, sshClientFactory: () => stub,
|
|
58
|
+
});
|
|
59
|
+
const events = [];
|
|
60
|
+
adapter.on('error', (e) => events.push(e));
|
|
61
|
+
await adapter.start();
|
|
62
|
+
await sleep(400);
|
|
63
|
+
await adapter.stop();
|
|
64
|
+
assert.ok(events.length >= 2, `expected 2 events, got ${events.length}`);
|
|
65
|
+
const e = events[0];
|
|
66
|
+
assert.equal(Object.keys(e).sort().join(','), 'adapter,errorCode,file,message,source,timestamp');
|
|
67
|
+
assert.equal(e.adapter, 'as400');
|
|
68
|
+
assert.match(e.errorCode, /^(CPF|MCH|SQL)\d{4}$/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('non-matching lines do not emit', async () => {
|
|
72
|
+
const stub = new StubSshClient();
|
|
73
|
+
stub.setCannedOutput('INFO Just an info line\nDEBUG diag\n');
|
|
74
|
+
const adapter = new As400LogAdapter({
|
|
75
|
+
host: '1.2.3.4', username: 'u', password: 'p',
|
|
76
|
+
pollInterval: 100, sshClientFactory: () => stub,
|
|
77
|
+
});
|
|
78
|
+
const events = [];
|
|
79
|
+
adapter.on('error', (e) => events.push(e));
|
|
80
|
+
await adapter.start();
|
|
81
|
+
await sleep(400);
|
|
82
|
+
await adapter.stop();
|
|
83
|
+
assert.equal(events.length, 0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('reconnect on close with exponential backoff (T-2-02)', async () => {
|
|
87
|
+
const factories = [];
|
|
88
|
+
const adapter = new As400LogAdapter({
|
|
89
|
+
host: '1.2.3.4', username: 'u', password: 'p',
|
|
90
|
+
pollInterval: 60000, // no poll churn
|
|
91
|
+
sshClientFactory: () => { const s = new StubSshClient(); factories.push(s); return s; },
|
|
92
|
+
backoffSchedule: [50, 100, 200], // shortened for test
|
|
93
|
+
});
|
|
94
|
+
await adapter.start();
|
|
95
|
+
await sleep(50);
|
|
96
|
+
// First client connected; force a close.
|
|
97
|
+
assert.ok(factories.length >= 1);
|
|
98
|
+
factories[0].emit('close');
|
|
99
|
+
await sleep(200); // wait for first backoff (50ms) to fire reconnect
|
|
100
|
+
assert.ok(factories.length >= 2, 'reconnect must spawn a second client after close');
|
|
101
|
+
await adapter.stop();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('lifecycle idempotent (AC-1)', async () => {
|
|
105
|
+
const stub = new StubSshClient();
|
|
106
|
+
const adapter = new As400LogAdapter({
|
|
107
|
+
host: '1.2.3.4', username: 'u', password: 'p',
|
|
108
|
+
pollInterval: 60000, sshClientFactory: () => stub,
|
|
109
|
+
});
|
|
110
|
+
await adapter.start();
|
|
111
|
+
await adapter.start();
|
|
112
|
+
await adapter.stop();
|
|
113
|
+
await adapter.stop();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('parser regex exported and isolated', () => {
|
|
117
|
+
assert.ok(AS400_MSGID_REGEX instanceof RegExp);
|
|
118
|
+
assert.ok('CPF1234 ...'.match(AS400_MSGID_REGEX));
|
|
119
|
+
assert.equal('INFO ...'.match(AS400_MSGID_REGEX), null);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// test/adapters/cobol/cobol.test.js
|
|
2
|
+
// Plan 02-11 contract tests for CobolLogAdapter.
|
|
3
|
+
// 10 tests covering AC-1/2/3/9/10, AD-6 CRLF tolerance + 80-col fixed-width
|
|
4
|
+
// preservation, multi-pattern parser (S0C7, SQLCODE, IEF, FILE STATUS),
|
|
5
|
+
// and parser-isolation surface.
|
|
6
|
+
|
|
7
|
+
import { test, describe } from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import {
|
|
10
|
+
writeFileSync,
|
|
11
|
+
appendFileSync,
|
|
12
|
+
truncateSync,
|
|
13
|
+
mkdtempSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
openSync,
|
|
16
|
+
writeSync,
|
|
17
|
+
closeSync,
|
|
18
|
+
} from 'node:fs';
|
|
19
|
+
import { tmpdir } from 'node:os';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
22
|
+
import { CobolLogAdapter } from '../../../src/adapters/cobol/index.js';
|
|
23
|
+
import {
|
|
24
|
+
COBOL_COMPLETION_REGEX,
|
|
25
|
+
COBOL_SQLCODE_REGEX,
|
|
26
|
+
COBOL_IEF_REGEX,
|
|
27
|
+
COBOL_FILE_STATUS_REGEX,
|
|
28
|
+
} from '../../../src/adapters/cobol/cobol-parser.js';
|
|
29
|
+
|
|
30
|
+
function tempDir() {
|
|
31
|
+
return mkdtempSync(join(tmpdir(), 'cobol-test-'));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function awaitEvents(adapter, n, timeoutMs = 12000) {
|
|
35
|
+
const events = [];
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const onErr = (e) => {
|
|
38
|
+
events.push(e);
|
|
39
|
+
if (events.length >= n) {
|
|
40
|
+
adapter.off('error', onErr);
|
|
41
|
+
resolve(events);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
adapter.on('error', onErr);
|
|
45
|
+
const t = setTimeout(() => {
|
|
46
|
+
adapter.off('error', onErr);
|
|
47
|
+
resolve(events);
|
|
48
|
+
}, timeoutMs);
|
|
49
|
+
t.unref?.();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('CobolLogAdapter', () => {
|
|
54
|
+
test('TAIL default — pre-existing not emitted (AC-9)', async () => {
|
|
55
|
+
const root = tempDir();
|
|
56
|
+
writeFileSync(join(root, 'spool1.txt'), 'SYSTEM COMPLETION CODE=0C7 pre-existing\n');
|
|
57
|
+
const adapter = new CobolLogAdapter({ spoolDir: root, interval: 200 });
|
|
58
|
+
await adapter.start();
|
|
59
|
+
const events = await awaitEvents(adapter, 1, 1500);
|
|
60
|
+
await adapter.stop();
|
|
61
|
+
assert.equal(events.length, 0);
|
|
62
|
+
rmSync(root, { recursive: true, force: true });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('emits S0C7 event with 6-key alphabetical-sorted shape (AC-2)', async () => {
|
|
66
|
+
const root = tempDir();
|
|
67
|
+
const f = join(root, 'spool1.txt');
|
|
68
|
+
writeFileSync(f, 'header\n');
|
|
69
|
+
const adapter = new CobolLogAdapter({ spoolDir: root, interval: 200 });
|
|
70
|
+
await adapter.start();
|
|
71
|
+
await sleep(400);
|
|
72
|
+
appendFileSync(f, ' SYSTEM COMPLETION CODE=0C7 REASON CODE=00000007\n');
|
|
73
|
+
const events = await awaitEvents(adapter, 1, 5000);
|
|
74
|
+
await adapter.stop();
|
|
75
|
+
assert.equal(events.length, 1);
|
|
76
|
+
assert.equal(
|
|
77
|
+
Object.keys(events[0]).sort().join(','),
|
|
78
|
+
'adapter,errorCode,file,message,source,timestamp',
|
|
79
|
+
);
|
|
80
|
+
assert.equal(events[0].adapter, 'cobol');
|
|
81
|
+
assert.equal(events[0].errorCode, 'S0C7');
|
|
82
|
+
rmSync(root, { recursive: true, force: true });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('emits SQLCODE -805 event', async () => {
|
|
86
|
+
const root = tempDir();
|
|
87
|
+
const f = join(root, 'spool1.txt');
|
|
88
|
+
writeFileSync(f, 'header\n');
|
|
89
|
+
const adapter = new CobolLogAdapter({ spoolDir: root, interval: 200 });
|
|
90
|
+
await adapter.start();
|
|
91
|
+
await sleep(400);
|
|
92
|
+
appendFileSync(
|
|
93
|
+
f,
|
|
94
|
+
'DSNT408I SQLCODE = -805, ERROR: DBRM OR PACKAGE NAME PROD.CUSTUPD NOT FOUND IN PLAN\n',
|
|
95
|
+
);
|
|
96
|
+
const events = await awaitEvents(adapter, 1, 5000);
|
|
97
|
+
await adapter.stop();
|
|
98
|
+
assert.ok(events.length >= 1);
|
|
99
|
+
assert.equal(events[0].errorCode, '-805');
|
|
100
|
+
rmSync(root, { recursive: true, force: true });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('emits FILE STATUS 35 event', async () => {
|
|
104
|
+
const root = tempDir();
|
|
105
|
+
const f = join(root, 'spool1.txt');
|
|
106
|
+
writeFileSync(f, 'header\n');
|
|
107
|
+
const adapter = new CobolLogAdapter({ spoolDir: root, interval: 200 });
|
|
108
|
+
await adapter.start();
|
|
109
|
+
await sleep(400);
|
|
110
|
+
appendFileSync(f, "IGZ0035S - FILE STATUS 35 - File 'CUSTIN' was not found.\n");
|
|
111
|
+
const events = await awaitEvents(adapter, 1, 5000);
|
|
112
|
+
await adapter.stop();
|
|
113
|
+
assert.ok(events.length >= 1);
|
|
114
|
+
assert.match(events[0].errorCode, /^(FILE_STATUS_35|IGZ0035S)$/);
|
|
115
|
+
rmSync(root, { recursive: true, force: true });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('CRLF line endings tolerated (AD-6 z/OS spool trap)', async () => {
|
|
119
|
+
const root = tempDir();
|
|
120
|
+
const f = join(root, 'spool1.txt');
|
|
121
|
+
writeFileSync(f, 'header\r\n');
|
|
122
|
+
const adapter = new CobolLogAdapter({ spoolDir: root, interval: 200 });
|
|
123
|
+
await adapter.start();
|
|
124
|
+
await sleep(400);
|
|
125
|
+
const fd = openSync(f, 'a');
|
|
126
|
+
const buf = Buffer.from(
|
|
127
|
+
' SYSTEM COMPLETION CODE=0C7 REASON CODE=00000007\r\n',
|
|
128
|
+
'utf8',
|
|
129
|
+
);
|
|
130
|
+
writeSync(fd, buf, 0, buf.length);
|
|
131
|
+
closeSync(fd);
|
|
132
|
+
const events = await awaitEvents(adapter, 1, 5000);
|
|
133
|
+
await adapter.stop();
|
|
134
|
+
assert.ok(events.length >= 1);
|
|
135
|
+
assert.equal(events[0].errorCode, 'S0C7');
|
|
136
|
+
// \r artifact must NOT appear in event.message
|
|
137
|
+
assert.equal(
|
|
138
|
+
events[0].message.includes('\r'),
|
|
139
|
+
false,
|
|
140
|
+
'event.message must not contain raw \\r artifact',
|
|
141
|
+
);
|
|
142
|
+
rmSync(root, { recursive: true, force: true });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('80-col padded ABEND header preserves column-1 control char (AD-6 fixed-width trap)', async () => {
|
|
146
|
+
const root = tempDir();
|
|
147
|
+
const f = join(root, 'spool1.txt');
|
|
148
|
+
writeFileSync(f, 'header\n');
|
|
149
|
+
const adapter = new CobolLogAdapter({ spoolDir: root, interval: 200 });
|
|
150
|
+
await adapter.start();
|
|
151
|
+
await sleep(400);
|
|
152
|
+
// Line begins with column-1 '0' (page-eject control char), padded to 80 cols
|
|
153
|
+
const line =
|
|
154
|
+
'0SYSTEM COMPLETION CODE=0C7 REASON CODE=00000007 ';
|
|
155
|
+
assert.equal(line.length, 80);
|
|
156
|
+
appendFileSync(f, line + '\n');
|
|
157
|
+
const events = await awaitEvents(adapter, 1, 5000);
|
|
158
|
+
await adapter.stop();
|
|
159
|
+
assert.ok(events.length >= 1);
|
|
160
|
+
// message should retain the leading '0' control char
|
|
161
|
+
assert.equal(
|
|
162
|
+
events[0].message[0],
|
|
163
|
+
'0',
|
|
164
|
+
'column-1 control char must not be trimmed',
|
|
165
|
+
);
|
|
166
|
+
assert.equal(events[0].errorCode, 'S0C7');
|
|
167
|
+
rmSync(root, { recursive: true, force: true });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('lifecycle idempotent — start/start/stop/stop (AC-1)', async () => {
|
|
171
|
+
const root = tempDir();
|
|
172
|
+
writeFileSync(join(root, 'spool1.txt'), '');
|
|
173
|
+
const adapter = new CobolLogAdapter({ spoolDir: root, interval: 200 });
|
|
174
|
+
await adapter.start();
|
|
175
|
+
await adapter.start();
|
|
176
|
+
await adapter.stop();
|
|
177
|
+
await adapter.stop();
|
|
178
|
+
rmSync(root, { recursive: true, force: true });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('rotation — truncate + append (AC-10)', async () => {
|
|
182
|
+
const root = tempDir();
|
|
183
|
+
const f = join(root, 'spool1.txt');
|
|
184
|
+
writeFileSync(f, 'old\n');
|
|
185
|
+
const adapter = new CobolLogAdapter({ spoolDir: root, interval: 200 });
|
|
186
|
+
await adapter.start();
|
|
187
|
+
await sleep(400);
|
|
188
|
+
truncateSync(f, 0);
|
|
189
|
+
await sleep(400);
|
|
190
|
+
appendFileSync(f, 'SYSTEM COMPLETION CODE=0C7\n');
|
|
191
|
+
const events = await awaitEvents(adapter, 1, 5000);
|
|
192
|
+
await adapter.stop();
|
|
193
|
+
assert.ok(events.length >= 1);
|
|
194
|
+
rmSync(root, { recursive: true, force: true });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('ENOENT silent — nonexistent spoolDir does not throw', async () => {
|
|
198
|
+
const adapter = new CobolLogAdapter({
|
|
199
|
+
spoolDir: '/nonexistent-' + Date.now(),
|
|
200
|
+
interval: 200,
|
|
201
|
+
});
|
|
202
|
+
await adapter.start();
|
|
203
|
+
await sleep(500);
|
|
204
|
+
await adapter.stop();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('parser regexes isolated and exported as named consts', () => {
|
|
208
|
+
assert.ok(COBOL_COMPLETION_REGEX instanceof RegExp);
|
|
209
|
+
assert.ok(COBOL_SQLCODE_REGEX instanceof RegExp);
|
|
210
|
+
assert.ok(COBOL_IEF_REGEX instanceof RegExp);
|
|
211
|
+
assert.ok(COBOL_FILE_STATUS_REGEX instanceof RegExp);
|
|
212
|
+
assert.ok('SYSTEM COMPLETION CODE=0C7'.match(COBOL_COMPLETION_REGEX));
|
|
213
|
+
assert.ok('SQLCODE = -805'.match(COBOL_SQLCODE_REGEX));
|
|
214
|
+
assert.ok('IEF450I DAILY01 STEP01 - ABEND'.match(COBOL_IEF_REGEX));
|
|
215
|
+
assert.ok('FILE STATUS 35'.match(COBOL_FILE_STATUS_REGEX));
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { writeFileSync, appendFileSync, truncateSync, mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
7
|
+
import { IbmMqLogAdapter } from '../../../src/adapters/ibmmq/index.js';
|
|
8
|
+
import { IBMMQ_AMQ_REGEX } from '../../../src/adapters/ibmmq/ibmmq-parser.js';
|
|
9
|
+
|
|
10
|
+
function tempErrorsDir() {
|
|
11
|
+
return mkdtempSync(join(tmpdir(), 'ibmmq-test-'));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function awaitEvents(adapter, n, timeoutMs = 12000) {
|
|
15
|
+
const events = [];
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
const onErr = (e) => {
|
|
18
|
+
events.push(e);
|
|
19
|
+
if (events.length >= n) {
|
|
20
|
+
adapter.off('error', onErr);
|
|
21
|
+
resolve(events);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
adapter.on('error', onErr);
|
|
25
|
+
setTimeout(() => { adapter.off('error', onErr); resolve(events); }, timeoutMs);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('IbmMqLogAdapter', () => {
|
|
30
|
+
test('TAIL default — pre-existing not emitted (AC-9)', async () => {
|
|
31
|
+
const root = tempErrorsDir();
|
|
32
|
+
writeFileSync(join(root, 'AMQERR01.LOG'), 'AMQ7472W: pre-existing line\n');
|
|
33
|
+
const adapter = new IbmMqLogAdapter({ errorsDir: root, interval: 200 });
|
|
34
|
+
await adapter.start();
|
|
35
|
+
const events = await awaitEvents(adapter, 1, 1500);
|
|
36
|
+
await adapter.stop();
|
|
37
|
+
assert.equal(events.length, 0, 'no events should fire for pre-existing content');
|
|
38
|
+
rmSync(root, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('emits 6-key event on AMQERR line (AC-2)', async () => {
|
|
42
|
+
const root = tempErrorsDir();
|
|
43
|
+
const f = join(root, 'AMQERR01.LOG');
|
|
44
|
+
writeFileSync(f, '-- start --\n');
|
|
45
|
+
const adapter = new IbmMqLogAdapter({ errorsDir: root, interval: 200 });
|
|
46
|
+
await adapter.start();
|
|
47
|
+
await sleep(400);
|
|
48
|
+
appendFileSync(f, 'AMQ7472W: Object Q.ORDER.IN, type queue, get returned 2033 (MQRC_NO_MSG_AVAILABLE).\n');
|
|
49
|
+
const events = await awaitEvents(adapter, 1, 5000);
|
|
50
|
+
await adapter.stop();
|
|
51
|
+
assert.equal(events.length, 1);
|
|
52
|
+
assert.equal(Object.keys(events[0]).sort().join(','), 'adapter,errorCode,file,message,source,timestamp');
|
|
53
|
+
assert.equal(events[0].adapter, 'ibmmq');
|
|
54
|
+
assert.equal(events[0].errorCode, 'AMQ7472W');
|
|
55
|
+
assert.equal(events[0].file, f);
|
|
56
|
+
assert.match(events[0].message, /AMQ7472W/);
|
|
57
|
+
assert.equal(events[0].source, root);
|
|
58
|
+
assert.match(events[0].timestamp, /^\d{4}-\d{2}-\d{2}T/);
|
|
59
|
+
rmSync(root, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('lifecycle idempotent (AC-1)', async () => {
|
|
63
|
+
const root = tempErrorsDir();
|
|
64
|
+
writeFileSync(join(root, 'AMQERR01.LOG'), '');
|
|
65
|
+
const adapter = new IbmMqLogAdapter({ errorsDir: root, interval: 200 });
|
|
66
|
+
await adapter.start();
|
|
67
|
+
await adapter.start(); // idempotent
|
|
68
|
+
await adapter.stop();
|
|
69
|
+
await adapter.stop(); // idempotent
|
|
70
|
+
await adapter.start();
|
|
71
|
+
await adapter.stop();
|
|
72
|
+
rmSync(root, { recursive: true, force: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('rotation: truncate + append (AC-10)', async () => {
|
|
76
|
+
const root = tempErrorsDir();
|
|
77
|
+
const f = join(root, 'AMQERR01.LOG');
|
|
78
|
+
writeFileSync(f, 'AMQ0000I: old\n');
|
|
79
|
+
const adapter = new IbmMqLogAdapter({ errorsDir: root, interval: 200 });
|
|
80
|
+
await adapter.start();
|
|
81
|
+
await sleep(400);
|
|
82
|
+
truncateSync(f, 0);
|
|
83
|
+
await sleep(400);
|
|
84
|
+
appendFileSync(f, 'AMQ9540E: queue manager name error.\n');
|
|
85
|
+
const events = await awaitEvents(adapter, 1, 5000);
|
|
86
|
+
await adapter.stop();
|
|
87
|
+
assert.ok(events.length >= 1, 'rotation should produce at least 1 event');
|
|
88
|
+
rmSync(root, { recursive: true, force: true });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('ENOENT silent — no event, no throw', async () => {
|
|
92
|
+
const adapter = new IbmMqLogAdapter({ errorsDir: '/nonexistent-' + Date.now(), interval: 200 });
|
|
93
|
+
await adapter.start();
|
|
94
|
+
await sleep(500);
|
|
95
|
+
await adapter.stop();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('parser regex exported and isolated', () => {
|
|
99
|
+
assert.ok(IBMMQ_AMQ_REGEX instanceof RegExp);
|
|
100
|
+
const m = 'AMQ7472W: Object ...'.match(IBMMQ_AMQ_REGEX);
|
|
101
|
+
assert.ok(m);
|
|
102
|
+
assert.equal(m[1], 'AMQ7472W');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('glob anchored — AMQERR04.LOG not watched', async () => {
|
|
106
|
+
const root = tempErrorsDir();
|
|
107
|
+
// Create AMQERR04.LOG (NOT in 01..03 range) and a random file
|
|
108
|
+
const out = join(root, 'AMQERR04.LOG');
|
|
109
|
+
const random = join(root, 'random.log');
|
|
110
|
+
writeFileSync(out, '');
|
|
111
|
+
writeFileSync(random, '');
|
|
112
|
+
const adapter = new IbmMqLogAdapter({ errorsDir: root, interval: 200 });
|
|
113
|
+
await adapter.start();
|
|
114
|
+
await sleep(400);
|
|
115
|
+
appendFileSync(out, 'AMQ9999E: should not be emitted\n');
|
|
116
|
+
appendFileSync(random, 'AMQ9999E: should not be emitted\n');
|
|
117
|
+
const events = await awaitEvents(adapter, 1, 1500);
|
|
118
|
+
await adapter.stop();
|
|
119
|
+
assert.equal(events.length, 0, 'adapter must not watch outside AMQERR0[1-3].LOG glob');
|
|
120
|
+
rmSync(root, { recursive: true, force: true });
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
writeFileSync,
|
|
5
|
+
appendFileSync,
|
|
6
|
+
truncateSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
openSync,
|
|
10
|
+
writeSync,
|
|
11
|
+
closeSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
16
|
+
import { OracleFormsLogAdapter } from '../../../src/adapters/oracle-forms/index.js';
|
|
17
|
+
import { ORACLE_FORMS_REGEX } from '../../../src/adapters/oracle-forms/oracle-forms-parser.js';
|
|
18
|
+
|
|
19
|
+
function tempDir() {
|
|
20
|
+
return mkdtempSync(join(tmpdir(), 'forms-test-'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function awaitEvents(adapter, n, timeoutMs = 12000) {
|
|
24
|
+
const events = [];
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const onErr = (e) => {
|
|
27
|
+
events.push(e);
|
|
28
|
+
if (events.length >= n) {
|
|
29
|
+
adapter.off('error', onErr);
|
|
30
|
+
resolve(events);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
adapter.on('error', onErr);
|
|
34
|
+
const t = setTimeout(() => {
|
|
35
|
+
adapter.off('error', onErr);
|
|
36
|
+
resolve(events);
|
|
37
|
+
}, timeoutMs);
|
|
38
|
+
t.unref?.();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('OracleFormsLogAdapter', () => {
|
|
43
|
+
test('TAIL default — pre-existing not emitted (AC-9)', async () => {
|
|
44
|
+
const root = tempDir();
|
|
45
|
+
writeFileSync(join(root, 'forms.trc'), 'Line 1: ORA-01403: no data found\n');
|
|
46
|
+
const adapter = new OracleFormsLogAdapter({ traceDir: root, interval: 200 });
|
|
47
|
+
await adapter.start();
|
|
48
|
+
const events = await awaitEvents(adapter, 1, 1500);
|
|
49
|
+
await adapter.stop();
|
|
50
|
+
assert.equal(events.length, 0);
|
|
51
|
+
rmSync(root, { recursive: true, force: true });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('emits 6-key event for ORA line (AC-2)', async () => {
|
|
55
|
+
const root = tempDir();
|
|
56
|
+
const f = join(root, 'forms.trc');
|
|
57
|
+
writeFileSync(f, 'header\n');
|
|
58
|
+
const adapter = new OracleFormsLogAdapter({ traceDir: root, interval: 200 });
|
|
59
|
+
await adapter.start();
|
|
60
|
+
await sleep(400);
|
|
61
|
+
appendFileSync(f, 'Line 44: ORA-01403: no data found\n');
|
|
62
|
+
const events = await awaitEvents(adapter, 1, 5000);
|
|
63
|
+
await adapter.stop();
|
|
64
|
+
assert.equal(events.length, 1);
|
|
65
|
+
assert.equal(
|
|
66
|
+
Object.keys(events[0]).sort().join(','),
|
|
67
|
+
'adapter,errorCode,file,message,source,timestamp',
|
|
68
|
+
);
|
|
69
|
+
assert.equal(events[0].adapter, 'oracle-forms');
|
|
70
|
+
assert.equal(events[0].errorCode, 'ORA-01403');
|
|
71
|
+
rmSync(root, { recursive: true, force: true });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('emits FRM event', async () => {
|
|
75
|
+
const root = tempDir();
|
|
76
|
+
const f = join(root, 'forms.trc');
|
|
77
|
+
writeFileSync(f, 'header\n');
|
|
78
|
+
const adapter = new OracleFormsLogAdapter({ traceDir: root, interval: 200 });
|
|
79
|
+
await adapter.start();
|
|
80
|
+
await sleep(400);
|
|
81
|
+
appendFileSync(
|
|
82
|
+
f,
|
|
83
|
+
'Line 89: FRM-40735: ON-ERROR trigger raised unhandled exception ORA-20100.\n',
|
|
84
|
+
);
|
|
85
|
+
const events = await awaitEvents(adapter, 1, 5000);
|
|
86
|
+
await adapter.stop();
|
|
87
|
+
assert.equal(events.length, 1);
|
|
88
|
+
assert.equal(events[0].errorCode, 'FRM-40735');
|
|
89
|
+
rmSync(root, { recursive: true, force: true });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('Latin-1 bytes do not crash adapter (AD-6 mojibake trap)', async () => {
|
|
93
|
+
const root = tempDir();
|
|
94
|
+
const f = join(root, 'forms.trc');
|
|
95
|
+
writeFileSync(f, 'header\n');
|
|
96
|
+
const adapter = new OracleFormsLogAdapter({ traceDir: root, interval: 200 });
|
|
97
|
+
await adapter.start();
|
|
98
|
+
await sleep(400);
|
|
99
|
+
// Append Latin-1 bytes including 0xE9 ("é"): "Line 102: ORA-06502: Café\n"
|
|
100
|
+
const fd = openSync(f, 'a');
|
|
101
|
+
const latin1 = Buffer.from([
|
|
102
|
+
0x4c, 0x69, 0x6e, 0x65, 0x20, 0x31, 0x30, 0x32, 0x3a, 0x20, 0x4f, 0x52,
|
|
103
|
+
0x41, 0x2d, 0x30, 0x36, 0x35, 0x30, 0x32, 0x3a, 0x20, 0x43, 0x61, 0x66,
|
|
104
|
+
0xe9, 0x0a,
|
|
105
|
+
]);
|
|
106
|
+
writeSync(fd, latin1, 0, latin1.length);
|
|
107
|
+
closeSync(fd);
|
|
108
|
+
const events = await awaitEvents(adapter, 1, 5000);
|
|
109
|
+
await adapter.stop();
|
|
110
|
+
assert.ok(
|
|
111
|
+
events.length >= 1,
|
|
112
|
+
'adapter should not crash on Latin-1 byte; should still emit ORA-06502',
|
|
113
|
+
);
|
|
114
|
+
assert.equal(events[0].errorCode, 'ORA-06502');
|
|
115
|
+
rmSync(root, { recursive: true, force: true });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('lifecycle idempotent (AC-1)', async () => {
|
|
119
|
+
const root = tempDir();
|
|
120
|
+
writeFileSync(join(root, 'forms.trc'), '');
|
|
121
|
+
const adapter = new OracleFormsLogAdapter({ traceDir: root, interval: 200 });
|
|
122
|
+
await adapter.start();
|
|
123
|
+
await adapter.start();
|
|
124
|
+
await adapter.stop();
|
|
125
|
+
await adapter.stop();
|
|
126
|
+
rmSync(root, { recursive: true, force: true });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('rotation: truncate + append (AC-10)', async () => {
|
|
130
|
+
const root = tempDir();
|
|
131
|
+
const f = join(root, 'forms.trc');
|
|
132
|
+
writeFileSync(f, 'Line 1: OLD ORA-99999: stale\n');
|
|
133
|
+
const adapter = new OracleFormsLogAdapter({ traceDir: root, interval: 200 });
|
|
134
|
+
await adapter.start();
|
|
135
|
+
await sleep(400);
|
|
136
|
+
truncateSync(f, 0);
|
|
137
|
+
await sleep(400);
|
|
138
|
+
appendFileSync(f, 'Line 1: ORA-01403: no data found\n');
|
|
139
|
+
const events = await awaitEvents(adapter, 1, 5000);
|
|
140
|
+
await adapter.stop();
|
|
141
|
+
assert.ok(events.length >= 1);
|
|
142
|
+
rmSync(root, { recursive: true, force: true });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('ENOENT silent', async () => {
|
|
146
|
+
const adapter = new OracleFormsLogAdapter({
|
|
147
|
+
traceDir: '/nonexistent-' + Date.now(),
|
|
148
|
+
interval: 200,
|
|
149
|
+
});
|
|
150
|
+
await adapter.start();
|
|
151
|
+
await sleep(500);
|
|
152
|
+
await adapter.stop();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('parser regex isolated', () => {
|
|
156
|
+
assert.ok(ORACLE_FORMS_REGEX instanceof RegExp);
|
|
157
|
+
const m1 = 'Line 44: ORA-01403: no data found'.match(ORACLE_FORMS_REGEX);
|
|
158
|
+
assert.ok(m1);
|
|
159
|
+
assert.equal(m1[1], 'ORA-01403');
|
|
160
|
+
const m2 = 'Line 89: FRM-40735: ON-ERROR ...'.match(ORACLE_FORMS_REGEX);
|
|
161
|
+
assert.ok(m2);
|
|
162
|
+
assert.equal(m2[1], 'FRM-40735');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('glob anchored — *.log not watched', async () => {
|
|
166
|
+
const root = tempDir();
|
|
167
|
+
writeFileSync(join(root, 'other.log'), '');
|
|
168
|
+
const adapter = new OracleFormsLogAdapter({ traceDir: root, interval: 200 });
|
|
169
|
+
await adapter.start();
|
|
170
|
+
await sleep(400);
|
|
171
|
+
appendFileSync(
|
|
172
|
+
join(root, 'other.log'),
|
|
173
|
+
'Line 1: ORA-01403: should not be emitted\n',
|
|
174
|
+
);
|
|
175
|
+
const events = await awaitEvents(adapter, 1, 1500);
|
|
176
|
+
await adapter.stop();
|
|
177
|
+
assert.equal(events.length, 0);
|
|
178
|
+
rmSync(root, { recursive: true, force: true });
|
|
179
|
+
});
|
|
180
|
+
});
|