deepdebug-local-agent 1.0.22 → 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/README.md +81 -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/server.js +46 -2
- 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,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
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// test/adapters/registry.test.js
|
|
2
|
+
// Contract tests for the Phase 2 adapter registry — Plan 02-12 Wave 3.
|
|
3
|
+
// Validates loadAdapters() construction, allowed-stack enforcement, and
|
|
4
|
+
// stopAll() lifecycle/timeout behaviour (AC-4, AC-11).
|
|
5
|
+
|
|
6
|
+
import { test, describe } from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import {
|
|
9
|
+
loadAdapters,
|
|
10
|
+
stopAll,
|
|
11
|
+
ALLOWED_STACK_LIST,
|
|
12
|
+
} from '../../src/adapters/index.js';
|
|
13
|
+
|
|
14
|
+
describe('adapter registry', () => {
|
|
15
|
+
test('loadAdapters({legacyStacks:[]}) returns []', () => {
|
|
16
|
+
assert.deepEqual(loadAdapters({ legacyStacks: [] }), []);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('loadAdapters({}) returns [] (missing legacyStacks treated as empty)', () => {
|
|
20
|
+
assert.deepEqual(loadAdapters({}), []);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('loadAdapters({legacyStacks:["sap"]}) returns 1 SapLogAdapter, unstarted', () => {
|
|
24
|
+
const adapters = loadAdapters({ legacyStacks: ['sap'] });
|
|
25
|
+
assert.equal(adapters.length, 1);
|
|
26
|
+
assert.equal(adapters[0].constructor.name, 'SapLogAdapter');
|
|
27
|
+
assert.equal(adapters[0].isRunning, false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('loadAdapters with all 5 stacks returns 5 unstarted adapters', () => {
|
|
31
|
+
const adapters = loadAdapters({ legacyStacks: ALLOWED_STACK_LIST.slice() });
|
|
32
|
+
assert.equal(adapters.length, 5);
|
|
33
|
+
const ctorNames = adapters.map((a) => a.constructor.name).sort();
|
|
34
|
+
assert.deepEqual(
|
|
35
|
+
ctorNames,
|
|
36
|
+
[
|
|
37
|
+
'As400LogAdapter',
|
|
38
|
+
'CobolLogAdapter',
|
|
39
|
+
'IbmMqLogAdapter',
|
|
40
|
+
'OracleFormsLogAdapter',
|
|
41
|
+
'SapLogAdapter',
|
|
42
|
+
],
|
|
43
|
+
);
|
|
44
|
+
for (const a of adapters) assert.equal(a.isRunning, false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('loadAdapters({legacyStacks:["unknown"]}) throws with stack name', () => {
|
|
48
|
+
assert.throws(
|
|
49
|
+
() => loadAdapters({ legacyStacks: ['unknown'] }),
|
|
50
|
+
/unknown/,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('loadAdapters propagates per-stack config to constructor', () => {
|
|
55
|
+
const adapters = loadAdapters({
|
|
56
|
+
legacyStacks: ['sap'],
|
|
57
|
+
sap: { sapDir: '/tmp/test-sap', instanceNumber: '42', interval: 1234 },
|
|
58
|
+
});
|
|
59
|
+
assert.equal(adapters[0].sapDir, '/tmp/test-sap');
|
|
60
|
+
assert.equal(adapters[0].instanceNumber, '42');
|
|
61
|
+
assert.equal(adapters[0].interval, 1234);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('stopAll([]) resolves silently', async () => {
|
|
65
|
+
await stopAll([]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('stopAll(undefined) resolves silently', async () => {
|
|
69
|
+
await stopAll(undefined);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('stopAll calls stop() on each adapter', async () => {
|
|
73
|
+
let stopped = 0;
|
|
74
|
+
const fake = { stop: async () => { stopped++; } };
|
|
75
|
+
await stopAll([fake, fake, fake]);
|
|
76
|
+
assert.equal(stopped, 3);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('stopAll respects timeout — slow stop does not block forever', async () => {
|
|
80
|
+
const slow = { stop: () => new Promise(() => { /* never resolve */ }) };
|
|
81
|
+
const before = Date.now();
|
|
82
|
+
await stopAll([slow], 200);
|
|
83
|
+
const elapsed = Date.now() - before;
|
|
84
|
+
assert.ok(elapsed < 1000, `stopAll should return within timeout; took ${elapsed}ms`);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('stopAll swallows per-adapter stop() errors (never throws)', async () => {
|
|
88
|
+
const bad = { stop: async () => { throw new Error('boom'); } };
|
|
89
|
+
const good = { stop: async () => { /* ok */ } };
|
|
90
|
+
// Should resolve without rejecting.
|
|
91
|
+
await stopAll([bad, good, bad]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('ALLOWED_STACK_LIST is the canonical 5-stack list, frozen', () => {
|
|
95
|
+
assert.deepEqual(
|
|
96
|
+
[...ALLOWED_STACK_LIST].sort(),
|
|
97
|
+
['as400', 'cobol', 'ibmmq', 'oracle-forms', 'sap'],
|
|
98
|
+
);
|
|
99
|
+
assert.equal(Object.isFrozen(ALLOWED_STACK_LIST), true);
|
|
100
|
+
});
|
|
101
|
+
});
|