@jrpool/kilotest 46.1.2 → 47.0.1

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.
Files changed (147) hide show
  1. package/.claude/settings.local.json +13 -1
  2. package/.devin/config.local.json +5 -1
  3. package/.github/workflows/ci.yml +0 -3
  4. package/.github/workflows/periodic-smoke-tests.yml +30 -0
  5. package/.github/workflows/publish-mcp.yml +4 -4
  6. package/{alerts.test.js → alerts.test.ts} +17 -29
  7. package/{alerts.js → alerts.ts} +10 -13
  8. package/api/{getReport.test.js → getReport.test.ts} +9 -10
  9. package/api/{getReport.js → getReport.ts} +23 -24
  10. package/api/{listDiagnoses.test.js → listDiagnoses.test.ts} +26 -27
  11. package/api/{listDiagnoses.js → listDiagnoses.ts} +46 -70
  12. package/api/{listIssues.test.js → listIssues.test.ts} +21 -14
  13. package/api/{listIssues.js → listIssues.ts} +75 -74
  14. package/api/{listReports.test.js → listReports.test.ts} +17 -16
  15. package/api/listReports.ts +87 -0
  16. package/api/{listViolators.test.js → listViolators.test.ts} +19 -20
  17. package/api/{listViolators.js → listViolators.ts} +45 -70
  18. package/api/requestFeature.test.ts +54 -0
  19. package/api/{requestFeature.js → requestFeature.ts} +18 -15
  20. package/api/{requestRetest.test.js → requestRetest.test.ts} +33 -28
  21. package/api/{requestRetest.js → requestRetest.ts} +32 -25
  22. package/api/{requestTest.test.js → requestTest.test.ts} +35 -30
  23. package/api/{requestTest.js → requestTest.ts} +35 -28
  24. package/api/{routes.test.js → routes.test.ts} +5 -5
  25. package/api/{routes.js → routes.ts} +6 -6
  26. package/api/{schemas.js → schemas.ts} +69 -50
  27. package/api/{util.test.js → util.test.ts} +31 -11
  28. package/api/{util.js → util.ts} +61 -36
  29. package/api/version.ts +8 -0
  30. package/{apiIntegration.test.js → apiIntegration.test.ts} +12 -13
  31. package/balances.ts +87 -0
  32. package/{checkinfo.js → checkinfo.ts} +24 -20
  33. package/docs/AI-TOOLS.md +4 -4
  34. package/docs/BACKLOG.md +356 -0
  35. package/docs/SERVICE.md +144 -17
  36. package/docs/decisions/002-coverage-tool.md +40 -0
  37. package/docs/decisions/003-adr-threshold.md +34 -0
  38. package/docs/decisions/004-docs-directory.md +23 -0
  39. package/docs/decisions/005-observability.md +28 -0
  40. package/docs/decisions/006-git-workflow.md +28 -0
  41. package/docs/decisions/007-internal-testing.md +34 -0
  42. package/docs/decisions/008-dependencies.md +28 -0
  43. package/docs/decisions/009-qai-integration.md +40 -0
  44. package/docs/decisions/QAI-HISTORICAL-CONTEXT.md +42 -0
  45. package/eslint.config.mjs +27 -0
  46. package/index.html +3 -3
  47. package/{index.test.js → index.test.ts} +380 -143
  48. package/{index.js → index.ts} +382 -283
  49. package/{indexBalances.test.js → indexBalances.test.ts} +21 -17
  50. package/{indexServe.test.js → indexServe.test.ts} +9 -9
  51. package/linkValidation.test.ts +190 -0
  52. package/{mcp.test.js → mcp.test.ts} +53 -26
  53. package/{mcp.js → mcp.ts} +29 -20
  54. package/memory/MEMORY.md +7 -0
  55. package/memory/user_maintainer_context.md +24 -0
  56. package/openapi.yaml +115 -68
  57. package/package.json +35 -15
  58. package/pm2.config.cjs +29 -0
  59. package/{routes.test.js → routes.test.ts} +8 -7
  60. package/{routing.test.js → routing.test.ts} +11 -12
  61. package/scripts/{generate-openapi.js → generate-openapi.ts} +13 -13
  62. package/serve.ts +15 -0
  63. package/smokeTest.ts +234 -0
  64. package/test/dbFixture.ts +21 -0
  65. package/test/fixtures/{buildFixtures.js → buildFixtures.ts} +14 -13
  66. package/test/{setup.cjs → setup.ts} +2 -2
  67. package/tsconfig.json +16 -0
  68. package/{util.test.js → util.test.ts} +350 -257
  69. package/util.ts +921 -0
  70. package/web/ai0BalanceForm/{ai0BalanceForm.test.js → ai0BalanceForm.test.ts} +15 -15
  71. package/web/ai0BalanceForm/{index.js → index.ts} +10 -18
  72. package/web/enqueue/index.ts +56 -0
  73. package/web/enqueueForm/index.html +9 -9
  74. package/web/enqueueForm/index.ts +43 -0
  75. package/web/expungeReportsForm/{expungeReportsForm.test.js → expungeReportsForm.test.ts} +18 -20
  76. package/web/expungeReportsForm/index.ts +18 -0
  77. package/web/hideReportForm/{hideReportForm.test.js → hideReportForm.test.ts} +14 -14
  78. package/web/hideReportForm/{index.js → index.ts} +17 -21
  79. package/web/listDiagnoses/{index.js → index.ts} +48 -58
  80. package/web/listDiagnoses/{listDiagnoses.test.js → listDiagnoses.test.ts} +39 -34
  81. package/web/listIssues/{index.js → index.ts} +67 -92
  82. package/web/listIssues/{listIssues.test.js → listIssues.test.ts} +45 -51
  83. package/web/listReports/index.html +3 -3
  84. package/web/listReports/{index.js → index.ts} +48 -53
  85. package/web/listReports/{listReports.test.js → listReports.test.ts} +41 -43
  86. package/web/listRules/{index.js → index.ts} +18 -23
  87. package/web/listRules/{listRules.test.js → listRules.test.ts} +26 -19
  88. package/web/listTopIssues/{index.js → index.ts} +36 -47
  89. package/web/listTopIssues/{listTopIssues.test.js → listTopIssues.test.ts} +26 -17
  90. package/web/listViolators/{index.js → index.ts} +51 -71
  91. package/web/listViolators/{listViolators.test.js → listViolators.test.ts} +43 -36
  92. package/web/manage/index.ts +22 -0
  93. package/web/pruneReportsForm/index.ts +18 -0
  94. package/web/pruneReportsForm/{pruneReportsForm.test.js → pruneReportsForm.test.ts} +11 -11
  95. package/web/reannotate/index.ts +70 -0
  96. package/web/reannotate/{reannotate.test.js → reannotate.test.ts} +19 -22
  97. package/web/reannotateForm/{index.js → index.ts} +41 -50
  98. package/web/reannotateForm/{reannotateForm.test.js → reannotateForm.test.ts} +13 -16
  99. package/web/renewWCAG/{index.js → index.ts} +9 -9
  100. package/web/renewWCAG/{renewWCAG.test.js → renewWCAG.test.ts} +17 -20
  101. package/web/renewWCAGForm/index.ts +22 -0
  102. package/web/{expungeReportsForm/index.js → reportDeletion.ts} +56 -38
  103. package/web/requestRetest/index.html +7 -7
  104. package/web/requestRetest/index.ts +31 -0
  105. package/web/requestRetest/requestRetest.test.ts +116 -0
  106. package/web/requestRetestForm/{index.js → index.ts} +10 -16
  107. package/web/requestRetestForm/{requestRetestForm.test.js → requestRetestForm.test.ts} +11 -12
  108. package/web/requestTest/index.html +7 -7
  109. package/web/requestTest/{index.js → index.ts} +5 -5
  110. package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +21 -21
  111. package/web/requestTestForm/index.html +4 -4
  112. package/web/requestTestForm/index.ts +21 -0
  113. package/web/requestTestForm/{requestTestForm.test.js → requestTestForm.test.ts} +9 -9
  114. package/web/rewindReportsForm/index.ts +18 -0
  115. package/web/rewindReportsForm/{rewindReportsForm.test.js → rewindReportsForm.test.ts} +11 -11
  116. package/web/tutorialAI/index.html +184 -0
  117. package/web/tutorialAI/index.ts +80 -0
  118. package/web/tutorialAI/tutorialAI.test.ts +192 -0
  119. package/web/{tutorial → tutorialWeb}/index.html +9 -8
  120. package/web/tutorialWeb/index.ts +78 -0
  121. package/web/tutorialWeb/tutorialWeb.test.ts +195 -0
  122. package/web/unhideReportForm/{index.js → index.ts} +17 -21
  123. package/web/unhideReportForm/{unhideReportForm.test.js → unhideReportForm.test.ts} +16 -16
  124. package/aceconfig.js +0 -28
  125. package/api/listReports.js +0 -89
  126. package/api/requestFeature.test.js +0 -58
  127. package/api/version.js +0 -8
  128. package/indexFallback.test.js +0 -33
  129. package/indexWorkers.test.js +0 -79
  130. package/jsconfig.json +0 -8
  131. package/pm2.config.js +0 -18
  132. package/smokeTest.js +0 -128
  133. package/util.js +0 -744
  134. package/web/enqueue/index.js +0 -67
  135. package/web/enqueueForm/index.js +0 -49
  136. package/web/manage/index.js +0 -22
  137. package/web/pruneReportsForm/index.js +0 -123
  138. package/web/reannotate/index.js +0 -58
  139. package/web/renewWCAGForm/index.js +0 -22
  140. package/web/requestRetest/index.js +0 -31
  141. package/web/requestRetest/requestRetest.test.js +0 -119
  142. package/web/requestTestForm/index.js +0 -21
  143. package/web/rewindReportsForm/index.js +0 -121
  144. package/web/tutorial/index.js +0 -63
  145. package/web/tutorial/tutorial.test.js +0 -97
  146. /package/test/fixtures/db/jobs/{recs.json → testRequests.json} +0 -0
  147. /package/web/{tutorial → tutorialWeb}/images/newsletter-form.png +0 -0
