@jrpool/kilotest 46.1.2 → 48.0.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/.claude/settings.local.json +13 -1
- package/.devin/config.local.json +5 -1
- package/.github/workflows/ci.yml +0 -3
- package/.github/workflows/periodic-smoke-tests.yml +30 -0
- package/.github/workflows/publish-mcp.yml +4 -4
- package/{alerts.test.js → alerts.test.ts} +17 -29
- package/{alerts.js → alerts.ts} +10 -13
- package/api/{getReport.test.js → getReport.test.ts} +9 -10
- package/api/{getReport.js → getReport.ts} +23 -24
- package/api/{listDiagnoses.test.js → listDiagnoses.test.ts} +26 -27
- package/api/{listDiagnoses.js → listDiagnoses.ts} +46 -70
- package/api/{listIssues.test.js → listIssues.test.ts} +21 -14
- package/api/{listIssues.js → listIssues.ts} +75 -74
- package/api/{listReports.test.js → listReports.test.ts} +17 -16
- package/api/listReports.ts +87 -0
- package/api/{listViolators.test.js → listViolators.test.ts} +19 -20
- package/api/{listViolators.js → listViolators.ts} +45 -70
- package/api/requestFeature.test.ts +54 -0
- package/api/{requestFeature.js → requestFeature.ts} +18 -15
- package/api/requestRetest.test.ts +144 -0
- package/api/requestRetest.ts +119 -0
- package/api/requestTest.test.ts +153 -0
- package/api/requestTest.ts +123 -0
- package/api/{routes.test.js → routes.test.ts} +5 -5
- package/api/{routes.js → routes.ts} +6 -6
- package/api/{schemas.js → schemas.ts} +69 -50
- package/api/{util.test.js → util.test.ts} +35 -14
- package/api/{util.js → util.ts} +54 -51
- package/api/version.ts +8 -0
- package/{apiIntegration.test.js → apiIntegration.test.ts} +12 -13
- package/balances.ts +87 -0
- package/{checkinfo.js → checkinfo.ts} +24 -20
- package/docs/AI-TOOLS.md +4 -4
- package/docs/BACKLOG.md +360 -0
- package/docs/SERVICE.md +170 -17
- package/docs/decisions/002-coverage-tool.md +40 -0
- package/docs/decisions/003-adr-threshold.md +34 -0
- package/docs/decisions/004-docs-directory.md +23 -0
- package/docs/decisions/005-observability.md +28 -0
- package/docs/decisions/006-git-workflow.md +28 -0
- package/docs/decisions/007-internal-testing.md +34 -0
- package/docs/decisions/008-dependencies.md +28 -0
- package/docs/decisions/009-qai-integration.md +40 -0
- package/docs/decisions/QAI-HISTORICAL-CONTEXT.md +42 -0
- package/eslint.config.mjs +27 -0
- package/index.html +3 -3
- package/{index.test.js → index.test.ts} +397 -152
- package/{index.js → index.ts} +386 -285
- package/{indexBalances.test.js → indexBalances.test.ts} +21 -17
- package/{indexServe.test.js → indexServe.test.ts} +9 -9
- package/linkValidation.test.ts +190 -0
- package/{mcp.test.js → mcp.test.ts} +53 -26
- package/{mcp.js → mcp.ts} +29 -20
- package/memory/MEMORY.md +7 -0
- package/memory/user_maintainer_context.md +24 -0
- package/openapi.yaml +115 -68
- package/package.json +35 -15
- package/pm2.config.cjs +29 -0
- package/{routes.test.js → routes.test.ts} +8 -7
- package/{routing.test.js → routing.test.ts} +11 -12
- package/scripts/{generate-openapi.js → generate-openapi.ts} +13 -13
- package/serve.ts +15 -0
- package/smokeTest.ts +234 -0
- package/test/dbFixture.ts +21 -0
- package/test/fixtures/{buildFixtures.js → buildFixtures.ts} +14 -13
- package/test/{setup.cjs → setup.ts} +2 -2
- package/tsconfig.json +16 -0
- package/util.test.ts +1129 -0
- package/util.ts +1017 -0
- package/web/ai0BalanceForm/{ai0BalanceForm.test.js → ai0BalanceForm.test.ts} +15 -15
- package/web/ai0BalanceForm/{index.js → index.ts} +10 -18
- package/web/enqueue/index.ts +56 -0
- package/web/enqueueForm/index.html +9 -9
- package/web/enqueueForm/index.ts +43 -0
- package/web/expungeReportsForm/{expungeReportsForm.test.js → expungeReportsForm.test.ts} +18 -20
- package/web/expungeReportsForm/index.ts +18 -0
- package/web/hideReportForm/{hideReportForm.test.js → hideReportForm.test.ts} +14 -14
- package/web/hideReportForm/{index.js → index.ts} +17 -21
- package/web/listDiagnoses/{index.js → index.ts} +48 -58
- package/web/listDiagnoses/{listDiagnoses.test.js → listDiagnoses.test.ts} +39 -34
- package/web/listIssues/{index.js → index.ts} +67 -92
- package/web/listIssues/{listIssues.test.js → listIssues.test.ts} +45 -51
- package/web/listReports/index.html +3 -3
- package/web/listReports/{index.js → index.ts} +74 -58
- package/web/listReports/{listReports.test.js → listReports.test.ts} +41 -43
- package/web/listRules/{index.js → index.ts} +18 -23
- package/web/listRules/{listRules.test.js → listRules.test.ts} +26 -19
- package/web/listTopIssues/{index.js → index.ts} +36 -47
- package/web/listTopIssues/{listTopIssues.test.js → listTopIssues.test.ts} +26 -17
- package/web/listViolators/{index.js → index.ts} +51 -71
- package/web/listViolators/{listViolators.test.js → listViolators.test.ts} +43 -36
- package/web/manage/index.ts +22 -0
- package/web/pruneReportsForm/index.ts +18 -0
- package/web/pruneReportsForm/{pruneReportsForm.test.js → pruneReportsForm.test.ts} +11 -11
- package/web/reannotate/index.ts +71 -0
- package/web/reannotate/{reannotate.test.js → reannotate.test.ts} +19 -22
- package/web/reannotateForm/{index.js → index.ts} +41 -50
- package/web/reannotateForm/{reannotateForm.test.js → reannotateForm.test.ts} +13 -16
- package/web/renewWCAG/{index.js → index.ts} +9 -9
- package/web/renewWCAG/{renewWCAG.test.js → renewWCAG.test.ts} +17 -20
- package/web/renewWCAGForm/index.ts +22 -0
- package/web/{expungeReportsForm/index.js → reportDeletion.ts} +57 -39
- package/web/requestRetest/index.html +7 -7
- package/web/requestRetest/index.ts +62 -0
- package/web/requestRetest/requestRetest.test.ts +130 -0
- package/web/requestRetestForm/{index.js → index.ts} +10 -16
- package/web/requestRetestForm/{requestRetestForm.test.js → requestRetestForm.test.ts} +11 -12
- package/web/requestTest/index.html +7 -7
- package/web/requestTest/index.ts +52 -0
- package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +39 -24
- package/web/requestTestForm/index.html +4 -4
- package/web/requestTestForm/index.ts +21 -0
- package/web/requestTestForm/{requestTestForm.test.js → requestTestForm.test.ts} +9 -9
- package/web/rewindReportsForm/index.ts +18 -0
- package/web/rewindReportsForm/{rewindReportsForm.test.js → rewindReportsForm.test.ts} +11 -11
- package/web/tutorialAI/index.html +184 -0
- package/web/tutorialAI/index.ts +80 -0
- package/web/tutorialAI/tutorialAI.test.ts +192 -0
- package/web/{tutorial → tutorialWeb}/index.html +9 -8
- package/web/tutorialWeb/index.ts +78 -0
- package/web/tutorialWeb/tutorialWeb.test.ts +195 -0
- package/web/unhideReportForm/{index.js → index.ts} +18 -22
- package/web/unhideReportForm/{unhideReportForm.test.js → unhideReportForm.test.ts} +16 -16
- package/aceconfig.js +0 -28
- package/api/listReports.js +0 -89
- package/api/requestFeature.test.js +0 -58
- package/api/requestRetest.js +0 -96
- package/api/requestRetest.test.js +0 -80
- package/api/requestTest.js +0 -106
- package/api/requestTest.test.js +0 -92
- package/api/version.js +0 -8
- package/indexFallback.test.js +0 -33
- package/indexWorkers.test.js +0 -79
- package/jsconfig.json +0 -8
- package/pm2.config.js +0 -18
- package/smokeTest.js +0 -128
- package/util.js +0 -744
- package/util.test.js +0 -968
- package/web/enqueue/index.js +0 -67
- package/web/enqueueForm/index.js +0 -49
- package/web/manage/index.js +0 -22
- package/web/pruneReportsForm/index.js +0 -123
- package/web/reannotate/index.js +0 -58
- package/web/renewWCAGForm/index.js +0 -22
- package/web/requestRetest/index.js +0 -31
- package/web/requestRetest/requestRetest.test.js +0 -119
- package/web/requestTest/index.js +0 -24
- package/web/requestTestForm/index.js +0 -21
- package/web/rewindReportsForm/index.js +0 -121
- package/web/tutorial/index.js +0 -63
- package/web/tutorial/tutorial.test.js +0 -97
- /package/test/fixtures/db/jobs/{recs.json → testRequests.json} +0 -0
- /package/web/{tutorial → tutorialWeb}/images/newsletter-form.png +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/*
|
|
2
|
-
indexBalances.test.
|
|
2
|
+
indexBalances.test.ts
|
|
3
3
|
Tests for checkBalancesForAlerts in index.js, which requires balance-related
|
|
4
4
|
env vars to be set before index.js is loaded.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
// ENVIRONMENT (must be set before requiring index.js)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import {fixtureDBDir} from './test/dbFixture.ts';
|
|
11
11
|
|
|
12
12
|
process.env.DB_DIR = fixtureDBDir;
|
|
13
13
|
process.env.AUTH_CODE = 'test-auth-code';
|
|
@@ -19,16 +19,16 @@ process.env.AI_SERVICE0_BALANCE_THRESHOLD = '50';
|
|
|
19
19
|
process.env.AI_MODEL0_INPUT_PRICE = '0.001';
|
|
20
20
|
process.env.AI_MODEL0_OUTPUT_PRICE = '0.002';
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
import {test, before, after} from 'node:test';
|
|
23
|
+
import assert from 'node:assert/strict';
|
|
24
|
+
import http from 'node:http';
|
|
25
|
+
import fs from 'node:fs/promises';
|
|
26
|
+
import {requestHandler} from './index.ts';
|
|
27
27
|
|
|
28
28
|
// CONSTANTS
|
|
29
29
|
|
|
30
30
|
const port = 3994;
|
|
31
|
-
const balancePath = path.join(
|
|
31
|
+
const balancePath = path.join(import.meta.dirname, 'ai0Balance.json');
|
|
32
32
|
const jobID = '990101T0001-bal';
|
|
33
33
|
const reportPath = path.join(fixtureDBDir, 'reports', `${jobID}.json`);
|
|
34
34
|
const claimedDir = path.join(fixtureDBDir, 'jobs', 'claimed');
|
|
@@ -36,18 +36,22 @@ const jobPath = path.join(claimedDir, `${jobID}.json`);
|
|
|
36
36
|
|
|
37
37
|
// SETUP AND TEARDOWN
|
|
38
38
|
|
|
39
|
-
let server;
|
|
40
|
-
let originalBalance;
|
|
39
|
+
let server: http.Server;
|
|
40
|
+
let originalBalance: any;
|
|
41
41
|
|
|
42
42
|
before(async () => {
|
|
43
43
|
originalBalance = await fs.readFile(balancePath, 'utf8').catch(() => null);
|
|
44
|
+
// Create the job directories, which are not tracked by git and may not exist on a fresh checkout.
|
|
45
|
+
for (const sub of ['claimed', 'queue', 'failed']) {
|
|
46
|
+
await fs.mkdir(path.join(fixtureDBDir, 'jobs', sub), {recursive: true});
|
|
47
|
+
}
|
|
44
48
|
server = http.createServer(requestHandler);
|
|
45
|
-
await new Promise(resolve => server.listen(port, () => resolve()));
|
|
49
|
+
await new Promise<void>(resolve => server.listen(port, () => resolve()));
|
|
46
50
|
});
|
|
47
51
|
|
|
48
52
|
after(async () => {
|
|
49
53
|
server.closeAllConnections?.();
|
|
50
|
-
await new Promise(resolve => {
|
|
54
|
+
await new Promise<void>(resolve => {
|
|
51
55
|
const timer = setTimeout(() => {
|
|
52
56
|
server.closeAllConnections?.();
|
|
53
57
|
resolve();
|
|
@@ -70,7 +74,7 @@ after(async () => {
|
|
|
70
74
|
|
|
71
75
|
// HELPERS
|
|
72
76
|
|
|
73
|
-
const request = (method, requestPath, body = null, headers = {}) => new Promise((resolve, reject) => {
|
|
77
|
+
const request = (method: string, requestPath: string, body: any = null, headers: any = {}): Promise<any> => new Promise((resolve, reject) => {
|
|
74
78
|
const options = {method, host: 'localhost', port, path: requestPath, headers: {...headers}};
|
|
75
79
|
let bodyData = '';
|
|
76
80
|
if (body) {
|
|
@@ -79,7 +83,7 @@ const request = (method, requestPath, body = null, headers = {}) => new Promise(
|
|
|
79
83
|
options.headers['content-length'] = Buffer.byteLength(bodyData);
|
|
80
84
|
}
|
|
81
85
|
const req = http.request(options, response => {
|
|
82
|
-
const chunks = [];
|
|
86
|
+
const chunks: Buffer[] = [];
|
|
83
87
|
response.on('data', chunk => chunks.push(chunk));
|
|
84
88
|
response.on('end', () => {
|
|
85
89
|
resolve({statusCode: response.statusCode, headers: response.headers, body: Buffer.concat(chunks).toString()});
|
|
@@ -89,7 +93,7 @@ const request = (method, requestPath, body = null, headers = {}) => new Promise(
|
|
|
89
93
|
req.end(bodyData || '');
|
|
90
94
|
});
|
|
91
95
|
|
|
92
|
-
const jsonBody = res => {
|
|
96
|
+
const jsonBody = (res: any) => {
|
|
93
97
|
try {
|
|
94
98
|
return JSON.parse(res.body);
|
|
95
99
|
}
|
|
@@ -98,7 +102,7 @@ const jsonBody = res => {
|
|
|
98
102
|
}
|
|
99
103
|
};
|
|
100
104
|
|
|
101
|
-
const submitReport = async (acts, balanceFileContent) => {
|
|
105
|
+
const submitReport = async (acts: any, balanceFileContent: any) => {
|
|
102
106
|
if (balanceFileContent !== undefined) {
|
|
103
107
|
if (balanceFileContent === null) {
|
|
104
108
|
await fs.unlink(balancePath).catch(() => {});
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
/*
|
|
2
|
-
indexServe.test.
|
|
2
|
+
indexServe.test.ts
|
|
3
3
|
Tests for the serve function in index.js, which starts the HTTP or HTTPS
|
|
4
4
|
server. The serve function is exported for testing.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
// ENVIRONMENT (must be set before requiring index.js)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import {fixtureDBDir} from './test/dbFixture.ts';
|
|
11
11
|
|
|
12
12
|
process.env.DB_DIR = fixtureDBDir;
|
|
13
13
|
process.env.AUTH_CODE = 'test-auth-code';
|
|
14
14
|
process.env.TESTARO_WORKERS = '{}';
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
import {test} from 'node:test';
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import http from 'node:http';
|
|
19
|
+
import fs from 'node:fs/promises';
|
|
20
|
+
import {serve} from './index.ts';
|
|
21
21
|
|
|
22
22
|
// HELPER
|
|
23
23
|
|
|
24
|
-
const closeServer = server => new Promise(resolve => {
|
|
24
|
+
const closeServer = (server: any) => new Promise<void>(resolve => {
|
|
25
25
|
if (!server) {
|
|
26
26
|
resolve();
|
|
27
27
|
return;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Link validation test: ensures all internal href attributes point to valid paths
|
|
2
|
+
|
|
3
|
+
import {test} from 'node:test';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
const projectRoot = import.meta.dirname;
|
|
9
|
+
|
|
10
|
+
// Collect all valid paths (both static and route patterns)
|
|
11
|
+
const validPathPatterns = new Set<string>();
|
|
12
|
+
const validPathsExact = new Set<string>();
|
|
13
|
+
|
|
14
|
+
// Add exact paths and static files
|
|
15
|
+
const staticFiles = [
|
|
16
|
+
'/index.html',
|
|
17
|
+
'/style.css',
|
|
18
|
+
'/favicon.ico',
|
|
19
|
+
'/openapi.yaml',
|
|
20
|
+
'/openapi.json',
|
|
21
|
+
'/capability.md',
|
|
22
|
+
'/robots.txt',
|
|
23
|
+
'/sitemap.xml',
|
|
24
|
+
'/llms.txt',
|
|
25
|
+
'/llms-full.txt',
|
|
26
|
+
'/mcp'
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
staticFiles.forEach(p => validPathsExact.add(p));
|
|
30
|
+
|
|
31
|
+
// Add pattern-based routes
|
|
32
|
+
validPathPatterns.add('*.html*');
|
|
33
|
+
validPathPatterns.add('/api/*');
|
|
34
|
+
validPathPatterns.add('/fullReport.json/*');
|
|
35
|
+
validPathPatterns.add('/tutorialWeb/images/*');
|
|
36
|
+
validPathPatterns.add('/tutorialAI/images/*');
|
|
37
|
+
|
|
38
|
+
// Specific page routes (those that end in .html)
|
|
39
|
+
const htmlRoutes = [
|
|
40
|
+
'/tutorialWeb.html',
|
|
41
|
+
'/tutorialAI.html',
|
|
42
|
+
'/tutorialWebComment.html',
|
|
43
|
+
'/tutorialAIComment.html',
|
|
44
|
+
'/listReports.html',
|
|
45
|
+
'/listTopIssues.html',
|
|
46
|
+
'/listIssues.html',
|
|
47
|
+
'/listDiagnoses.html',
|
|
48
|
+
'/listViolators.html',
|
|
49
|
+
'/listRules.html',
|
|
50
|
+
'/manage.html',
|
|
51
|
+
'/requestTest.html',
|
|
52
|
+
'/requestTestForm.html',
|
|
53
|
+
'/requestRetest.html',
|
|
54
|
+
'/requestRetestForm.html',
|
|
55
|
+
'/reannotate.html',
|
|
56
|
+
'/reannotateForm.html',
|
|
57
|
+
'/hideReportForm.html',
|
|
58
|
+
'/unhideReportForm.html',
|
|
59
|
+
'/expungeReportsForm.html',
|
|
60
|
+
'/pruneReportsForm.html',
|
|
61
|
+
'/rewindReportsForm.html',
|
|
62
|
+
'/renewWCAG.html',
|
|
63
|
+
'/renewWCAGForm.html',
|
|
64
|
+
'/enqueue.html',
|
|
65
|
+
'/enqueueForm.html',
|
|
66
|
+
'/manage.html'
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
htmlRoutes.forEach(p => validPathsExact.add(p));
|
|
70
|
+
|
|
71
|
+
// Add root path
|
|
72
|
+
validPathsExact.add('/');
|
|
73
|
+
|
|
74
|
+
const isValidPath = (href: string): boolean => {
|
|
75
|
+
// Absolute URLs and email links are not internal paths
|
|
76
|
+
if (href.includes('://') || href.startsWith('mailto:') || href.startsWith('#')) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Exact match
|
|
81
|
+
if (validPathsExact.has(href)) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Pattern match
|
|
86
|
+
for (const pattern of validPathPatterns) {
|
|
87
|
+
const regexPattern = '^' + pattern.replace(/\*/g, '.*') + '$';
|
|
88
|
+
if (new RegExp(regexPattern).test(href)) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return false;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Find all HTML files in the project (excluding generated/build directories)
|
|
97
|
+
const findHtmlFiles = (dir: string): string[] => {
|
|
98
|
+
const files: string[] = [];
|
|
99
|
+
const entries = fs.readdirSync(dir, {withFileTypes: true});
|
|
100
|
+
const ignoredDirs = new Set(['node_modules', 'coverage', '.git', '.github', 'dist', 'build']);
|
|
101
|
+
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
const fullPath = path.join(dir, entry.name);
|
|
104
|
+
if (entry.name.startsWith('.') || ignoredDirs.has(entry.name)) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (entry.isDirectory()) {
|
|
108
|
+
files.push(...findHtmlFiles(fullPath));
|
|
109
|
+
} else if (entry.name.endsWith('.html')) {
|
|
110
|
+
files.push(fullPath);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return files;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const htmlFiles = findHtmlFiles(projectRoot);
|
|
118
|
+
|
|
119
|
+
test('All internal links in HTML files point to valid paths', () => {
|
|
120
|
+
const brokenLinks: Array<{file: string; href: string; type: string}> = [];
|
|
121
|
+
|
|
122
|
+
for (const htmlFile of htmlFiles) {
|
|
123
|
+
const content = fs.readFileSync(htmlFile, 'utf-8');
|
|
124
|
+
const relativePath = path.relative(projectRoot, htmlFile);
|
|
125
|
+
|
|
126
|
+
// Find all href attributes
|
|
127
|
+
const hrefRegex = /href=["']([^"']+)["']/g;
|
|
128
|
+
let match: RegExpExecArray | null;
|
|
129
|
+
while ((match = hrefRegex.exec(content)) !== null) {
|
|
130
|
+
const href = match[1];
|
|
131
|
+
if (href && !isValidPath(href)) {
|
|
132
|
+
brokenLinks.push({
|
|
133
|
+
file: relativePath,
|
|
134
|
+
href,
|
|
135
|
+
type: 'href'
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Find all src attributes (images, scripts, etc.)
|
|
141
|
+
const srcRegex = /src=["']([^"']+)["']/g;
|
|
142
|
+
while ((match = srcRegex.exec(content)) !== null) {
|
|
143
|
+
const src = match[1];
|
|
144
|
+
if (src && !src.includes('://') && !isValidPath(src)) {
|
|
145
|
+
brokenLinks.push({
|
|
146
|
+
file: relativePath,
|
|
147
|
+
href: src,
|
|
148
|
+
type: 'src'
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Find all fetch paths (JavaScript: fetch('/path', ...))
|
|
154
|
+
const fetchRegex = /fetch\s*\(\s*["']([^"']+)["']/g;
|
|
155
|
+
while ((match = fetchRegex.exec(content)) !== null) {
|
|
156
|
+
const fetchPath = match[1];
|
|
157
|
+
if (fetchPath && !fetchPath.includes('://') && !isValidPath(fetchPath)) {
|
|
158
|
+
brokenLinks.push({
|
|
159
|
+
file: relativePath,
|
|
160
|
+
href: fetchPath,
|
|
161
|
+
type: 'fetch'
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (brokenLinks.length > 0) {
|
|
168
|
+
const message = brokenLinks
|
|
169
|
+
.map(link => `${link.file}: ${link.type}="${link.href}" (invalid path)`)
|
|
170
|
+
.join('\n');
|
|
171
|
+
assert.fail(`Found ${brokenLinks.length} broken links:\n${message}`);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('Internal links in index.html resolve to expected paths', () => {
|
|
176
|
+
const expectedLinks = [
|
|
177
|
+
'/tutorialWeb.html',
|
|
178
|
+
'/tutorialAI.html',
|
|
179
|
+
'/listReports.html',
|
|
180
|
+
'/listTopIssues.html',
|
|
181
|
+
'/manage.html',
|
|
182
|
+
'/mcp',
|
|
183
|
+
'/capability.md',
|
|
184
|
+
'/openapi.yaml'
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
for (const expectedHref of expectedLinks) {
|
|
188
|
+
assert.ok(isValidPath(expectedHref), `Expected path ${expectedHref} should be valid`);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
/*
|
|
2
|
-
mcp.test.
|
|
2
|
+
mcp.test.ts
|
|
3
3
|
Tests for mcp.js, covering tool registration and handler behavior.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const {mcpPath, createMCPServer} = require('./mcp');
|
|
8
|
+
import {test} from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import http from 'node:http';
|
|
11
|
+
import {mcpPath, createMCPServer, handleMCP} from './mcp.ts';
|
|
13
12
|
|
|
14
13
|
// CONSTANTS
|
|
15
14
|
|
|
16
15
|
// Set DB_DIR to the fixture database for all tests.
|
|
17
|
-
process.env.DB_DIR =
|
|
16
|
+
process.env.DB_DIR = (await import('./test/dbFixture.ts')).fixtureDBDir;
|
|
18
17
|
|
|
19
18
|
// TESTS
|
|
20
19
|
|
|
@@ -24,7 +23,7 @@ test('mcpPath is /mcp', () => {
|
|
|
24
23
|
|
|
25
24
|
test('createMCPServer registers all 8 tools', () => {
|
|
26
25
|
const server = createMCPServer();
|
|
27
|
-
const toolNames = Object.keys(server._registeredTools);
|
|
26
|
+
const toolNames = Object.keys((server as any)._registeredTools);
|
|
28
27
|
assert.equal(toolNames.length, 8);
|
|
29
28
|
assert.deepEqual(toolNames, [
|
|
30
29
|
'listReports',
|
|
@@ -40,8 +39,8 @@ test('createMCPServer registers all 8 tools', () => {
|
|
|
40
39
|
|
|
41
40
|
test('each tool has a description and a handler function', () => {
|
|
42
41
|
const server = createMCPServer();
|
|
43
|
-
const tools = server._registeredTools;
|
|
44
|
-
for (const [name, tool] of Object.entries(tools)) {
|
|
42
|
+
const tools = (server as any)._registeredTools;
|
|
43
|
+
for (const [name, tool] of Object.entries(tools) as [string, any][]) {
|
|
45
44
|
assert.ok(tool.description, `${name} has a description`);
|
|
46
45
|
assert.equal(typeof tool.handler, 'function', `${name} has a handler function`);
|
|
47
46
|
}
|
|
@@ -49,7 +48,7 @@ test('each tool has a description and a handler function', () => {
|
|
|
49
48
|
|
|
50
49
|
test('listReports handler returns content and structuredContent', async () => {
|
|
51
50
|
const server = createMCPServer();
|
|
52
|
-
const result = await server._registeredTools.listReports.handler({});
|
|
51
|
+
const result = await (server as any)._registeredTools.listReports.handler({});
|
|
53
52
|
assert.ok(result.content);
|
|
54
53
|
assert.equal(result.content[0].type, 'text');
|
|
55
54
|
assert.ok(result.structuredContent);
|
|
@@ -57,7 +56,7 @@ test('listReports handler returns content and structuredContent', async () => {
|
|
|
57
56
|
|
|
58
57
|
test('listIssues handler returns content and structuredContent for a valid report', async () => {
|
|
59
58
|
const server = createMCPServer();
|
|
60
|
-
const result = await server._registeredTools.listIssues.handler({
|
|
59
|
+
const result = await (server as any)._registeredTools.listIssues.handler({
|
|
61
60
|
timeStamp: '260101T0000',
|
|
62
61
|
jobID: 'mix'
|
|
63
62
|
});
|
|
@@ -68,7 +67,7 @@ test('listIssues handler returns content and structuredContent for a valid repor
|
|
|
68
67
|
|
|
69
68
|
test('listViolators handler returns content and structuredContent for a valid issue', async () => {
|
|
70
69
|
const server = createMCPServer();
|
|
71
|
-
const result = await server._registeredTools.listViolators.handler({
|
|
70
|
+
const result = await (server as any)._registeredTools.listViolators.handler({
|
|
72
71
|
issueID: 'linkNoText',
|
|
73
72
|
timeStamp: '260101T0000',
|
|
74
73
|
jobID: 'mix'
|
|
@@ -80,7 +79,7 @@ test('listViolators handler returns content and structuredContent for a valid is
|
|
|
80
79
|
|
|
81
80
|
test('listDiagnoses handler returns content and structuredContent for a valid diagnosis', async () => {
|
|
82
81
|
const server = createMCPServer();
|
|
83
|
-
const result = await server._registeredTools.listDiagnoses.handler({
|
|
82
|
+
const result = await (server as any)._registeredTools.listDiagnoses.handler({
|
|
84
83
|
catalogIndex: '0',
|
|
85
84
|
issueID: 'linkNoText',
|
|
86
85
|
timeStamp: '260101T0000',
|
|
@@ -93,7 +92,7 @@ test('listDiagnoses handler returns content and structuredContent for a valid di
|
|
|
93
92
|
|
|
94
93
|
test('getReport handler returns content and structuredContent for a valid report', async () => {
|
|
95
94
|
const server = createMCPServer();
|
|
96
|
-
const result = await server._registeredTools.getReport.handler({
|
|
95
|
+
const result = await (server as any)._registeredTools.getReport.handler({
|
|
97
96
|
timeStamp: '260101T0000',
|
|
98
97
|
jobID: 'mix'
|
|
99
98
|
});
|
|
@@ -104,7 +103,7 @@ test('getReport handler returns content and structuredContent for a valid report
|
|
|
104
103
|
|
|
105
104
|
test('requestTest handler returns content and structuredContent', async () => {
|
|
106
105
|
const server = createMCPServer();
|
|
107
|
-
const result = await server._registeredTools.requestTest.handler({
|
|
106
|
+
const result = await (server as any)._registeredTools.requestTest.handler({
|
|
108
107
|
description: 'Test Page',
|
|
109
108
|
URL: 'https://example.com/test',
|
|
110
109
|
reason: 'Because accessibility matters'
|
|
@@ -116,7 +115,7 @@ test('requestTest handler returns content and structuredContent', async () => {
|
|
|
116
115
|
|
|
117
116
|
test('requestRetest handler returns content and structuredContent for a valid report', async () => {
|
|
118
117
|
const server = createMCPServer();
|
|
119
|
-
const result = await server._registeredTools.requestRetest.handler({
|
|
118
|
+
const result = await (server as any)._registeredTools.requestRetest.handler({
|
|
120
119
|
timeStamp: '260101T0000',
|
|
121
120
|
jobID: 'mix',
|
|
122
121
|
reason: 'Because the report is obsolete'
|
|
@@ -128,7 +127,7 @@ test('requestRetest handler returns content and structuredContent for a valid re
|
|
|
128
127
|
|
|
129
128
|
test('requestFeature handler returns content and structuredContent', async () => {
|
|
130
129
|
const server = createMCPServer();
|
|
131
|
-
const result = await server._registeredTools.requestFeature.handler({
|
|
130
|
+
const result = await (server as any)._registeredTools.requestFeature.handler({
|
|
132
131
|
feature: 'A new feature idea'
|
|
133
132
|
});
|
|
134
133
|
assert.ok(result.content);
|
|
@@ -138,7 +137,7 @@ test('requestFeature handler returns content and structuredContent', async () =>
|
|
|
138
137
|
|
|
139
138
|
test('listIssues handler returns an error for a nonexistent report', async () => {
|
|
140
139
|
const server = createMCPServer();
|
|
141
|
-
const result = await server._registeredTools.listIssues.handler({
|
|
140
|
+
const result = await (server as any)._registeredTools.listIssues.handler({
|
|
142
141
|
timeStamp: '990101T0000',
|
|
143
142
|
jobID: 'xxx'
|
|
144
143
|
});
|
|
@@ -148,7 +147,7 @@ test('listIssues handler returns an error for a nonexistent report', async () =>
|
|
|
148
147
|
|
|
149
148
|
test('listViolators handler returns an error for an unknown issue', async () => {
|
|
150
149
|
const server = createMCPServer();
|
|
151
|
-
const result = await server._registeredTools.listViolators.handler({
|
|
150
|
+
const result = await (server as any)._registeredTools.listViolators.handler({
|
|
152
151
|
issueID: 'nonexistentIssue',
|
|
153
152
|
timeStamp: '260101T0000',
|
|
154
153
|
jobID: 'mix'
|
|
@@ -160,7 +159,7 @@ test('listViolators handler returns an error for an unknown issue', async () =>
|
|
|
160
159
|
// INTEGRATION TESTS FOR handleMCP
|
|
161
160
|
|
|
162
161
|
// 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) => {
|
|
162
|
+
const sendMCPRequest = (port: number, method: string, params: any, id: any): Promise<any> => new Promise((resolve, reject) => {
|
|
164
163
|
const body = JSON.stringify({jsonrpc: '2.0', method, params, id});
|
|
165
164
|
const req = http.request({
|
|
166
165
|
port,
|
|
@@ -186,7 +185,7 @@ const sendMCPRequest = (port, method, params, id) => new Promise((resolve, rejec
|
|
|
186
185
|
});
|
|
187
186
|
|
|
188
187
|
// Helper: parses the JSON-RPC result from an SSE response body.
|
|
189
|
-
const parseSSEResult = body => {
|
|
188
|
+
const parseSSEResult = (body: string) => {
|
|
190
189
|
const jsonLine = body.split('\n').find(line => line.startsWith('data: '));
|
|
191
190
|
if (!jsonLine) {
|
|
192
191
|
throw new Error('No data line in SSE response');
|
|
@@ -195,14 +194,13 @@ const parseSSEResult = body => {
|
|
|
195
194
|
};
|
|
196
195
|
|
|
197
196
|
// Helper: starts a local HTTP server with handleMCP and returns it.
|
|
198
|
-
const startMCPServer = () => new Promise(resolve => {
|
|
199
|
-
const {handleMCP} = require('./mcp');
|
|
197
|
+
const startMCPServer = (): Promise<any> => new Promise(resolve => {
|
|
200
198
|
const server = http.createServer((req, res) => handleMCP(req, res));
|
|
201
199
|
server.listen(0, () => resolve(server));
|
|
202
200
|
});
|
|
203
201
|
|
|
204
202
|
// Helper: closes a server with a timeout fallback.
|
|
205
|
-
const closeMCPServer = server => new Promise(resolve => {
|
|
203
|
+
const closeMCPServer = (server: any) => new Promise<void>(resolve => {
|
|
206
204
|
if (!server) {
|
|
207
205
|
resolve();
|
|
208
206
|
return;
|
|
@@ -244,7 +242,7 @@ test('handleMCP lists all 8 tools via tools/list', async () => {
|
|
|
244
242
|
const res = await sendMCPRequest(port, 'tools/list', {}, 2);
|
|
245
243
|
assert.equal(res.statusCode, 200);
|
|
246
244
|
const result = parseSSEResult(res.body);
|
|
247
|
-
const toolNames = result.result.tools.map(t => t.name);
|
|
245
|
+
const toolNames = result.result.tools.map((t: any) => t.name);
|
|
248
246
|
assert.equal(toolNames.length, 8);
|
|
249
247
|
assert.deepEqual(toolNames, [
|
|
250
248
|
'listReports',
|
|
@@ -279,3 +277,32 @@ test('handleMCP executes listReports tool via tools/call', async () => {
|
|
|
279
277
|
await closeMCPServer(server);
|
|
280
278
|
}
|
|
281
279
|
});
|
|
280
|
+
|
|
281
|
+
test('handleMCP rejects a GET request with a 405 JSON-RPC error', async () => {
|
|
282
|
+
const server = await startMCPServer();
|
|
283
|
+
try {
|
|
284
|
+
const port = server.address().port;
|
|
285
|
+
const res = await new Promise<any>((resolve, reject) => {
|
|
286
|
+
const req = http.request({
|
|
287
|
+
port,
|
|
288
|
+
method: 'GET',
|
|
289
|
+
path: '/mcp',
|
|
290
|
+
headers: {accept: 'application/json, text/event-stream'}
|
|
291
|
+
}, response => {
|
|
292
|
+
let data = '';
|
|
293
|
+
response.on('data', chunk => {
|
|
294
|
+
data += chunk;
|
|
295
|
+
});
|
|
296
|
+
response.on('end', () => resolve({statusCode: response.statusCode, body: data}));
|
|
297
|
+
});
|
|
298
|
+
req.on('error', reject);
|
|
299
|
+
req.end();
|
|
300
|
+
});
|
|
301
|
+
assert.equal(res.statusCode, 405);
|
|
302
|
+
const message = JSON.parse(res.body);
|
|
303
|
+
assert.equal(message.error.code, -32000);
|
|
304
|
+
}
|
|
305
|
+
finally {
|
|
306
|
+
await closeMCPServer(server);
|
|
307
|
+
}
|
|
308
|
+
});
|
package/{mcp.js → mcp.ts}
RENAMED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
/*
|
|
2
|
-
mcp.
|
|
2
|
+
mcp.ts
|
|
3
3
|
Handles MCP (Model Context Protocol) requests for Kilotest tools.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const {version} = require('./api/version');
|
|
8
|
+
import type {IncomingMessage, ServerResponse} from 'node:http';
|
|
9
|
+
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
11
|
+
import * as getReportAPI from './api/getReport.ts';
|
|
12
|
+
import * as listReportsAPI from './api/listReports.ts';
|
|
13
|
+
import * as listIssuesAPI from './api/listIssues.ts';
|
|
14
|
+
import * as listViolatorsAPI from './api/listViolators.ts';
|
|
15
|
+
import * as listDiagnosesAPI from './api/listDiagnoses.ts';
|
|
16
|
+
import * as requestTestAPI from './api/requestTest.ts';
|
|
17
|
+
import * as requestRetestAPI from './api/requestRetest.ts';
|
|
18
|
+
import * as requestFeatureAPI from './api/requestFeature.ts';
|
|
19
|
+
import {version} from './api/version.ts';
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
import {
|
|
23
22
|
getReportSchema,
|
|
24
23
|
listIssuesSchema,
|
|
25
24
|
listViolatorsSchema,
|
|
@@ -35,16 +34,16 @@ const {
|
|
|
35
34
|
requestTestResponseSchema,
|
|
36
35
|
requestRetestResponseSchema,
|
|
37
36
|
requestFeatureResponseSchema
|
|
38
|
-
}
|
|
37
|
+
} from './api/schemas.ts';
|
|
39
38
|
|
|
40
39
|
// CONSTANTS
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
export const mcpPath = '/mcp';
|
|
43
42
|
|
|
44
43
|
// FUNCTIONS
|
|
45
44
|
|
|
46
45
|
// Creates and returns an McpServer with Kilotest tools registered.
|
|
47
|
-
|
|
46
|
+
export const createMCPServer = (): McpServer => {
|
|
48
47
|
const server = new McpServer({
|
|
49
48
|
name: 'Kilotest',
|
|
50
49
|
version,
|
|
@@ -208,9 +207,19 @@ exports.createMCPServer = () => {
|
|
|
208
207
|
return server;
|
|
209
208
|
};
|
|
210
209
|
// Handles an MCP request.
|
|
211
|
-
|
|
210
|
+
export const handleMCP = async (request: IncomingMessage, response: ServerResponse): Promise<void> => {
|
|
211
|
+
// If the request is not a POST request: reject it, because this stateless server has no sessions for GET-opened SSE streams or DELETE requests to act on, and leaving such streams open would leak resources.
|
|
212
|
+
if (request.method !== 'POST') {
|
|
213
|
+
response.writeHead(405, {allow: 'POST', 'content-type': 'application/json'});
|
|
214
|
+
response.end(JSON.stringify({
|
|
215
|
+
jsonrpc: '2.0',
|
|
216
|
+
error: {code: -32000, message: 'Method Not Allowed: only POST requests are accepted'},
|
|
217
|
+
id: null
|
|
218
|
+
}));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
212
221
|
const transport = new StreamableHTTPServerTransport({sessionIdGenerator: undefined});
|
|
213
|
-
const server =
|
|
222
|
+
const server = createMCPServer();
|
|
214
223
|
await server.connect(transport);
|
|
215
224
|
await transport.handleRequest(request, response);
|
|
216
225
|
};
|
package/memory/MEMORY.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Kilotest Memory Index
|
|
2
|
+
|
|
3
|
+
This directory holds persistent context files for the Kilotest project. Each file captures a decision, profile, or plan that should inform future work on this codebase.
|
|
4
|
+
|
|
5
|
+
## Index
|
|
6
|
+
|
|
7
|
+
- [user_maintainer_context.md](user_maintainer_context.md) — user: Maintainer is 84 years old, sole contributor, prioritizes legibility for future maintainers over short-term velocity
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: user
|
|
3
|
+
name: Kilotest maintainer profile
|
|
4
|
+
description: Maintainer is the sole contributor until now and assigns a high probability to maintainership passing to others within 3 years, so prioritizes legibility for future maintainers.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Kilotest maintaitenance strategy
|
|
8
|
+
|
|
9
|
+
## Primary goal
|
|
10
|
+
|
|
11
|
+
Make the codebase forkable and maintainable by future discoverers who have no prior knowledge of the project. Legibility for a cold reader outweighs any speed or terseness optimization.
|
|
12
|
+
|
|
13
|
+
## Working style
|
|
14
|
+
|
|
15
|
+
- Major refactorings for architectural improvement are acceptable.
|
|
16
|
+
- Responsibility can be shared between the maintainer and AI agents, with ultimate maintainer responsibility.
|
|
17
|
+
- Delegation of discretion to AI agents is expected to increase gradually as its benefits are demonstrted.
|
|
18
|
+
- Correctness should be promoted by a combination of:
|
|
19
|
+
- Type constraints
|
|
20
|
+
- Comprehensive tests
|
|
21
|
+
- Nonduplication of code
|
|
22
|
+
- Separation of concerns
|
|
23
|
+
- Focused purposes of functions and modules
|
|
24
|
+
- Thorough documentation
|