@jrpool/kilotest 46.0.8 → 46.0.11
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/.devin/config.local.json +3 -1
- package/.github/workflows/{smoke-test.yml → ci.yml} +8 -2
- package/alerts.test.js +137 -0
- package/api/listDiagnoses.test.js +44 -0
- package/api/listIssues.js +1 -1
- package/api/listIssues.test.js +10 -0
- package/api/listReports.test.js +6 -2
- package/api/listViolators.test.js +35 -0
- package/api/requestTest.test.js +7 -0
- package/api/routes.test.js +63 -0
- package/api/util.js +8 -10
- package/api/util.test.js +68 -2
- package/apiIntegration.test.js +13 -3
- package/docs/{Smoke Test.json → CI.json} +4 -3
- package/index.js +64 -77
- package/index.test.js +1152 -0
- package/indexBalances.test.js +188 -0
- package/indexFallback.test.js +33 -0
- package/indexServe.test.js +59 -0
- package/indexWorkers.test.js +79 -0
- package/mcp.js +2 -2
- package/mcp.test.js +281 -0
- package/package.json +10 -5
- package/routing.test.js +13 -3
- package/test/fixtures/buildFixtures.js +83 -0
- package/test/fixtures/db/reports/260101T0008-mul.json +137 -0
- package/test/fixtures/db/reports/260101T0009-brd.json +147 -0
- package/test/setup.cjs +14 -0
- package/util.js +16 -6
- package/util.test.js +886 -3
- package/web/ai0BalanceForm/ai0BalanceForm.test.js +88 -0
- package/web/expungeReportsForm/expungeReportsForm.test.js +114 -0
- package/web/hideReportForm/hideReportForm.test.js +82 -0
- package/web/listDiagnoses/listDiagnoses.test.js +160 -0
- package/web/listIssues/listIssues.test.js +109 -0
- package/web/listReports/listReports.test.js +139 -3
- package/web/listRules/listRules.test.js +103 -0
- package/web/listTopIssues/listTopIssues.test.js +126 -0
- package/web/listViolators/index.js +1 -2
- package/web/listViolators/listViolators.test.js +256 -0
- package/web/pruneReportsForm/pruneReportsForm.test.js +86 -0
- package/web/reannotate/reannotate.test.js +144 -0
- package/web/reannotateForm/reannotateForm.test.js +134 -0
- package/web/renewWCAG/renewWCAG.test.js +124 -0
- package/web/requestRetest/requestRetest.test.js +119 -0
- package/web/requestRetestForm/requestRetestForm.test.js +62 -0
- package/web/requestTest/requestTest.test.js +115 -0
- package/web/requestTestForm/requestTestForm.test.js +40 -0
- package/web/rewindReportsForm/rewindReportsForm.test.js +86 -0
- package/web/tutorial/tutorial.test.js +97 -0
- package/web/unhideReportForm/unhideReportForm.test.js +135 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/*
|
|
2
|
+
indexBalances.test.js
|
|
3
|
+
Tests for checkBalancesForAlerts in index.js, which requires balance-related
|
|
4
|
+
env vars to be set before index.js is loaded.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ENVIRONMENT (must be set before requiring index.js)
|
|
8
|
+
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const fixtureDBDir = path.join(__dirname, 'test', 'fixtures', 'db');
|
|
11
|
+
|
|
12
|
+
process.env.DB_DIR = fixtureDBDir;
|
|
13
|
+
process.env.AUTH_CODE = 'test-auth-code';
|
|
14
|
+
process.env.TESTARO_WORKERS = JSON.stringify({
|
|
15
|
+
worker1: {secret: 'secret1', name: 'Worker One'}
|
|
16
|
+
});
|
|
17
|
+
process.env.WAVE_BALANCE_THRESHOLD = '100';
|
|
18
|
+
process.env.AI_SERVICE0_BALANCE_THRESHOLD = '50';
|
|
19
|
+
process.env.AI_MODEL0_INPUT_PRICE = '0.001';
|
|
20
|
+
process.env.AI_MODEL0_OUTPUT_PRICE = '0.002';
|
|
21
|
+
|
|
22
|
+
const {test, before, after} = require('node:test');
|
|
23
|
+
const assert = require('node:assert/strict');
|
|
24
|
+
const http = require('node:http');
|
|
25
|
+
const fs = require('node:fs/promises');
|
|
26
|
+
const {requestHandler} = require('./index');
|
|
27
|
+
|
|
28
|
+
// CONSTANTS
|
|
29
|
+
|
|
30
|
+
const port = 3994;
|
|
31
|
+
const balancePath = path.join(__dirname, 'ai0Balance.json');
|
|
32
|
+
const jobID = '990101T0001-bal';
|
|
33
|
+
const reportPath = path.join(fixtureDBDir, 'reports', `${jobID}.json`);
|
|
34
|
+
const claimedDir = path.join(fixtureDBDir, 'jobs', 'claimed');
|
|
35
|
+
const jobPath = path.join(claimedDir, `${jobID}.json`);
|
|
36
|
+
|
|
37
|
+
// SETUP AND TEARDOWN
|
|
38
|
+
|
|
39
|
+
let server;
|
|
40
|
+
let originalBalance;
|
|
41
|
+
|
|
42
|
+
before(async () => {
|
|
43
|
+
originalBalance = await fs.readFile(balancePath, 'utf8').catch(() => null);
|
|
44
|
+
server = http.createServer(requestHandler);
|
|
45
|
+
await new Promise(resolve => server.listen(port, () => resolve()));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
after(async () => {
|
|
49
|
+
server.closeAllConnections?.();
|
|
50
|
+
await new Promise(resolve => {
|
|
51
|
+
const timer = setTimeout(() => {
|
|
52
|
+
server.closeAllConnections?.();
|
|
53
|
+
resolve();
|
|
54
|
+
}, 1000);
|
|
55
|
+
server.close(() => {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
resolve();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
if (originalBalance !== null) {
|
|
61
|
+
await fs.writeFile(balancePath, originalBalance);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
await fs.unlink(balancePath).catch(() => {});
|
|
65
|
+
}
|
|
66
|
+
// Clean up any report and job files created by tests.
|
|
67
|
+
await fs.unlink(reportPath).catch(() => {});
|
|
68
|
+
await fs.unlink(jobPath).catch(() => {});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// HELPERS
|
|
72
|
+
|
|
73
|
+
const request = (method, requestPath, body = null, headers = {}) => new Promise((resolve, reject) => {
|
|
74
|
+
const options = {method, host: 'localhost', port, path: requestPath, headers: {...headers}};
|
|
75
|
+
let bodyData = '';
|
|
76
|
+
if (body) {
|
|
77
|
+
bodyData = JSON.stringify(body);
|
|
78
|
+
options.headers['content-type'] = 'application/json';
|
|
79
|
+
options.headers['content-length'] = Buffer.byteLength(bodyData);
|
|
80
|
+
}
|
|
81
|
+
const req = http.request(options, response => {
|
|
82
|
+
const chunks = [];
|
|
83
|
+
response.on('data', chunk => chunks.push(chunk));
|
|
84
|
+
response.on('end', () => {
|
|
85
|
+
resolve({statusCode: response.statusCode, headers: response.headers, body: Buffer.concat(chunks).toString()});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
req.on('error', reject);
|
|
89
|
+
req.end(bodyData || '');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const jsonBody = res => {
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(res.body);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const submitReport = async (acts, balanceFileContent) => {
|
|
102
|
+
if (balanceFileContent !== undefined) {
|
|
103
|
+
if (balanceFileContent === null) {
|
|
104
|
+
await fs.unlink(balancePath).catch(() => {});
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
await fs.writeFile(balancePath, balanceFileContent);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
await fs.writeFile(jobPath, JSON.stringify({
|
|
111
|
+
id: jobID,
|
|
112
|
+
target: {what: 'Test', url: 'https://example.com/test'},
|
|
113
|
+
sources: {worker: 'Worker One'}
|
|
114
|
+
}));
|
|
115
|
+
const auth = Buffer.from('worker1:secret1').toString('base64');
|
|
116
|
+
const report = {
|
|
117
|
+
id: jobID,
|
|
118
|
+
target: {what: 'Test', url: 'https://example.com/test'},
|
|
119
|
+
acts,
|
|
120
|
+
jobData: {endTime: '26-01-01T00:00'},
|
|
121
|
+
catalog: {}
|
|
122
|
+
};
|
|
123
|
+
const res = await request('POST', '/worker/report', {report}, {
|
|
124
|
+
authorization: `Basic ${auth}`
|
|
125
|
+
});
|
|
126
|
+
await fs.unlink(jobPath).catch(() => {});
|
|
127
|
+
await fs.unlink(reportPath).catch(() => {});
|
|
128
|
+
return res;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// TESTS
|
|
132
|
+
|
|
133
|
+
test('worker/report with WAVE balance low triggers a balance alert', async () => {
|
|
134
|
+
const res = await submitReport(
|
|
135
|
+
[
|
|
136
|
+
{type: 'test', which: 'wave', data: {creditsRemaining: 5}},
|
|
137
|
+
{type: 'test', which: 'testaro', data: {ruleData: {allCaps: {aiModelUsage: {inputTokens: 100, outputTokens: 50}}}}}
|
|
138
|
+
],
|
|
139
|
+
JSON.stringify({balance: 100})
|
|
140
|
+
);
|
|
141
|
+
assert.equal(res.statusCode, 200);
|
|
142
|
+
const body = jsonBody(res);
|
|
143
|
+
assert.equal(body.status, 'ok');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('worker/report with AI service balance low triggers a balance alert', async () => {
|
|
147
|
+
const res = await submitReport(
|
|
148
|
+
[
|
|
149
|
+
{type: 'test', which: 'wave', data: {creditsRemaining: 200}},
|
|
150
|
+
{type: 'test', which: 'testaro', data: {ruleData: {allCaps: {aiModelUsage: {inputTokens: 5000, outputTokens: 3000}}}}}
|
|
151
|
+
],
|
|
152
|
+
JSON.stringify({balance: 10})
|
|
153
|
+
);
|
|
154
|
+
assert.equal(res.statusCode, 200);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('worker/report with no balance file logs an error', async () => {
|
|
158
|
+
const res = await submitReport(
|
|
159
|
+
[
|
|
160
|
+
{type: 'test', which: 'wave', data: {creditsRemaining: 200}},
|
|
161
|
+
{type: 'test', which: 'testaro', data: {ruleData: {allCaps: {aiModelUsage: {inputTokens: 100, outputTokens: 50}}}}}
|
|
162
|
+
],
|
|
163
|
+
null
|
|
164
|
+
);
|
|
165
|
+
assert.equal(res.statusCode, 200);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('worker/report with invalid balance JSON logs an error', async () => {
|
|
169
|
+
const res = await submitReport(
|
|
170
|
+
[
|
|
171
|
+
{type: 'test', which: 'wave', data: {creditsRemaining: 200}},
|
|
172
|
+
{type: 'test', which: 'testaro', data: {ruleData: {allCaps: {aiModelUsage: {inputTokens: 100, outputTokens: 50}}}}}
|
|
173
|
+
],
|
|
174
|
+
'not valid json'
|
|
175
|
+
);
|
|
176
|
+
assert.equal(res.statusCode, 200);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('worker/report with non-number balance logs an error', async () => {
|
|
180
|
+
const res = await submitReport(
|
|
181
|
+
[
|
|
182
|
+
{type: 'test', which: 'wave', data: {creditsRemaining: 200}},
|
|
183
|
+
{type: 'test', which: 'testaro', data: {ruleData: {allCaps: {aiModelUsage: {inputTokens: 100, outputTokens: 50}}}}}
|
|
184
|
+
],
|
|
185
|
+
JSON.stringify({balance: 'not a number'})
|
|
186
|
+
);
|
|
187
|
+
assert.equal(res.statusCode, 200);
|
|
188
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
indexFallback.test.js
|
|
3
|
+
Tests that index.js loads successfully when PROTOCOL and TESTARO_WORKERS
|
|
4
|
+
are not set, exercising the || fallback branches at module load time.
|
|
5
|
+
This must be a separate file because index.js reads these env vars
|
|
6
|
+
at module load time, and each test file runs in its own process.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ENVIRONMENT (must be set before requiring index.js)
|
|
10
|
+
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const fixtureDBDir = path.join(__dirname, 'test', 'fixtures', 'db');
|
|
13
|
+
|
|
14
|
+
process.env.DB_DIR = fixtureDBDir;
|
|
15
|
+
process.env.AUTH_CODE = 'test-auth-code';
|
|
16
|
+
// Set PROTOCOL and TESTARO_WORKERS to empty strings (falsy) so that
|
|
17
|
+
// the || fallbacks in index.js are exercised. dotenv does not override
|
|
18
|
+
// existing env vars, so .env will not replace these.
|
|
19
|
+
process.env.PROTOCOL = '';
|
|
20
|
+
process.env.TESTARO_WORKERS = '';
|
|
21
|
+
|
|
22
|
+
// IMPORTS
|
|
23
|
+
|
|
24
|
+
const {test} = require('node:test');
|
|
25
|
+
const assert = require('node:assert/strict');
|
|
26
|
+
const {requestHandler} = require('./index');
|
|
27
|
+
|
|
28
|
+
// TESTS
|
|
29
|
+
|
|
30
|
+
test('index.js loads with empty PROTOCOL and TESTARO_WORKERS using fallbacks', () => {
|
|
31
|
+
// If index.js loaded successfully, requestHandler is a function.
|
|
32
|
+
assert.equal(typeof requestHandler, 'function');
|
|
33
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
indexServe.test.js
|
|
3
|
+
Tests for the serve function in index.js, which starts the HTTP or HTTPS
|
|
4
|
+
server. The serve function is exported for testing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ENVIRONMENT (must be set before requiring index.js)
|
|
8
|
+
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const fixtureDBDir = path.join(__dirname, 'test', 'fixtures', 'db');
|
|
11
|
+
|
|
12
|
+
process.env.DB_DIR = fixtureDBDir;
|
|
13
|
+
process.env.AUTH_CODE = 'test-auth-code';
|
|
14
|
+
process.env.TESTARO_WORKERS = '{}';
|
|
15
|
+
|
|
16
|
+
const {test} = require('node:test');
|
|
17
|
+
const assert = require('node:assert/strict');
|
|
18
|
+
const http = require('node:http');
|
|
19
|
+
const fs = require('node:fs/promises');
|
|
20
|
+
const {serve} = require('./index');
|
|
21
|
+
|
|
22
|
+
// HELPER
|
|
23
|
+
|
|
24
|
+
const closeServer = server => new Promise(resolve => {
|
|
25
|
+
if (!server) {
|
|
26
|
+
resolve();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// Close all idle connections, then close the server.
|
|
30
|
+
server.closeAllConnections?.();
|
|
31
|
+
const timer = setTimeout(() => {
|
|
32
|
+
server.closeAllConnections?.();
|
|
33
|
+
resolve();
|
|
34
|
+
}, 500);
|
|
35
|
+
server.close(() => {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
resolve();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// TESTS
|
|
42
|
+
|
|
43
|
+
test('serve creates missing directories and returns an HTTP server', async () => {
|
|
44
|
+
// Remove the queue directory to verify serve recreates it.
|
|
45
|
+
const queueDir = path.join(fixtureDBDir, 'jobs', 'queue');
|
|
46
|
+
await fs.rm(queueDir, {recursive: true}).catch(() => {});
|
|
47
|
+
process.env.PORT = '3987';
|
|
48
|
+
const server = await serve(http, {});
|
|
49
|
+
try {
|
|
50
|
+
assert.ok(server);
|
|
51
|
+
assert.equal(typeof server.listen, 'function');
|
|
52
|
+
// Verify the queue directory was recreated.
|
|
53
|
+
const exists = await fs.access(queueDir).then(() => true).catch(() => false);
|
|
54
|
+
assert.ok(exists, 'serve should create the queue directory');
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
await closeServer(server);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/*
|
|
2
|
+
indexWorkers.test.js
|
|
3
|
+
Tests for index.js when TESTARO_WORKERS is invalid JSON.
|
|
4
|
+
This must be a separate file because index.js reads TESTARO_WORKERS
|
|
5
|
+
at module load time.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ENVIRONMENT (must be set before requiring index.js)
|
|
9
|
+
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const fixtureDBDir = path.join(__dirname, 'test', 'fixtures', 'db');
|
|
12
|
+
|
|
13
|
+
process.env.DB_DIR = fixtureDBDir;
|
|
14
|
+
process.env.AUTH_CODE = 'test-auth-code';
|
|
15
|
+
process.env.TESTARO_WORKERS = 'not valid json';
|
|
16
|
+
|
|
17
|
+
// IMPORTS
|
|
18
|
+
|
|
19
|
+
const {test} = require('node:test');
|
|
20
|
+
const assert = require('node:assert/strict');
|
|
21
|
+
const http = require('node:http');
|
|
22
|
+
const {requestHandler} = require('./index');
|
|
23
|
+
|
|
24
|
+
// CONSTANTS
|
|
25
|
+
|
|
26
|
+
const port = 3993;
|
|
27
|
+
|
|
28
|
+
// HELPERS
|
|
29
|
+
|
|
30
|
+
const request = (method, requestPath, body = null, headers = {}) => new Promise((resolve, reject) => {
|
|
31
|
+
const options = {method, host: 'localhost', port, path: requestPath, headers: {...headers}};
|
|
32
|
+
let bodyData = '';
|
|
33
|
+
if (body) {
|
|
34
|
+
bodyData = JSON.stringify(body);
|
|
35
|
+
options.headers['content-type'] = 'application/json';
|
|
36
|
+
options.headers['content-length'] = Buffer.byteLength(bodyData);
|
|
37
|
+
}
|
|
38
|
+
const req = http.request(options, response => {
|
|
39
|
+
const chunks = [];
|
|
40
|
+
response.on('data', chunk => chunks.push(chunk));
|
|
41
|
+
response.on('end', () => {
|
|
42
|
+
resolve({statusCode: response.statusCode, headers: response.headers, body: Buffer.concat(chunks).toString()});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
req.on('error', reject);
|
|
46
|
+
req.end(bodyData || '');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// TESTS
|
|
50
|
+
|
|
51
|
+
test('index.js loads with invalid TESTARO_WORKERS and treats workers as empty', () => {
|
|
52
|
+
// If index.js loaded successfully, requestHandler is a function.
|
|
53
|
+
assert.equal(typeof requestHandler, 'function');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('POST /worker/job with invalid TESTARO_WORKERS returns 401 for any credentials', async () => {
|
|
57
|
+
const server = http.createServer(requestHandler);
|
|
58
|
+
await new Promise(resolve => server.listen(port, () => resolve()));
|
|
59
|
+
try {
|
|
60
|
+
const auth = Buffer.from('worker1:secret1').toString('base64');
|
|
61
|
+
const res = await request('POST', '/worker/job', {}, {
|
|
62
|
+
authorization: `Basic ${auth}`
|
|
63
|
+
});
|
|
64
|
+
assert.equal(res.statusCode, 401);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
server.closeAllConnections?.();
|
|
68
|
+
await new Promise(resolve => {
|
|
69
|
+
const timer = setTimeout(() => {
|
|
70
|
+
server.closeAllConnections?.();
|
|
71
|
+
resolve();
|
|
72
|
+
}, 1000);
|
|
73
|
+
server.close(() => {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
resolve();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|
package/mcp.js
CHANGED
|
@@ -44,7 +44,7 @@ exports.mcpPath = '/mcp';
|
|
|
44
44
|
// FUNCTIONS
|
|
45
45
|
|
|
46
46
|
// Creates and returns an McpServer with Kilotest tools registered.
|
|
47
|
-
|
|
47
|
+
exports.createMCPServer = () => {
|
|
48
48
|
const server = new McpServer({
|
|
49
49
|
name: 'Kilotest',
|
|
50
50
|
version,
|
|
@@ -210,7 +210,7 @@ const createMCPServer = () => {
|
|
|
210
210
|
// Handles an MCP request.
|
|
211
211
|
exports.handleMCP = async (request, response) => {
|
|
212
212
|
const transport = new StreamableHTTPServerTransport({sessionIdGenerator: undefined});
|
|
213
|
-
const server = createMCPServer();
|
|
213
|
+
const server = exports.createMCPServer();
|
|
214
214
|
await server.connect(transport);
|
|
215
215
|
await transport.handleRequest(request, response);
|
|
216
216
|
};
|
package/mcp.test.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/*
|
|
2
|
+
mcp.test.js
|
|
3
|
+
Tests for mcp.js, covering tool registration and handler behavior.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const {test} = require('node:test');
|
|
9
|
+
const assert = require('node:assert/strict');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const http = require('node:http');
|
|
12
|
+
const {mcpPath, createMCPServer} = require('./mcp');
|
|
13
|
+
|
|
14
|
+
// CONSTANTS
|
|
15
|
+
|
|
16
|
+
// Set DB_DIR to the fixture database for all tests.
|
|
17
|
+
process.env.DB_DIR = path.join(__dirname, 'test', 'fixtures', 'db');
|
|
18
|
+
|
|
19
|
+
// TESTS
|
|
20
|
+
|
|
21
|
+
test('mcpPath is /mcp', () => {
|
|
22
|
+
assert.equal(mcpPath, '/mcp');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('createMCPServer registers all 8 tools', () => {
|
|
26
|
+
const server = createMCPServer();
|
|
27
|
+
const toolNames = Object.keys(server._registeredTools);
|
|
28
|
+
assert.equal(toolNames.length, 8);
|
|
29
|
+
assert.deepEqual(toolNames, [
|
|
30
|
+
'listReports',
|
|
31
|
+
'listIssues',
|
|
32
|
+
'listViolators',
|
|
33
|
+
'listDiagnoses',
|
|
34
|
+
'getReport',
|
|
35
|
+
'requestTest',
|
|
36
|
+
'requestRetest',
|
|
37
|
+
'requestFeature'
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('each tool has a description and a handler function', () => {
|
|
42
|
+
const server = createMCPServer();
|
|
43
|
+
const tools = server._registeredTools;
|
|
44
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
45
|
+
assert.ok(tool.description, `${name} has a description`);
|
|
46
|
+
assert.equal(typeof tool.handler, 'function', `${name} has a handler function`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('listReports handler returns content and structuredContent', async () => {
|
|
51
|
+
const server = createMCPServer();
|
|
52
|
+
const result = await server._registeredTools.listReports.handler({});
|
|
53
|
+
assert.ok(result.content);
|
|
54
|
+
assert.equal(result.content[0].type, 'text');
|
|
55
|
+
assert.ok(result.structuredContent);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('listIssues handler returns content and structuredContent for a valid report', async () => {
|
|
59
|
+
const server = createMCPServer();
|
|
60
|
+
const result = await server._registeredTools.listIssues.handler({
|
|
61
|
+
timeStamp: '260101T0000',
|
|
62
|
+
jobID: 'mix'
|
|
63
|
+
});
|
|
64
|
+
assert.ok(result.content);
|
|
65
|
+
assert.equal(result.content[0].type, 'text');
|
|
66
|
+
assert.ok(result.structuredContent);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('listViolators handler returns content and structuredContent for a valid issue', async () => {
|
|
70
|
+
const server = createMCPServer();
|
|
71
|
+
const result = await server._registeredTools.listViolators.handler({
|
|
72
|
+
issueID: 'linkNoText',
|
|
73
|
+
timeStamp: '260101T0000',
|
|
74
|
+
jobID: 'mix'
|
|
75
|
+
});
|
|
76
|
+
assert.ok(result.content);
|
|
77
|
+
assert.equal(result.content[0].type, 'text');
|
|
78
|
+
assert.ok(result.structuredContent);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('listDiagnoses handler returns content and structuredContent for a valid diagnosis', async () => {
|
|
82
|
+
const server = createMCPServer();
|
|
83
|
+
const result = await server._registeredTools.listDiagnoses.handler({
|
|
84
|
+
catalogIndex: '0',
|
|
85
|
+
issueID: 'linkNoText',
|
|
86
|
+
timeStamp: '260101T0000',
|
|
87
|
+
jobID: 'mix'
|
|
88
|
+
});
|
|
89
|
+
assert.ok(result.content);
|
|
90
|
+
assert.equal(result.content[0].type, 'text');
|
|
91
|
+
assert.ok(result.structuredContent);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('getReport handler returns content and structuredContent for a valid report', async () => {
|
|
95
|
+
const server = createMCPServer();
|
|
96
|
+
const result = await server._registeredTools.getReport.handler({
|
|
97
|
+
timeStamp: '260101T0000',
|
|
98
|
+
jobID: 'mix'
|
|
99
|
+
});
|
|
100
|
+
assert.ok(result.content);
|
|
101
|
+
assert.equal(result.content[0].type, 'text');
|
|
102
|
+
assert.ok(result.structuredContent);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('requestTest handler returns content and structuredContent', async () => {
|
|
106
|
+
const server = createMCPServer();
|
|
107
|
+
const result = await server._registeredTools.requestTest.handler({
|
|
108
|
+
description: 'Test Page',
|
|
109
|
+
URL: 'https://example.com/test',
|
|
110
|
+
reason: 'Because accessibility matters'
|
|
111
|
+
});
|
|
112
|
+
assert.ok(result.content);
|
|
113
|
+
assert.equal(result.content[0].type, 'text');
|
|
114
|
+
assert.ok(result.structuredContent);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('requestRetest handler returns content and structuredContent for a valid report', async () => {
|
|
118
|
+
const server = createMCPServer();
|
|
119
|
+
const result = await server._registeredTools.requestRetest.handler({
|
|
120
|
+
timeStamp: '260101T0000',
|
|
121
|
+
jobID: 'mix',
|
|
122
|
+
reason: 'Because the report is obsolete'
|
|
123
|
+
});
|
|
124
|
+
assert.ok(result.content);
|
|
125
|
+
assert.equal(result.content[0].type, 'text');
|
|
126
|
+
assert.ok(result.structuredContent);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('requestFeature handler returns content and structuredContent', async () => {
|
|
130
|
+
const server = createMCPServer();
|
|
131
|
+
const result = await server._registeredTools.requestFeature.handler({
|
|
132
|
+
feature: 'A new feature idea'
|
|
133
|
+
});
|
|
134
|
+
assert.ok(result.content);
|
|
135
|
+
assert.equal(result.content[0].type, 'text');
|
|
136
|
+
assert.ok(result.structuredContent);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('listIssues handler returns an error for a nonexistent report', async () => {
|
|
140
|
+
const server = createMCPServer();
|
|
141
|
+
const result = await server._registeredTools.listIssues.handler({
|
|
142
|
+
timeStamp: '990101T0000',
|
|
143
|
+
jobID: 'xxx'
|
|
144
|
+
});
|
|
145
|
+
const reportBasics = result.structuredContent['response content']['basics about the report'];
|
|
146
|
+
assert.ok(reportBasics.error);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('listViolators handler returns an error for an unknown issue', async () => {
|
|
150
|
+
const server = createMCPServer();
|
|
151
|
+
const result = await server._registeredTools.listViolators.handler({
|
|
152
|
+
issueID: 'nonexistentIssue',
|
|
153
|
+
timeStamp: '260101T0000',
|
|
154
|
+
jobID: 'mix'
|
|
155
|
+
});
|
|
156
|
+
const issueBasics = result.structuredContent['response content']['basics about the issue'];
|
|
157
|
+
assert.ok(issueBasics.error);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// INTEGRATION TESTS FOR handleMCP
|
|
161
|
+
|
|
162
|
+
// Helper: sends a single MCP JSON-RPC request to a local server and returns the parsed SSE response.
|
|
163
|
+
const sendMCPRequest = (port, method, params, id) => new Promise((resolve, reject) => {
|
|
164
|
+
const body = JSON.stringify({jsonrpc: '2.0', method, params, id});
|
|
165
|
+
const req = http.request({
|
|
166
|
+
port,
|
|
167
|
+
method: 'POST',
|
|
168
|
+
path: '/mcp',
|
|
169
|
+
headers: {
|
|
170
|
+
'content-type': 'application/json',
|
|
171
|
+
'accept': 'application/json, text/event-stream',
|
|
172
|
+
'content-length': Buffer.byteLength(body)
|
|
173
|
+
}
|
|
174
|
+
}, res => {
|
|
175
|
+
let data = '';
|
|
176
|
+
res.on('data', chunk => {
|
|
177
|
+
data += chunk;
|
|
178
|
+
});
|
|
179
|
+
res.on('end', () => {
|
|
180
|
+
resolve({statusCode: res.statusCode, headers: res.headers, body: data});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
req.on('error', reject);
|
|
184
|
+
req.write(body);
|
|
185
|
+
req.end();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Helper: parses the JSON-RPC result from an SSE response body.
|
|
189
|
+
const parseSSEResult = body => {
|
|
190
|
+
const jsonLine = body.split('\n').find(line => line.startsWith('data: '));
|
|
191
|
+
if (!jsonLine) {
|
|
192
|
+
throw new Error('No data line in SSE response');
|
|
193
|
+
}
|
|
194
|
+
return JSON.parse(jsonLine.slice(6));
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Helper: starts a local HTTP server with handleMCP and returns it.
|
|
198
|
+
const startMCPServer = () => new Promise(resolve => {
|
|
199
|
+
const {handleMCP} = require('./mcp');
|
|
200
|
+
const server = http.createServer((req, res) => handleMCP(req, res));
|
|
201
|
+
server.listen(0, () => resolve(server));
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Helper: closes a server with a timeout fallback.
|
|
205
|
+
const closeMCPServer = server => new Promise(resolve => {
|
|
206
|
+
if (!server) {
|
|
207
|
+
resolve();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
server.closeAllConnections?.();
|
|
211
|
+
const timer = setTimeout(() => {
|
|
212
|
+
server.closeAllConnections?.();
|
|
213
|
+
resolve();
|
|
214
|
+
}, 1000);
|
|
215
|
+
server.close(() => {
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
resolve();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('handleMCP responds to initialize with server info', async () => {
|
|
222
|
+
const server = await startMCPServer();
|
|
223
|
+
try {
|
|
224
|
+
const port = server.address().port;
|
|
225
|
+
const res = await sendMCPRequest(port, 'initialize', {
|
|
226
|
+
protocolVersion: '2025-06-18',
|
|
227
|
+
capabilities: {},
|
|
228
|
+
clientInfo: {name: 'test', version: '1.0'}
|
|
229
|
+
}, 1);
|
|
230
|
+
assert.equal(res.statusCode, 200);
|
|
231
|
+
const result = parseSSEResult(res.body);
|
|
232
|
+
assert.equal(result.result.serverInfo.name, 'Kilotest');
|
|
233
|
+
assert.equal(result.result.protocolVersion, '2025-06-18');
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
await closeMCPServer(server);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('handleMCP lists all 8 tools via tools/list', async () => {
|
|
241
|
+
const server = await startMCPServer();
|
|
242
|
+
try {
|
|
243
|
+
const port = server.address().port;
|
|
244
|
+
const res = await sendMCPRequest(port, 'tools/list', {}, 2);
|
|
245
|
+
assert.equal(res.statusCode, 200);
|
|
246
|
+
const result = parseSSEResult(res.body);
|
|
247
|
+
const toolNames = result.result.tools.map(t => t.name);
|
|
248
|
+
assert.equal(toolNames.length, 8);
|
|
249
|
+
assert.deepEqual(toolNames, [
|
|
250
|
+
'listReports',
|
|
251
|
+
'listIssues',
|
|
252
|
+
'listViolators',
|
|
253
|
+
'listDiagnoses',
|
|
254
|
+
'getReport',
|
|
255
|
+
'requestTest',
|
|
256
|
+
'requestRetest',
|
|
257
|
+
'requestFeature'
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
await closeMCPServer(server);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('handleMCP executes listReports tool via tools/call', async () => {
|
|
266
|
+
const server = await startMCPServer();
|
|
267
|
+
try {
|
|
268
|
+
const port = server.address().port;
|
|
269
|
+
const res = await sendMCPRequest(port, 'tools/call', {
|
|
270
|
+
name: 'listReports',
|
|
271
|
+
arguments: {}
|
|
272
|
+
}, 3);
|
|
273
|
+
assert.equal(res.statusCode, 200);
|
|
274
|
+
const result = parseSSEResult(res.body);
|
|
275
|
+
assert.ok(result.result.content);
|
|
276
|
+
assert.equal(result.result.content[0].type, 'text');
|
|
277
|
+
}
|
|
278
|
+
finally {
|
|
279
|
+
await closeMCPServer(server);
|
|
280
|
+
}
|
|
281
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jrpool/kilotest",
|
|
3
|
-
"version": "46.0.
|
|
3
|
+
"version": "46.0.11",
|
|
4
4
|
"description": "Ensemble testing for front-end web quality",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"engines": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node index",
|
|
11
11
|
"lint": "npx eslint && npx markdownlint-cli2",
|
|
12
|
-
"test": "c8 node --test",
|
|
12
|
+
"test": "c8 node --require ./test/setup.cjs --test --test-concurrency=1 --test-timeout=70",
|
|
13
13
|
"test:smoke": "node smokeTest",
|
|
14
14
|
"generate:openapi": "node scripts/generate-openapi"
|
|
15
15
|
},
|
|
@@ -66,7 +66,8 @@
|
|
|
66
66
|
"mcp.js",
|
|
67
67
|
"alerts.js",
|
|
68
68
|
"util.js",
|
|
69
|
-
"api/**/*.js"
|
|
69
|
+
"api/**/*.js",
|
|
70
|
+
"web/**/*.js"
|
|
70
71
|
],
|
|
71
72
|
"exclude": [
|
|
72
73
|
"coverage/**",
|
|
@@ -74,8 +75,12 @@
|
|
|
74
75
|
"test{,-*}.{js,cjs,mjs}",
|
|
75
76
|
"**/*{.,-}test.{js,cjs,mjs}",
|
|
76
77
|
"**/__tests__/**",
|
|
77
|
-
"web/**",
|
|
78
78
|
"scripts/**"
|
|
79
|
-
]
|
|
79
|
+
],
|
|
80
|
+
"check-coverage": true,
|
|
81
|
+
"lines": 100,
|
|
82
|
+
"statements": 100,
|
|
83
|
+
"functions": 100,
|
|
84
|
+
"branches": 100
|
|
80
85
|
}
|
|
81
86
|
}
|