@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
package/api/{util.js → util.ts}
RENAMED
|
@@ -1,38 +1,45 @@
|
|
|
1
1
|
/*
|
|
2
|
-
util.
|
|
2
|
+
util.ts
|
|
3
3
|
Utilities for API requests.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
const {
|
|
8
|
+
import {
|
|
10
9
|
getAgoDays,
|
|
11
|
-
|
|
10
|
+
getReportExtracts,
|
|
12
11
|
getNowStamp,
|
|
13
|
-
getPlainText,
|
|
14
12
|
getRandomString,
|
|
15
13
|
getReportExtract,
|
|
16
14
|
getReportStats,
|
|
17
15
|
objectSort,
|
|
18
|
-
ruleEngines
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
16
|
+
ruleEngines
|
|
17
|
+
} from '../util.ts';
|
|
18
|
+
import type {ReportExtract} from '../util.ts';
|
|
19
|
+
import {issues as issueSpecs} from 'testaro-issues';
|
|
22
20
|
|
|
23
|
-
//
|
|
21
|
+
// TYPES
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
// The basics about a report, as returned by getReportBasics.
|
|
24
|
+
export type ReportBasics = {
|
|
25
|
+
identifier: string;
|
|
26
|
+
'completion date and time': string;
|
|
27
|
+
'days since the report was completed': number | null;
|
|
28
|
+
'tested web page': {description: string; URL: string};
|
|
29
|
+
'whether a later report about the same page exists': boolean;
|
|
30
|
+
};
|
|
26
31
|
|
|
27
32
|
// FUNCTIONS
|
|
28
33
|
|
|
34
|
+
// Returns the base URL of this Kilotest host.
|
|
35
|
+
export const getThisHost = () => process.env.THIS_KILOTEST_HOST;
|
|
29
36
|
// Returns uniform metadata for every response.
|
|
30
|
-
|
|
37
|
+
export const getResponseMetadata = () => ({
|
|
31
38
|
identifier: `${getNowStamp()}-${getRandomString(3)}`,
|
|
32
39
|
'date and time': new Date().toISOString()
|
|
33
40
|
});
|
|
34
41
|
// Returns facts about the tool collection (Kilotest).
|
|
35
|
-
|
|
42
|
+
export const getToolsFacts = () => ({
|
|
36
43
|
'name': 'Kilotest',
|
|
37
44
|
'description': {
|
|
38
45
|
'what Kilotest does': 'Kilotest tools generate and make available findings about the front-end quality (i.e. accessibility, usability, and standards conformity) of web pages. A Kilotest job generates findings by using Testaro to test a page against about 1300 rules defined by an ensemble of twelve rule engines. Testaro produces a report of the job. The report describes violations of the rules. Kilotest uses Testilo to enhance the report with a classification of the rule violations into about 380 issues. Kilotest makes facts about the issues and the violations retrievable at four levels of granularity.',
|
|
@@ -49,11 +56,11 @@ exports.getToolsFacts = () => ({
|
|
|
49
56
|
'confirmation': 'Use the listReports tool to determine whether a requested new report exists. There is currently no process for notification of the outcome of requests.'
|
|
50
57
|
}
|
|
51
58
|
},
|
|
52
|
-
'URL': `${
|
|
53
|
-
'web users can obtain similar functionalities at':
|
|
59
|
+
'URL': `${getThisHost()}/mcp`,
|
|
60
|
+
'web users can obtain similar functionalities at': getThisHost()
|
|
54
61
|
});
|
|
55
62
|
// Returns the facts about a rule engine.
|
|
56
|
-
const getRuleEngineFacts =
|
|
63
|
+
export const getRuleEngineFacts = (ruleEngineID: string) => {
|
|
57
64
|
const ruleEngineData = ruleEngines[ruleEngineID] || [null, null];
|
|
58
65
|
return {
|
|
59
66
|
identifier: ruleEngineID,
|
|
@@ -62,15 +69,28 @@ const getRuleEngineFacts = exports.getRuleEngineFacts = ruleEngineID => {
|
|
|
62
69
|
};
|
|
63
70
|
};
|
|
64
71
|
// Returns the facts about rule engines.
|
|
65
|
-
|
|
72
|
+
export const getRuleEnginesFacts = (ruleEngineIDSet: Iterable<string>) => {
|
|
66
73
|
const ruleEnginesFacts = Array.from(ruleEngineIDSet).map(id => getRuleEngineFacts(id));
|
|
67
74
|
objectSort(ruleEnginesFacts, 'name', 'alpha');
|
|
68
75
|
return ruleEnginesFacts;
|
|
69
76
|
};
|
|
70
77
|
// Returns the basics about a report.
|
|
71
|
-
// Accepts optional precomputed extract
|
|
72
|
-
//
|
|
73
|
-
|
|
78
|
+
// Accepts an optional precomputed extract to avoid redundant reads when called
|
|
79
|
+
// in a loop over all reports (e.g. by listReports). When the extract comes from
|
|
80
|
+
// getReportExtracts, it carries a superseded flag; otherwise the flag is computed.
|
|
81
|
+
// With an extract provided, failure is impossible, so the return type narrows
|
|
82
|
+
// to ReportBasics; without one, an error object may be returned.
|
|
83
|
+
// XXX Why are these overloads necessary? Can't we just have one function?
|
|
84
|
+
export function getReportBasics(
|
|
85
|
+
timeStamp: string, jobID: string, extract: ReportExtract
|
|
86
|
+
): Promise<ReportBasics>;
|
|
87
|
+
export function getReportBasics(
|
|
88
|
+
timeStamp: string, jobID: string, extract?: ReportExtract | null
|
|
89
|
+
): Promise<ReportBasics | {error: string}>;
|
|
90
|
+
export async function getReportBasics(
|
|
91
|
+
timeStamp: string, jobID: string, extract: ReportExtract | null = null
|
|
92
|
+
): Promise<ReportBasics | {error: string}> {
|
|
93
|
+
const extractProvided = !!extract;
|
|
74
94
|
// If an extract was not provided, verify the report exists and read it.
|
|
75
95
|
if (!extract) {
|
|
76
96
|
// Get the creation time of the report.
|
|
@@ -84,32 +104,35 @@ exports.getReportBasics = async (timeStamp, jobID, extract = null, latestReportE
|
|
|
84
104
|
};
|
|
85
105
|
}
|
|
86
106
|
// Otherwise, i.e. if it exists, get an extract of the report.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
107
|
+
const fetchedExtract = await getReportExtract(timeStamp, jobID);
|
|
108
|
+
// If this failed, return why.
|
|
109
|
+
if ('error' in fetchedExtract) {
|
|
110
|
+
return fetchedExtract;
|
|
111
|
+
}
|
|
112
|
+
extract = fetchedExtract;
|
|
93
113
|
}
|
|
114
|
+
const {url, description, reportTime} = extract;
|
|
94
115
|
// Get whether this report has been superseded.
|
|
95
|
-
const isSuperseded =
|
|
96
|
-
|
|
116
|
+
const isSuperseded = extractProvided
|
|
117
|
+
? extract.superseded === true
|
|
118
|
+
: (await getReportExtracts(true))
|
|
119
|
+
.every(ex => ex.timeStamp !== timeStamp || ex.jobID !== jobID);
|
|
97
120
|
// Get the basics about the report.
|
|
98
121
|
const basics = {
|
|
99
122
|
identifier: `${timeStamp}-${jobID}`,
|
|
100
123
|
'completion date and time': reportTime,
|
|
101
124
|
'days since the report was completed': getAgoDays(new Date(reportTime)),
|
|
102
125
|
'tested web page': {
|
|
103
|
-
description
|
|
126
|
+
description,
|
|
104
127
|
URL: url
|
|
105
128
|
},
|
|
106
129
|
'whether a later report about the same page exists': isSuperseded
|
|
107
130
|
};
|
|
108
131
|
// Return them.
|
|
109
132
|
return basics;
|
|
110
|
-
}
|
|
133
|
+
}
|
|
111
134
|
// Returns the specification of an issue.
|
|
112
|
-
|
|
135
|
+
export const getIssueSpec = (issueID: string) => {
|
|
113
136
|
// Get the issue specification.
|
|
114
137
|
const issueSpec = issueSpecs[issueID];
|
|
115
138
|
// If it exists:
|
|
@@ -126,23 +149,3 @@ exports.getIssueSpec = issueID => {
|
|
|
126
149
|
// Otherwise, i.e. if it does not exist, return this.
|
|
127
150
|
return null;
|
|
128
151
|
};
|
|
129
|
-
// Processes a test or retest request.
|
|
130
|
-
exports.processTestRequest = async (testType, what, url, why) => {
|
|
131
|
-
// Get an email-safe version of the reason.
|
|
132
|
-
const plainWhy = getPlainText(why);
|
|
133
|
-
// Update the waiting recommendations as a transaction.
|
|
134
|
-
const updateResult = await updateRecs(what, url, plainWhy);
|
|
135
|
-
// If the recommendation was a duplicate:
|
|
136
|
-
if (updateResult.error === 'duplicate') {
|
|
137
|
-
// Return this.
|
|
138
|
-
return {
|
|
139
|
-
status: 'error',
|
|
140
|
-
message: 'Duplicate request'
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
// Otherwise, i.e. if it was not a duplicate, alert a manager about it.
|
|
144
|
-
await sendAlert(
|
|
145
|
-
`Kilotest: new ${testType} recommendation in the API`,
|
|
146
|
-
`Target: ${what}\nURL: ${url}\nReason: ${plainWhy}`
|
|
147
|
-
);
|
|
148
|
-
};
|
package/api/version.ts
ADDED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/*
|
|
2
|
-
apiIntegration.test.
|
|
2
|
+
apiIntegration.test.ts
|
|
3
3
|
Minimal HTTP integration tests verifying routing, body parsing, and JSON serialization, using the fixture corpus and a temporary in-process server.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const {requestHandler} = require('./index');
|
|
8
|
+
import {test, before, after} from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import http from 'node:http';
|
|
11
|
+
import {requestHandler} from './index.ts';
|
|
13
12
|
|
|
14
13
|
// CONSTANTS
|
|
15
14
|
|
|
@@ -18,12 +17,12 @@ const port = 3998;
|
|
|
18
17
|
// SETUP AND TEARDOWN
|
|
19
18
|
|
|
20
19
|
const savedDBDir = process.env.DB_DIR;
|
|
21
|
-
let server;
|
|
20
|
+
let server: http.Server;
|
|
22
21
|
|
|
23
22
|
before(async () => {
|
|
24
|
-
process.env.DB_DIR =
|
|
23
|
+
process.env.DB_DIR = (await import('./test/dbFixture.ts')).fixtureDBDir;
|
|
25
24
|
server = http.createServer(requestHandler);
|
|
26
|
-
await new Promise(resolve => server.listen(port, () => resolve()));
|
|
25
|
+
await new Promise<void>(resolve => server.listen(port, () => resolve()));
|
|
27
26
|
});
|
|
28
27
|
|
|
29
28
|
after(async () => {
|
|
@@ -34,7 +33,7 @@ after(async () => {
|
|
|
34
33
|
delete process.env.DB_DIR;
|
|
35
34
|
}
|
|
36
35
|
server.closeAllConnections?.();
|
|
37
|
-
await new Promise(resolve => {
|
|
36
|
+
await new Promise<void>(resolve => {
|
|
38
37
|
const timer = setTimeout(() => {
|
|
39
38
|
server.closeAllConnections?.();
|
|
40
39
|
resolve();
|
|
@@ -49,8 +48,8 @@ after(async () => {
|
|
|
49
48
|
// HELPERS
|
|
50
49
|
|
|
51
50
|
// Sends an HTTP request and returns the parsed JSON response body.
|
|
52
|
-
const request = (method, requestPath, body = null) => new Promise((resolve, reject) => {
|
|
53
|
-
const options = {method, host: 'localhost', port, path: requestPath};
|
|
51
|
+
const request = (method: string, requestPath: string, body: any = null): Promise<any> => new Promise((resolve, reject) => {
|
|
52
|
+
const options: any = {method, host: 'localhost', port, path: requestPath};
|
|
54
53
|
if (body) {
|
|
55
54
|
const bodyJSON = JSON.stringify(body);
|
|
56
55
|
options.headers = {
|
|
@@ -59,7 +58,7 @@ const request = (method, requestPath, body = null) => new Promise((resolve, reje
|
|
|
59
58
|
};
|
|
60
59
|
}
|
|
61
60
|
const req = http.request(options, response => {
|
|
62
|
-
const chunks = [];
|
|
61
|
+
const chunks: any[] = [];
|
|
63
62
|
response.on('data', chunk => chunks.push(chunk));
|
|
64
63
|
response.on('end', () => {
|
|
65
64
|
const bodyString = chunks.join('');
|
package/balances.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
balances.ts
|
|
3
|
+
Manages monetary balances and sends alerts when they near exhaustion.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
import {sendAlert} from './alerts.ts';
|
|
9
|
+
import {errorMessage, getJSON} from './util.ts';
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import type {Act, Report} from 'testaro';
|
|
13
|
+
|
|
14
|
+
// CONSTANTS
|
|
15
|
+
|
|
16
|
+
// The path of the file recording the AI service 0 balance.
|
|
17
|
+
export const balancePath = path.join(import.meta.dirname, 'ai0Balance.json');
|
|
18
|
+
|
|
19
|
+
// FUNCTIONS
|
|
20
|
+
|
|
21
|
+
// Checks a report for balances nearing exhaustion.
|
|
22
|
+
export const checkBalancesForAlerts = async (report: Report): Promise<void> => {
|
|
23
|
+
// Get the alert thresholds and prices from the environment.
|
|
24
|
+
const WAVE_THRESHOLD = Number(process.env.WAVE_BALANCE_THRESHOLD);
|
|
25
|
+
const AI_SERVICE0_THRESHOLD = Number(process.env.AI_SERVICE0_BALANCE_THRESHOLD);
|
|
26
|
+
const AI_MODEL0_INPUT_PRICE = Number(process.env.AI_MODEL0_INPUT_PRICE);
|
|
27
|
+
const AI_MODEL0_OUTPUT_PRICE = Number(process.env.AI_MODEL0_OUTPUT_PRICE);
|
|
28
|
+
// If the variables to be monitored for alerts are defined:
|
|
29
|
+
if (WAVE_THRESHOLD && AI_SERVICE0_THRESHOLD && AI_MODEL0_INPUT_PRICE && AI_MODEL0_OUTPUT_PRICE) {
|
|
30
|
+
// WAVE.
|
|
31
|
+
const waveAct = report.acts.find((act: Act) => act.type === 'test' && act.which === 'wave');
|
|
32
|
+
const creditsRemaining = waveAct?.data?.creditsRemaining;
|
|
33
|
+
// If a WAVE balance nearing exhaustion is reported:
|
|
34
|
+
if (typeof creditsRemaining === 'number' && creditsRemaining < WAVE_THRESHOLD) {
|
|
35
|
+
// Alert a manager.
|
|
36
|
+
await sendAlert(
|
|
37
|
+
'Kilotest: WAVE balance low',
|
|
38
|
+
`Only ${creditsRemaining} WAVE credits remain (3 used per job)`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const testaroAct = report.acts.find((act: Act) => act.type === 'test' && act.which === 'testaro');
|
|
42
|
+
// Get the AI model token usage for the testaro allCaps test.
|
|
43
|
+
const usage = (testaroAct?.data?.ruleData as {
|
|
44
|
+
allCaps?: {aiModelUsage?: {inputTokens: number; outputTokens: number}}
|
|
45
|
+
} | undefined)?.allCaps?.aiModelUsage;
|
|
46
|
+
let balanceJSON = null;
|
|
47
|
+
try {
|
|
48
|
+
// Get the recorded AI service 0 balance.
|
|
49
|
+
balanceJSON = await fs.readFile(balancePath, 'utf8');
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
console.error('ERROR: AI service 0 balance file missing');
|
|
53
|
+
}
|
|
54
|
+
// If the variables required for an AI service 0 balance alert are defined:
|
|
55
|
+
if (usage && AI_MODEL0_INPUT_PRICE && AI_MODEL0_OUTPUT_PRICE && balanceJSON) {
|
|
56
|
+
const inputCost = AI_MODEL0_INPUT_PRICE * usage.inputTokens;
|
|
57
|
+
const outputCost = AI_MODEL0_OUTPUT_PRICE * usage.outputTokens;
|
|
58
|
+
const cost = inputCost + outputCost;
|
|
59
|
+
// If any cost was incurred:
|
|
60
|
+
if (cost > 0) {
|
|
61
|
+
// Warn about this.
|
|
62
|
+
console.log(
|
|
63
|
+
'This job has made the production AI service 0 balance record wrong. Update it.'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const balanceData = JSON.parse(balanceJSON);
|
|
68
|
+
// Get an estimate of the balance after this job.
|
|
69
|
+
const newBalance = balanceData.balance - cost;
|
|
70
|
+
// Update the recorded balance.
|
|
71
|
+
await fs.writeFile(balancePath, getJSON({balance: newBalance}));
|
|
72
|
+
console.log(`Estimated new AI Service 0 balance: $${newBalance.toFixed(2)}`);
|
|
73
|
+
// If it is nearing exhaustion:
|
|
74
|
+
if (newBalance < AI_SERVICE0_THRESHOLD) {
|
|
75
|
+
// Alert a manager.
|
|
76
|
+
await sendAlert(
|
|
77
|
+
'Kilotest: AI service 0 balance low',
|
|
78
|
+
`Balance of AI service 0 account (https://console.anthropic.com) only about $${newBalance.toFixed(2)} (about $0.01 used per job)`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
console.log(`ERROR managing AI service 0 balance: ${errorMessage(error)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
/*
|
|
2
|
-
checkinfo.
|
|
2
|
+
checkinfo.ts
|
|
3
3
|
Summarizes the validation scope of the lint, test, and test:smoke scripts by deriving file and test-case counts from their actual configurations, so the summary stays accurate as the codebase changes.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import {execSync} from 'node:child_process';
|
|
11
|
+
import {routes} from './index.ts';
|
|
11
12
|
|
|
12
13
|
// CONSTANTS
|
|
13
14
|
|
|
14
|
-
const rootDir =
|
|
15
|
+
const rootDir = import.meta.dirname;
|
|
15
16
|
const excludeDirs = new Set(['node_modules', '.git', 'coverage']);
|
|
16
17
|
|
|
17
18
|
// FUNCTIONS
|
|
18
19
|
|
|
19
20
|
// Recursively collects all files under a directory, skipping excluded directories.
|
|
20
|
-
const collectFiles = dir => {
|
|
21
|
-
const results = [];
|
|
21
|
+
const collectFiles = (dir: string): string[] => {
|
|
22
|
+
const results: string[] = [];
|
|
22
23
|
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
|
|
23
24
|
const fullPath = path.join(dir, entry.name);
|
|
24
25
|
if (entry.isDirectory()) {
|
|
@@ -35,7 +36,7 @@ const collectFiles = dir => {
|
|
|
35
36
|
|
|
36
37
|
// Converts a glob pattern to a RegExp for matching relative paths.
|
|
37
38
|
// Supports * (non-segment), ** (any), and literal characters.
|
|
38
|
-
const globToRegExp = pattern => {
|
|
39
|
+
const globToRegExp = (pattern: string): RegExp => {
|
|
39
40
|
const escaped = pattern
|
|
40
41
|
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
41
42
|
.replace(/\*\*/g, '\x00')
|
|
@@ -45,33 +46,34 @@ const globToRegExp = pattern => {
|
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
// Returns whether a relative path matches any pattern in a list.
|
|
48
|
-
const matchesAny = (relPath, patterns) =>
|
|
49
|
+
const matchesAny = (relPath: string, patterns: string[]) =>
|
|
50
|
+
patterns.some(p => globToRegExp(p).test(relPath));
|
|
49
51
|
|
|
50
52
|
// Extracts the top-level ignores array from eslint.config.mjs.
|
|
51
|
-
const getEslintIgnores = () => {
|
|
53
|
+
const getEslintIgnores = (): string[] => {
|
|
52
54
|
const content = fs.readFileSync(path.join(rootDir, 'eslint.config.mjs'), 'utf8');
|
|
53
55
|
const match = content.match(/ignores:\s*\[([\s\S]*?)\]/);
|
|
54
56
|
if (!match) {
|
|
55
57
|
return [];
|
|
56
58
|
}
|
|
57
|
-
return [...match[1]
|
|
59
|
+
return [...match[1]!.matchAll(/"([^"]+)"/g)].map(m => m[1]!);
|
|
58
60
|
};
|
|
59
61
|
|
|
60
62
|
// Parses .markdownlint-cli2.jsonc and returns its ignores array.
|
|
61
|
-
const getMarkdownlintIgnores = () => {
|
|
63
|
+
const getMarkdownlintIgnores = (): string[] => {
|
|
62
64
|
const content = fs.readFileSync(path.join(rootDir, '.markdownlint-cli2.jsonc'), 'utf8');
|
|
63
65
|
const stripped = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
64
66
|
const match = stripped.match(/"ignores"\s*:\s*\[([\s\S]*?)\]/);
|
|
65
67
|
if (!match) {
|
|
66
68
|
return [];
|
|
67
69
|
}
|
|
68
|
-
return [...match[1]
|
|
70
|
+
return [...match[1]!.matchAll(/"([^"]+)"/g)].map(m => m[1]!);
|
|
69
71
|
};
|
|
70
72
|
|
|
71
73
|
// Runs the Node test runner with the TAP reporter and parses the test count, so dynamically generated tests (e.g., from for-loops) are counted accurately.
|
|
72
74
|
const getTestCaseCount = () => {
|
|
73
75
|
const output = execSync(
|
|
74
|
-
|
|
76
|
+
"node --require ./test/setup.ts --experimental-test-module-mocks --test --test-concurrency=1 --test-timeout=100 --test-reporter=tap '**/*.test.ts'",
|
|
75
77
|
{cwd: rootDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']}
|
|
76
78
|
);
|
|
77
79
|
const match = output.match(/^# tests (\d+)$/m);
|
|
@@ -87,7 +89,7 @@ const eslintIgnores = getEslintIgnores();
|
|
|
87
89
|
const markdownlintIgnores = getMarkdownlintIgnores();
|
|
88
90
|
|
|
89
91
|
// ESLint file counts by extension, excluding ignored files.
|
|
90
|
-
const eslintExts = {js: 0, json: 0, md: 0, css: 0};
|
|
92
|
+
const eslintExts = {js: 0, ts: 0, json: 0, md: 0, css: 0};
|
|
91
93
|
for (const rel of relFiles) {
|
|
92
94
|
if (matchesAny(rel, eslintIgnores)) {
|
|
93
95
|
continue;
|
|
@@ -95,6 +97,9 @@ for (const rel of relFiles) {
|
|
|
95
97
|
if (/\.(js|mjs|cjs)$/.test(rel)) {
|
|
96
98
|
eslintExts.js++;
|
|
97
99
|
}
|
|
100
|
+
else if (/\.ts$/.test(rel)) {
|
|
101
|
+
eslintExts.ts++;
|
|
102
|
+
}
|
|
98
103
|
else if (/\.json$/.test(rel)) {
|
|
99
104
|
eslintExts.json++;
|
|
100
105
|
}
|
|
@@ -113,20 +118,19 @@ const markdownlintCount = relFiles.filter(
|
|
|
113
118
|
).length;
|
|
114
119
|
|
|
115
120
|
// Test file and test-case counts.
|
|
116
|
-
const testFiles = relFiles.filter(rel => /\.test\.
|
|
121
|
+
const testFiles = relFiles.filter(rel => /\.test\.ts$/.test(rel));
|
|
117
122
|
const testCaseCount = getTestCaseCount();
|
|
118
123
|
|
|
119
|
-
// Smoke-test path counts from the routes table in index.
|
|
120
|
-
const {routes} = require('./index');
|
|
124
|
+
// Smoke-test path counts from the routes table in index.ts.
|
|
121
125
|
const smokeGetCount = routes.GET.length;
|
|
122
126
|
const smokePostCount = routes.POST.length;
|
|
123
127
|
const smokeTotal = smokeGetCount + smokePostCount;
|
|
124
128
|
|
|
125
129
|
// OUTPUT
|
|
126
130
|
|
|
127
|
-
const parts = [
|
|
131
|
+
const parts: [string, string[]][] = [
|
|
128
132
|
['lint', [
|
|
129
|
-
`ESLint: ${eslintTotal} files (${eslintExts.js} JS, ${eslintExts.json} JSON, ${eslintExts.md} MD, ${eslintExts.css} CSS)`,
|
|
133
|
+
`ESLint: ${eslintTotal} files (${eslintExts.js} JS, ${eslintExts.ts} TS, ${eslintExts.json} JSON, ${eslintExts.md} MD, ${eslintExts.css} CSS)`,
|
|
130
134
|
`markdownlint: ${markdownlintCount} files`
|
|
131
135
|
]],
|
|
132
136
|
['test', [
|
package/docs/AI-TOOLS.md
CHANGED
|
@@ -26,14 +26,14 @@ The internal features that make Kilotest a collection of tools for language mode
|
|
|
26
26
|
- An `llms.txt` file and an `llms-full.txt` file, documenting the use of Kilotest by language models, conforming to the [llms-txt](https://llmstxt.org/) specification.
|
|
27
27
|
- A generated `openapi.yaml` file, documenting the Kilotest API, conforming to the [OpenAPI specification](https://spec.openapis.org/oas/v3.1.0).
|
|
28
28
|
- An `mcp.js` file, providing an MCP server for Kilotest.
|
|
29
|
-
- A `server.json` file, conforming to the [MCP server schema](https://modelcontextprotocol.io/), and a GitHub Actions workflow (`publish-mcp.yml`) that authenticates via GitHub OIDC and publishes that file to the official MCP Registry whenever `server.json` or `api/version.
|
|
29
|
+
- A `server.json` file, conforming to the [MCP server schema](https://modelcontextprotocol.io/), and a GitHub Actions workflow (`publish-mcp.yml`) that authenticates via GitHub OIDC and publishes that file to the official MCP Registry whenever `server.json` or `api/version.ts` changes on the `main` branch.
|
|
30
30
|
- A `capability.md` file with a URL, summarizing what Kilotest tools can do.
|
|
31
31
|
- `mcp`, `mcp-server`, and `modelcontextprotocol` keywords in `package.json`, supporting discovery of the `@jrpool/kilotest` npm package by tools and humans searching the npm registry for MCP servers.
|
|
32
32
|
- An `lhm.plugin.json` file, conforming to the [LobeHub plugin manifest format](https://market.lobehub.com/s/publish-mcp/references/manifest), declaring Kilotest as a plugin in the [LobeHub MCP Market](https://lobehub.com/mcp). Publishing and updating are non-automated (`lhm plugin publish`/`update`), because LobeHub requires browser-based OAuth and does not support machine-to-machine publishing.
|
|
33
33
|
- A `sitemap.xml` file.
|
|
34
34
|
- Documentation in the `README.md` file.
|
|
35
35
|
- This `AI-TOOLS.md` file.
|
|
36
|
-
- A [tutorial on authorizing language models to use Kilotest](https://kilotest.com/
|
|
36
|
+
- A [tutorial on authorizing language models to use Kilotest](https://kilotest.com/tutorialAI.html).
|
|
37
37
|
|
|
38
38
|
## External features
|
|
39
39
|
|
|
@@ -44,7 +44,7 @@ The external features that support the use of Kilotest as a collection of AI too
|
|
|
44
44
|
- A [pull request](https://github.com/w3c/wai-evaluation-tools-list/pull/1153) to add Kilotest to the WAI evaluation tools list.
|
|
45
45
|
- Registration of Kilotest as an active server in the [official MCP Registry](https://registry.modelcontextprotocol.io/v0/servers?search=kilotest) (`io.github.jrpool/kilotest`), maintained by the [Model Context Protocol](https://modelcontextprotocol.io/) project. The [PulseMCP](https://www.pulsemcp.com/servers) directory and the VS Code MCP gallery both crawl this registry, so Kilotest is discoverable through them with no separate submission.
|
|
46
46
|
- Registration of Kilotest with the [Smithery](https://smithery.ai/servers/pool/kilotest) MCP server registry.
|
|
47
|
-
- Registration of Kilotest with the [Glama](https://glama.ai/mcp/connectors/com.kilotest/kilotest) MCP server registry.
|
|
47
|
+
- Registration of Kilotest with the [Glama](https://glama.ai/mcp/connectors/com.kilotest/kilotest) MCP server registry, including a claim of ownership of the MCP connector by the maintainer via a `TXT` DNS record for `_glama-claim.kilotest.com`, with the value `glama_claim_gVV4HrPdcXr9TEfj1p3icEUBJ3_OVJGX`.
|
|
48
48
|
- Registration of Kilotest with [Awesome MCP Servers](https://www.tensorblock.co/mcp/servers/github-jrpool-kilotest-f518ba36).
|
|
49
49
|
- Registration of Kilotest with the [RapidAPI](https://rapidapi.com/jrpool/api/kilotest/playground/apiendpoint_0f03577a-ff9a-472a-a0ed-533bd198981a) Hub.
|
|
50
50
|
- Registration of Kilotest with the [LobeHub MCP Market](https://lobehub.com/mcp) (identifier: `jrpool-kilotest`), enabled with an `lhm.plugin.json` file.
|
|
@@ -61,7 +61,7 @@ The external features that support the use of Kilotest as a collection of AI too
|
|
|
61
61
|
|
|
62
62
|
## Required maintenance
|
|
63
63
|
|
|
64
|
-
Whenever the `/api/version.
|
|
64
|
+
Whenever the `/api/version.ts` file is updated:
|
|
65
65
|
|
|
66
66
|
- Update the `server.json` file (version field).
|
|
67
67
|
- Update the `lhm.plugin.json` file (version field).
|