@@ -1,13 +1,13 @@
1
1
  /*
2
- indexBalances.test.js
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
- const path = require('node:path');
10
- const fixtureDBDir = path.join(__dirname, 'test', 'fixtures', 'db');
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
- 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');
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(__dirname, 'ai0Balance.json');
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.js
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
- const path = require('node:path');
10
- const fixtureDBDir = path.join(__dirname, 'test', 'fixtures', 'db');
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
- 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');
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.js
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
- 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');
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 = path.join(__dirname, 'test', 'fixtures', 'db');
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
+ });
@@ -1,25 +1,24 @@
1
1
  /*
2
- mcp.js
2
+ mcp.ts
3
3
  Handles MCP (Model Context Protocol) requests for Kilotest tools.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {McpServer} = require('@modelcontextprotocol/sdk/server/mcp.js');
9
- const {StreamableHTTPServerTransport} = require(
10
- '@modelcontextprotocol/sdk/server/streamableHttp.js'
11
- );
12
- const getReportAPI = require('./api/getReport');
13
- const listReportsAPI = require('./api/listReports');
14
- const listIssuesAPI = require('./api/listIssues');
15
- const listViolatorsAPI = require('./api/listViolators');
16
- const listDiagnosesAPI = require('./api/listDiagnoses');
17
- const requestTestAPI = require('./api/requestTest');
18
- const requestRetestAPI = require('./api/requestRetest');
19
- const requestFeatureAPI = require('./api/requestFeature');
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
- const {
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
- } = require('./api/schemas');
37
+ } from './api/schemas.ts';
39
38
 
40
39
  // CONSTANTS
41
40
 
42
- exports.mcpPath = '/mcp';
41
+ export const mcpPath = '/mcp';
43
42
 
44
43
  // FUNCTIONS
45
44
 
46
45
  // Creates and returns an McpServer with Kilotest tools registered.
47
- exports.createMCPServer = () => {
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
- exports.handleMCP = async (request, response) => {
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 = exports.createMCPServer();
222
+ const server = createMCPServer();
214
223
  await server.connect(transport);
215
224
  await transport.handleRequest(request, response);
216
225
  };
@@ -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