@jrpool/kilotest 48.0.0 → 50.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/scheduled_tasks.lock +1 -0
- package/api/getReport.ts +3 -1
- package/api/requestFeature.test.ts +23 -3
- package/api/requestFeature.ts +19 -11
- package/api/requestRetest.test.ts +39 -13
- package/api/requestRetest.ts +11 -5
- package/api/requestTest.ts +6 -6
- package/api/schemas.ts +2 -2
- package/docs/BACKLOG.md +232 -12
- package/docs/SERVICE.md +84 -3
- package/index.test.ts +443 -20
- package/index.ts +246 -27
- package/linkValidation.test.ts +1 -0
- package/mcp.test.ts +31 -1
- package/mcp.ts +9 -0
- package/package.json +1 -1
- package/smokeTest.ts +16 -0
- package/test/dbFixture.ts +8 -0
- package/util.test.ts +295 -0
- package/util.ts +121 -0
- package/web/ai0BalanceForm/ai0BalanceForm.test.ts +16 -10
- package/web/ai0BalanceForm/index.html +1 -1
- package/web/ai0BalanceForm/index.ts +12 -8
- package/web/enqueue/index.ts +33 -39
- package/web/expungeReportsForm/expungeReportsForm.test.ts +19 -11
- package/web/expungeReportsForm/index.html +1 -1
- package/web/expungeReportsForm/index.ts +1 -1
- package/web/hideReportForm/hideReportForm.test.ts +24 -9
- package/web/hideReportForm/index.html +1 -1
- package/web/hideReportForm/index.ts +13 -8
- package/web/listTopIssues/index.ts +1 -1
- package/web/manage/index.html +2 -1
- package/web/metrics/index.html +19 -0
- package/web/metrics/index.ts +127 -0
- package/web/metrics/metrics.test.ts +211 -0
- package/web/pruneReportsForm/index.html +1 -1
- package/web/pruneReportsForm/index.ts +1 -1
- package/web/pruneReportsForm/pruneReportsForm.test.ts +18 -10
- package/web/reannotate/index.ts +7 -3
- package/web/reannotate/reannotate.test.ts +1 -1
- package/web/renewWCAG/index.ts +6 -3
- package/web/renewWCAG/renewWCAG.test.ts +1 -1
- package/web/reportDeletion.ts +13 -7
- package/web/rewindReportsForm/index.html +1 -1
- package/web/rewindReportsForm/index.ts +1 -1
- package/web/rewindReportsForm/rewindReportsForm.test.ts +18 -10
- package/web/showHiddenReportsForm/index.html +24 -0
- package/web/showHiddenReportsForm/index.ts +45 -0
- package/web/showHiddenReportsForm/showHiddenReportsForm.test.ts +103 -0
- package/web/tutorialAI/index.ts +1 -1
- package/web/tutorialWeb/index.ts +1 -1
- package/web/unhideReportForm/index.html +1 -1
- package/web/unhideReportForm/index.ts +13 -8
- package/web/unhideReportForm/unhideReportForm.test.ts +19 -11
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"sessionId":"f44e6b44-375e-4007-879a-19e4e5ed34a0","pid":90042,"procStart":"Sun Sep 20 03:19:03 2026","acquiredAt":1789874948676}
|
package/api/getReport.ts
CHANGED
|
@@ -25,7 +25,9 @@ type ResponseContent = z.infer<typeof getReportResponseSchema>['response content
|
|
|
25
25
|
export const response = async (args: string[]) => {
|
|
26
26
|
const [timeStamp = '', jobID = ''] = args;
|
|
27
27
|
const thisHost = getThisHost();
|
|
28
|
-
// Get the report size.
|
|
28
|
+
// Get the report size. A malformed timestamp or job ID resolves to a nonexistent file
|
|
29
|
+
// path, which getReportStats already reports as unavailable, so no separate syntactic
|
|
30
|
+
// check is needed here.
|
|
29
31
|
const reportStats = await getReportStats(timeStamp, jobID);
|
|
30
32
|
// Create the response content.
|
|
31
33
|
const responseContent: ResponseContent = reportStats
|
|
@@ -32,7 +32,27 @@ test('requestFeature rejects an empty feature request', async () => {
|
|
|
32
32
|
const body = await response(['']);
|
|
33
33
|
const details = body['response content']['details about your request'] as any;
|
|
34
34
|
assert.ok(details.error);
|
|
35
|
-
assert.ok(
|
|
35
|
+
assert.ok(
|
|
36
|
+
!logged.some(line => line.startsWith('WARNING (Kilotest: MCP feature request received)'))
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('requestFeature rejects a feature request shorter than 20 characters', async () => {
|
|
41
|
+
const body = await response(['Too short']);
|
|
42
|
+
const details = body['response content']['details about your request'] as any;
|
|
43
|
+
assert.ok(details.error.includes('feature description'));
|
|
44
|
+
assert.ok(
|
|
45
|
+
!logged.some(line => line.startsWith('WARNING (Kilotest: MCP feature request received)'))
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('requestFeature rejects a feature request longer than 1000 characters', async () => {
|
|
50
|
+
const body = await response(['x'.repeat(1001)]);
|
|
51
|
+
const details = body['response content']['details about your request'] as any;
|
|
52
|
+
assert.ok(details.error.includes('feature description'));
|
|
53
|
+
assert.ok(
|
|
54
|
+
!logged.some(line => line.startsWith('WARNING (Kilotest: MCP feature request received)'))
|
|
55
|
+
);
|
|
36
56
|
});
|
|
37
57
|
|
|
38
58
|
test('requestFeature accepts a non-empty feature request and notifies the manager', async () => {
|
|
@@ -42,13 +62,13 @@ test('requestFeature accepts a non-empty feature request and notifies the manage
|
|
|
42
62
|
assert.ok(details['date and time received']);
|
|
43
63
|
assert.equal(details.disposition, 'received and logged; manager notified');
|
|
44
64
|
assert.ok(logged.some(line =>
|
|
45
|
-
line.startsWith('WARNING (MCP feature request received)')
|
|
65
|
+
line.startsWith('WARNING (Kilotest: MCP feature request received)')
|
|
46
66
|
&& line.includes('Add a dark mode toggle')
|
|
47
67
|
));
|
|
48
68
|
});
|
|
49
69
|
|
|
50
70
|
test('requestFeature includes tool name and metadata', async () => {
|
|
51
|
-
const body = await response(['
|
|
71
|
+
const body = await response(['A sufficiently long feature description']);
|
|
52
72
|
assert.equal(body['tool name'], 'requestFeature');
|
|
53
73
|
assert.ok(body['response metadata'].identifier);
|
|
54
74
|
});
|
package/api/requestFeature.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import {z} from 'zod';
|
|
9
9
|
import {getResponseMetadata, getThisHost, getToolsFacts} from './util.ts';
|
|
10
10
|
import {sendAlert} from '../alerts.ts';
|
|
11
|
+
import {checkLength} from '../util.ts';
|
|
11
12
|
import {requestFeatureResponseSchema} from './schemas.ts';
|
|
12
13
|
|
|
13
14
|
// TYPES
|
|
@@ -21,20 +22,27 @@ type ResponseContent = z.infer<typeof requestFeatureResponseSchema>['response co
|
|
|
21
22
|
export const response = async (args: string[]) => {
|
|
22
23
|
const [feature = ''] = args;
|
|
23
24
|
const thisHost = getThisHost();
|
|
25
|
+
const lengthCheck = checkLength(feature, 20, 1000, 'feature description');
|
|
24
26
|
// Initialize the response content.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
let responseContent: ResponseContent;
|
|
28
|
+
// If the feature description is invalid:
|
|
29
|
+
if (lengthCheck.status === 'error') {
|
|
30
|
+
responseContent = {
|
|
31
|
+
'details about your request': {
|
|
32
|
+
error: `request invalid: ${lengthCheck.message}`
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// Otherwise, i.e. if it is valid:
|
|
37
|
+
else {
|
|
32
38
|
// Notify the manager.
|
|
33
|
-
await sendAlert('MCP feature request received', feature);
|
|
39
|
+
await sendAlert('Kilotest: MCP feature request received', feature);
|
|
34
40
|
// Add the disposition to the response content.
|
|
35
|
-
responseContent
|
|
36
|
-
'
|
|
37
|
-
|
|
41
|
+
responseContent = {
|
|
42
|
+
'details about your request': {
|
|
43
|
+
'date and time received': new Date().toISOString(),
|
|
44
|
+
disposition: 'received and logged; manager notified'
|
|
45
|
+
}
|
|
38
46
|
};
|
|
39
47
|
}
|
|
40
48
|
// Create a response body.
|
|
@@ -15,11 +15,29 @@ import {fixtureDBDir} from '../test/dbFixture.ts';
|
|
|
15
15
|
|
|
16
16
|
const savedDBDir = process.env.DB_DIR;
|
|
17
17
|
const testRequestsPath = path.join(fixtureDBDir, 'jobs', 'testRequests.json');
|
|
18
|
+
// A report added only to this test's disposable copy of the fixture database (never to
|
|
19
|
+
// the tracked test/fixtures/db corpus), so its 3-character job ID satisfies isJobID
|
|
20
|
+
// without perturbing report-count tests elsewhere that copy the whole corpus. Its job ID
|
|
21
|
+
// intentionally differs from the tracked corpus's 2-character 260101T0001-ct.json, which
|
|
22
|
+
// isJobID would reject.
|
|
23
|
+
const retestFixturePath = path.join(fixtureDBDir, 'reports', '260101T0003-ret.json');
|
|
24
|
+
const retestFixtureReport = {
|
|
25
|
+
id: '260101T0003-ret',
|
|
26
|
+
what: 'Retest Fixture Page',
|
|
27
|
+
target: {what: 'Retest Fixture Page', url: 'https://example.com/retestfixture'},
|
|
28
|
+
sources: {worker: 'test-worker'},
|
|
29
|
+
acts: [],
|
|
30
|
+
jobData: {startTime: '26-01-01T00:00', endTime: '26-01-01T00:10', elapsedSeconds: 600, preventions: {}, issuelessRules: []},
|
|
31
|
+
catalog: {},
|
|
32
|
+
images: {},
|
|
33
|
+
checkpoints: []
|
|
34
|
+
};
|
|
18
35
|
let logged: any[] = [];
|
|
19
36
|
const originalLog = console.log;
|
|
20
37
|
|
|
21
|
-
before(() => {
|
|
38
|
+
before(async () => {
|
|
22
39
|
process.env.DB_DIR = fixtureDBDir;
|
|
40
|
+
await fs.writeFile(retestFixturePath, JSON.stringify(retestFixtureReport));
|
|
23
41
|
});
|
|
24
42
|
|
|
25
43
|
// Reset the test-requests file and capture console.log, so the alert that
|
|
@@ -32,8 +50,9 @@ beforeEach(async () => {
|
|
|
32
50
|
|
|
33
51
|
import {response} from './requestRetest.ts';
|
|
34
52
|
|
|
35
|
-
after(() => {
|
|
53
|
+
after(async () => {
|
|
36
54
|
console.log = originalLog;
|
|
55
|
+
await fs.unlink(retestFixturePath);
|
|
37
56
|
if (savedDBDir !== undefined) {
|
|
38
57
|
process.env.DB_DIR = savedDBDir;
|
|
39
58
|
}
|
|
@@ -45,12 +64,19 @@ after(() => {
|
|
|
45
64
|
// TESTS
|
|
46
65
|
|
|
47
66
|
test('requestRetest rejects a nonexistent report', async () => {
|
|
48
|
-
const body = await response(['
|
|
67
|
+
const body = await response(['251231T0000', 'zzz', 'A reason that is long enough.']);
|
|
49
68
|
const details = body['response content']['details about your request'] as any;
|
|
50
69
|
assert.ok(details.error.includes('does not exist'));
|
|
51
70
|
assert.ok(!logged.some(line => line.includes('new retest request awaits approval')));
|
|
52
71
|
});
|
|
53
72
|
|
|
73
|
+
test('requestRetest rejects a malformed timestamp or job identifier', async () => {
|
|
74
|
+
const body = await response(['999999T9999', 'xyz', 'A reason that is long enough.']);
|
|
75
|
+
const details = body['response content']['details about your request'] as any;
|
|
76
|
+
assert.ok(details.error.includes('malformed'));
|
|
77
|
+
assert.ok(!logged.some(line => line.includes('new retest request awaits approval')));
|
|
78
|
+
});
|
|
79
|
+
|
|
54
80
|
test('requestRetest rejects a superseded report', async () => {
|
|
55
81
|
// 260101T0000-mix is an earlier report of "Mixed Outcomes Page" than 260202T0000-new,
|
|
56
82
|
// so it is superseded and processTestRequest returns 'superseded', reported via the
|
|
@@ -66,7 +92,7 @@ test('requestRetest rejects a superseded report', async () => {
|
|
|
66
92
|
});
|
|
67
93
|
|
|
68
94
|
test('requestRetest rejects a reason shorter than 20 characters', async () => {
|
|
69
|
-
const body = await response(['
|
|
95
|
+
const body = await response(['260101T0003', 'ret', 'short']);
|
|
70
96
|
const details = body['response content']['details about your request'] as any;
|
|
71
97
|
assert.ok(details.error.includes('reason'));
|
|
72
98
|
assert.ok(!logged.some(line => line.includes('new retest request awaits approval')));
|
|
@@ -74,20 +100,20 @@ test('requestRetest rejects a reason shorter than 20 characters', async () => {
|
|
|
74
100
|
|
|
75
101
|
test('requestRetest rejects a reason longer than 100 characters', async () => {
|
|
76
102
|
const longReason = 'x'.repeat(101);
|
|
77
|
-
const body = await response(['
|
|
103
|
+
const body = await response(['260101T0003', 'ret', longReason]);
|
|
78
104
|
const details = body['response content']['details about your request'] as any;
|
|
79
105
|
assert.ok(details.error.includes('reason'));
|
|
80
106
|
assert.ok(!logged.some(line => line.includes('new retest request awaits approval')));
|
|
81
107
|
});
|
|
82
108
|
|
|
83
109
|
test('requestRetest rejects a report matching a claimed job by description', async () => {
|
|
84
|
-
//
|
|
110
|
+
// 260101T0003-ret is the "Retest Fixture Page" report.
|
|
85
111
|
const claimedPath = path.join(fixtureDBDir, 'jobs', 'claimed', 'clm.json');
|
|
86
112
|
await fs.writeFile(claimedPath, JSON.stringify({
|
|
87
|
-
target: {what: '
|
|
113
|
+
target: {what: 'Retest Fixture Page', url: 'https://example.com/unrelated'}
|
|
88
114
|
}));
|
|
89
115
|
try {
|
|
90
|
-
const body = await response(['
|
|
116
|
+
const body = await response(['260101T0003', 'ret', 'A reason that is long enough.']);
|
|
91
117
|
const details = body['response content']['details about your request'] as any;
|
|
92
118
|
assert.equal(details.error, undefined);
|
|
93
119
|
const disposition = body['response content']['disposition of your request'] as any;
|
|
@@ -102,13 +128,13 @@ test('requestRetest rejects a report matching a claimed job by description', asy
|
|
|
102
128
|
});
|
|
103
129
|
|
|
104
130
|
test('requestRetest rejects a report matching a queued job by URL', async () => {
|
|
105
|
-
//
|
|
131
|
+
// 260101T0003-ret's URL is https://example.com/retestfixture.
|
|
106
132
|
const queuedPath = path.join(fixtureDBDir, 'jobs', 'queue', 'que.json');
|
|
107
133
|
await fs.writeFile(queuedPath, JSON.stringify({
|
|
108
|
-
target: {what: 'Some Other Page', url: 'https://example.com/
|
|
134
|
+
target: {what: 'Some Other Page', url: 'https://example.com/retestfixture'}
|
|
109
135
|
}));
|
|
110
136
|
try {
|
|
111
|
-
const body = await response(['
|
|
137
|
+
const body = await response(['260101T0003', 'ret', 'A reason that is long enough.']);
|
|
112
138
|
const details = body['response content']['details about your request'] as any;
|
|
113
139
|
assert.equal(details.error, undefined);
|
|
114
140
|
const disposition = body['response content']['disposition of your request'] as any;
|
|
@@ -123,8 +149,8 @@ test('requestRetest rejects a report matching a queued job by URL', async () =>
|
|
|
123
149
|
});
|
|
124
150
|
|
|
125
151
|
test('requestRetest rejects a duplicate request', async () => {
|
|
126
|
-
await response(['
|
|
127
|
-
const body = await response(['
|
|
152
|
+
await response(['260101T0003', 'ret', 'A reason that is long enough.']);
|
|
153
|
+
const body = await response(['260101T0003', 'ret', 'A reason that is long enough.']);
|
|
128
154
|
const details = body['response content']['details about your request'] as any;
|
|
129
155
|
assert.equal(details.error, undefined);
|
|
130
156
|
const disposition = body['response content']['disposition of your request'] as any;
|
package/api/requestRetest.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import {z} from 'zod';
|
|
9
9
|
import {getResponseMetadata, getThisHost, getToolsFacts} from './util.ts';
|
|
10
|
-
import {processTestRequest} from '../util.ts';
|
|
10
|
+
import {checkLength, isJobID, isTimeStamp, processTestRequest} from '../util.ts';
|
|
11
11
|
import {requestRetestResponseSchema} from './schemas.ts';
|
|
12
12
|
|
|
13
13
|
// TYPES
|
|
@@ -21,14 +21,20 @@ type ResponseContent = z.infer<typeof requestRetestResponseSchema>['response con
|
|
|
21
21
|
export const response = async (args: string[]) => {
|
|
22
22
|
const [timeStamp = '', jobID = '', reason = ''] = args;
|
|
23
23
|
const thisHost = getThisHost();
|
|
24
|
-
const
|
|
24
|
+
const reasonCheck = checkLength(reason, 20, 100, 'reason');
|
|
25
25
|
// Initialize the response-content properties.
|
|
26
26
|
let requestDetails: ResponseContent['details about your request'];
|
|
27
27
|
let requestDisposition: ResponseContent['disposition of your request'] = null;
|
|
28
|
-
// If the
|
|
29
|
-
if (
|
|
28
|
+
// If the timestamp or job ID is not syntactically valid:
|
|
29
|
+
if (!isTimeStamp(timeStamp) || !isJobID(jobID)) {
|
|
30
30
|
requestDetails = {
|
|
31
|
-
error: 'request invalid:
|
|
31
|
+
error: 'request invalid: the report timestamp or job identifier is malformed'
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// Otherwise, if the encoded reason is too short or too long:
|
|
35
|
+
else if (reasonCheck.status === 'error') {
|
|
36
|
+
requestDetails = {
|
|
37
|
+
error: `request invalid: your ${reasonCheck.message}`
|
|
32
38
|
}
|
|
33
39
|
}
|
|
34
40
|
// Otherwise, i.e. if the request is facially valid:
|
package/api/requestTest.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import {z} from 'zod';
|
|
9
9
|
import {getResponseMetadata, getThisHost, getToolsFacts} from './util.ts';
|
|
10
|
-
import {isURL, processTestRequest} from '../util.ts';
|
|
10
|
+
import {checkLength, isURL, processTestRequest} from '../util.ts';
|
|
11
11
|
import {requestTestResponseSchema} from './schemas.ts';
|
|
12
12
|
|
|
13
13
|
// TYPES
|
|
@@ -24,14 +24,14 @@ export const response = async (args: string[]) => {
|
|
|
24
24
|
// Initialize the response-content properties.
|
|
25
25
|
let requestDetails: ResponseContent['details about your request'];
|
|
26
26
|
let requestDisposition: ResponseContent['disposition of your request'] = null;
|
|
27
|
-
const
|
|
28
|
-
// If the description is
|
|
29
|
-
if (
|
|
27
|
+
const descriptionCheck = checkLength(description, 1, 100, 'description of the page');
|
|
28
|
+
// If the description is invalid:
|
|
29
|
+
if (descriptionCheck.status === 'error') {
|
|
30
30
|
requestDetails = {
|
|
31
|
-
error:
|
|
31
|
+
error: `request invalid: your ${descriptionCheck.message}`
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
-
// Otherwise, i.e. if the URL is too short or too long
|
|
34
|
+
// Otherwise, i.e. if the URL is too short or too long:
|
|
35
35
|
else if (url.length < 12 || url.length > 300) {
|
|
36
36
|
requestDetails = {
|
|
37
37
|
error: 'request invalid: you specified a URL for the page that is not between 12 and 300 characters long'
|
package/api/schemas.ts
CHANGED
|
@@ -43,7 +43,7 @@ export const getReportSchema = {
|
|
|
43
43
|
|
|
44
44
|
// requestTest: POST /api/requestTest
|
|
45
45
|
export const requestTestSchema = {
|
|
46
|
-
description: z.string().describe('
|
|
46
|
+
description: z.string().describe('1- to 100-character description of the page conforming to the naming convention used in the listReports output'),
|
|
47
47
|
URL: z.string().describe('12- to 300-character URL of the page, including the https:// scheme and any query'),
|
|
48
48
|
reason: z.string().describe('20- to 100-character reason why the page should be tested')
|
|
49
49
|
};
|
|
@@ -57,7 +57,7 @@ export const requestRetestSchema = {
|
|
|
57
57
|
|
|
58
58
|
// requestFeature: POST /api/requestFeature
|
|
59
59
|
export const requestFeatureSchema = {
|
|
60
|
-
feature: z.string().describe('description of requested feature improvement or new feature')
|
|
60
|
+
feature: z.string().describe('20- to 1000-character description of requested feature improvement or new feature')
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
// listReports takes no input; omitted (mcp.ts already uses inputSchema: {}).
|