From 71385fe59374ba54c3c5b1252a32d027a6277dc8 Mon Sep 17 00:00:00 2001 From: Jonathan Pool Date: Mon, 28 Sep 2026 15:05:52 -0700 Subject: [PATCH 1/3] disambiguate test terms --- api/awaitTest.test.ts | 4 +- api/awaitTest.ts | 10 +-- api/listReports.ts | 4 +- ...orderTest.test.ts => orderNewTest.test.ts} | 34 +++---- api/{orderTest.ts => orderNewTest.ts} | 12 +-- ...estTest.test.ts => requestNewTest.test.ts} | 50 +++++------ api/{requestTest.ts => requestNewTest.ts} | 14 +-- api/requestRetest.test.ts | 12 +-- api/routes.test.ts | 6 +- api/routes.ts | 26 +++--- api/schemas.ts | 28 +++--- api/util.ts | 6 +- api/version.ts | 8 +- docs/SERVICE.md | 14 ++- env.example | 4 +- index.test.ts | 88 +++++++++++++------ index.ts | 70 ++++++++++----- lhm.plugin.json | 2 +- linkValidation.test.ts | 4 +- llms-full.txt | 20 ++--- llms.txt | 8 +- mcp.test.ts | 24 ++--- mcp.ts | 42 ++++----- openapi.yaml | 22 ++--- package.json | 2 +- routes.test.ts | 2 +- routing.test.ts | 4 +- server.json | 2 +- sitemap.xml | 2 +- smokeTest.ts | 4 +- util.test.ts | 8 +- util.ts | 23 +++-- web/listReports/index.html | 2 +- web/listReports/listReports.test.ts | 2 +- .../index.html | 0 web/{requestTest => requestNewTest}/index.ts | 2 +- .../requestNewTest.test.ts} | 4 +- .../index.html | 2 +- .../index.ts | 0 .../requestNewTestForm.test.ts} | 12 +-- 40 files changed, 324 insertions(+), 259 deletions(-) rename api/{orderTest.test.ts => orderNewTest.test.ts} (86%) rename api/{orderTest.ts => orderNewTest.ts} (94%) rename api/{requestTest.test.ts => requestNewTest.test.ts} (80%) rename api/{requestTest.ts => requestNewTest.ts} (91%) rename web/{requestTest => requestNewTest}/index.html (100%) rename web/{requestTest => requestNewTest}/index.ts (97%) rename web/{requestTest/requestTest.test.ts => requestNewTest/requestNewTest.test.ts} (98%) rename web/{requestTestForm => requestNewTestForm}/index.html (95%) rename web/{requestTestForm => requestNewTestForm}/index.ts (100%) rename web/{requestTestForm/requestTestForm.test.ts => requestNewTestForm/requestNewTestForm.test.ts} (68%) diff --git a/api/awaitTest.test.ts b/api/awaitTest.test.ts index f7090deb..cf9d29d4 100644 --- a/api/awaitTest.test.ts +++ b/api/awaitTest.test.ts @@ -102,12 +102,12 @@ test('awaitTest response has no web UI equivalents for this request or its ances assert.equal(similarWeb['closest ancestor request'], null); }); -test('awaitTest reports both orderTest and orderRetest as possible ancestors', async () => { +test('awaitTest reports both orderNewTest and orderRetest as possible ancestors', async () => { const body = await response(['260101T0000', 'mix']); const ancestors = (body['this request'] as any)['closest ancestor request']; assert.equal(ancestors.length, 2); const names = ancestors.map((a: any) => a['tool name']).sort(); - assert.deepEqual(names, ['orderRetest', 'orderTest']); + assert.deepEqual(names, ['orderNewTest', 'orderRetest']); for (const ancestor of ancestors) { assert.equal(ancestor.method, 'POST'); } diff --git a/api/awaitTest.ts b/api/awaitTest.ts index 053730eb..2998ff3c 100644 --- a/api/awaitTest.ts +++ b/api/awaitTest.ts @@ -1,6 +1,6 @@ /* awaitTest.ts - Waits for a job ordered via orderTest or orderRetest to complete, and returns its outcome. + Waits for a job ordered via orderNewTest or orderRetest to complete, and returns its outcome. */ // IMPORTS @@ -75,18 +75,18 @@ export const response = async (args: string[]) => { 'tool collection': getToolsFacts(), 'tool name': 'awaitTest', 'this request': { - description: 'Wait for a job ordered via the orderTest or orderRetest tool to complete. The timeStamp and jobID parameters are the report identifier that orderTest or orderRetest returned. This call blocks until the report is ready, the job fails, or a maximum wait time elapses.', + description: 'Wait for a job ordered via the orderNewTest or orderRetest tool to complete. The timeStamp and jobID parameters are the report identifier that orderNewTest or orderRetest returned. This call blocks until the report is ready, the job fails, or a maximum wait time elapses.', method: 'POST', URL: `${thisHost}/api/awaitTest/${timeStamp}/${jobID}`, - // awaitTest's own parameters do not say whether the job was ordered via orderTest + // awaitTest's own parameters do not say whether the job was ordered via orderNewTest // or orderRetest, so both are reported as possible ancestors, rather than // arbitrarily naming just one. 'closest ancestor request': [ { - 'tool name': 'orderTest', + 'tool name': 'orderNewTest', description: 'Fulfill or reject my order to test a page about which no report is available yet.', method: 'POST', - URL: `${thisHost}/api/orderTest` + URL: `${thisHost}/api/orderNewTest` }, { 'tool name': 'orderRetest', diff --git a/api/listReports.ts b/api/listReports.ts index 4b58565c..deeae6b7 100644 --- a/api/listReports.ts +++ b/api/listReports.ts @@ -53,7 +53,7 @@ export const response = async () => { 'basics about all available reports': reportsBasics, 'how to request that a page with no report be tested': { method: 'POST', - URL: `${thisHost}/api/requestTest`, + URL: `${thisHost}/api/requestNewTest`, 'request body': { description: '10- to 100-character description of the page conforming to the naming convention used in this list of reports', URL: '12- to 300-character URL of the page, including the https:// scheme and any query', @@ -62,7 +62,7 @@ export const response = async () => { 'how to check whether the request has been fulfilled': 'use this listReports tool to determine whether a report about the page has become available (typical wait time: 1 hour to 1 day)' }, 'how a web user can request that the page be tested': { - URL: `${thisHost}/requestTestForm.html` + URL: `${thisHost}/requestNewTestForm.html` } }; // Create a response body. diff --git a/api/orderTest.test.ts b/api/orderNewTest.test.ts similarity index 86% rename from api/orderTest.test.ts rename to api/orderNewTest.test.ts index 4d117f98..578b3b45 100644 --- a/api/orderTest.test.ts +++ b/api/orderNewTest.test.ts @@ -1,6 +1,6 @@ /* - orderTest.test.ts - Tests for api/orderTest.ts using the fixture corpus. + orderNewTest.test.ts + Tests for api/orderNewTest.ts using the fixture corpus. */ // IMPORTS @@ -11,7 +11,7 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import {fixtureDBDir} from '../test/dbFixture.ts'; -// Blank the alert configuration unconditionally, before ./orderTest.ts (whose orderJob +// Blank the alert configuration unconditionally, before ./orderNewTest.ts (whose orderJob // calls no alert directly, but whose redirect-rejection branch does) is ever imported // below, so this file sends no real alert emails even when run directly rather than via // `npm test`. @@ -44,7 +44,7 @@ beforeEach(async () => { console.log = (...args) => logged.push(args.join(' ')); }); -import {response} from './orderTest.ts'; +import {response} from './orderNewTest.ts'; after(() => { console.log = originalLog; @@ -58,36 +58,36 @@ after(() => { // TESTS -test('orderTest rejects an empty description', async () => { +test('orderNewTest rejects an empty description', async () => { const body = await response(['', 'https://example.com/test', 'A reason that is long enough.']); assert.ok((body['response content']['details about your order'] as any).error); }); -test('orderTest rejects a reason shorter than 20 characters', async () => { +test('orderNewTest rejects a reason shorter than 20 characters', async () => { const body = await response(['Ordered Page', 'https://example.com/ordered', 'short']); const details = body['response content']['details about your order'] as any; assert.ok(details.error.includes('reason')); }); -test('orderTest rejects a reason longer than 100 characters', async () => { +test('orderNewTest rejects a reason longer than 100 characters', async () => { const longReason = 'x'.repeat(101); const body = await response(['Ordered Page', 'https://example.com/ordered', longReason]); const details = body['response content']['details about your order'] as any; assert.ok(details.error.includes('reason')); }); -test('orderTest rejects a URL shorter than 12 characters', async () => { +test('orderNewTest rejects a URL shorter than 12 characters', async () => { const body = await response(['Test Page', 'short', 'A reason that is long enough.']); assert.ok((body['response content']['details about your order'] as any).error); }); -test('orderTest rejects a syntactically invalid URL with the correct length', async () => { +test('orderNewTest rejects a syntactically invalid URL with the correct length', async () => { const body = await response(['Test Page', 'not-a-valid-url', 'A reason that is long enough.']); const details = body['response content']['details about your order'] as any; assert.ok(details.error.includes('invalid URL')); }); -test('orderTest rejects a private-address URL unless internal targets are allowed', async () => { +test('orderNewTest rejects a private-address URL unless internal targets are allowed', async () => { delete process.env.ALLOW_INTERNAL_TARGETS; try { const body = await response(['Internal Page', 'https://192.168.1.1/page', 'A reason that is long enough.']); @@ -98,7 +98,7 @@ test('orderTest rejects a private-address URL unless internal targets are allowe } }); -test('orderTest rejects a URL that redirects to a disallowed target, and alerts a manager', async (t) => { +test('orderNewTest rejects a URL that redirects to a disallowed target, and alerts a manager', async (t) => { delete process.env.ALLOW_INTERNAL_TARGETS; t.mock.method(globalThis, 'fetch', async () => ({url: 'https://10.0.0.5/page'}) as Response); try { @@ -114,7 +114,7 @@ test('orderTest rejects a URL that redirects to a disallowed target, and alerts } }); -test('orderTest rejects an already-tested page', async () => { +test('orderNewTest rejects an already-tested page', async () => { const body = await response(['Mixed Outcomes Page', 'https://example.com/mixed', 'A reason that is long enough.']); const details = body['response content']['details about your order'] as any; assert.equal(details.error, undefined); @@ -126,7 +126,7 @@ test('orderTest rejects an already-tested page', async () => { assert.equal(disposition['report identifier'], null); }); -test('orderTest rejects a page matching a claimed job by description', async () => { +test('orderNewTest rejects a page matching a claimed job by description', async () => { const claimedPath = path.join(fixtureDBDir, 'jobs', 'claimed', 'clm.json'); await fs.writeFile(claimedPath, JSON.stringify({ target: {what: 'Claimed Test Page', url: 'https://example.com/claimed-job'} @@ -146,7 +146,7 @@ test('orderTest rejects a page matching a claimed job by description', async () } }); -test('orderTest rejects a page matching a queued job by URL', async () => { +test('orderNewTest rejects a page matching a queued job by URL', async () => { const queuedPath = path.join(fixtureDBDir, 'jobs', 'queue', 'que.json'); await fs.writeFile(queuedPath, JSON.stringify({ target: {what: 'Some Other Page', url: 'https://example.com/queued-url'} @@ -166,7 +166,7 @@ test('orderTest rejects a page matching a queued job by URL', async () => { } }); -test('orderTest rejects an order once the job queue is full', async () => { +test('orderNewTest rejects an order once the job queue is full', async () => { process.env.JOB_QUEUE_MAX = '0'; const fillerPaths = await Promise.all(Array.from({length: 1}, async (_, i) => { const fillerPath = path.join(fixtureDBDir, 'jobs', 'queue', `fil${i}.json`); @@ -187,7 +187,7 @@ test('orderTest rejects an order once the job queue is full', async () => { } }); -test('orderTest accepts a valid new page order and enqueues a job directly', async () => { +test('orderNewTest accepts a valid new page order and enqueues a job directly', async () => { const body = await response(['Brand New Ordered Page', 'https://example.com/brandnewordered', 'A reason that is long enough.']); const details = body['response content']['details about your order'] as any; assert.equal(details.error, undefined); @@ -205,7 +205,7 @@ test('orderTest accepts a valid new page order and enqueues a job directly', asy assert.equal(job.sources.reason, 'A reason that is long enough.'); }); -test('orderTest response omits a web UI URL for this request but includes the ancestor', async () => { +test('orderNewTest response omits a web UI URL for this request but includes the ancestor', async () => { const body = await response(['Brand New Ordered Page 2', 'https://example.com/brandnewordered2', 'A reason that is long enough.']); const similarWeb = body['URLs of similar requests for web users'] as any; assert.equal(similarWeb['this request'], null); diff --git a/api/orderTest.ts b/api/orderNewTest.ts similarity index 94% rename from api/orderTest.ts rename to api/orderNewTest.ts index 0c18f43c..f4d3b0fa 100644 --- a/api/orderTest.ts +++ b/api/orderNewTest.ts @@ -1,5 +1,5 @@ /* - orderTest.ts + orderNewTest.ts Processes an order to test an untested page immediately, without manual approval, and returns an acknowledgement. */ @@ -8,13 +8,13 @@ import {z} from 'zod'; import {getRequestFailureReason, getResponseMetadata, getThisHost, getToolsFacts} from './util.ts'; import {checkLength, isAllowedRedirectTarget, isAllowedTarget, isURL, orderJob} from '../util.ts'; -import {orderTestResponseSchema} from './schemas.ts'; +import {orderNewTestResponseSchema} from './schemas.ts'; import {sendAlert} from '../alerts.ts'; // TYPES // The response content defined by the response schema. -type ResponseContent = z.infer['response content']; +type ResponseContent = z.infer['response content']; // FUNCTIONS @@ -100,7 +100,7 @@ export const response = async (args: string[]) => { requestDisposition = { 'what happens next': `Your order will not be processed, because ${getRequestFailureReason( orderResult.result, - {result: 'retest', reason: 'a report about a page with the same description and URL is available.'} + {result: 'reportExists', reason: 'a report about a page with the same description and URL is available.'} )}`, 'report identifier': null, 'how you can check for completion': 'Not applicable.', @@ -116,11 +116,11 @@ export const response = async (args: string[]) => { // Create a response body. const body = { 'tool collection': getToolsFacts(), - 'tool name': 'orderTest', + 'tool name': 'orderNewTest', 'this request': { description: 'Process my order to test a page about which no report is available yet, immediately and without manual approval. I have provided a description and the URL of the page and a reason why it should be tested.', method: 'POST', - URL: `${thisHost}/api/orderTest`, + URL: `${thisHost}/api/orderNewTest`, body: { description, URL: url, diff --git a/api/requestTest.test.ts b/api/requestNewTest.test.ts similarity index 80% rename from api/requestTest.test.ts rename to api/requestNewTest.test.ts index 58752479..a6175e67 100644 --- a/api/requestTest.test.ts +++ b/api/requestNewTest.test.ts @@ -1,6 +1,6 @@ /* - requestTest.test.ts - Tests for api/requestTest.ts using the fixture corpus. + requestNewTest.test.ts + Tests for api/requestNewTest.ts using the fixture corpus. */ // IMPORTS @@ -11,7 +11,7 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import {fixtureDBDir} from '../test/dbFixture.ts'; -// Blank the alert configuration unconditionally, before ./requestTest.ts (whose +// Blank the alert configuration unconditionally, before ./requestNewTest.ts (whose // processTestRequest calls sendAlert) is ever imported below, so this file sends no // real alert emails even when run directly rather than via `npm test`. for (const key of ['MANAGER_EMAIL', 'ALERT_API_HOST', 'ALERT_API_PATH', 'ALERT_API_KEY', 'ALERT_FROM']) { @@ -41,7 +41,7 @@ beforeEach(async () => { console.log = (...args) => logged.push(args.join(' ')); }); -import {response} from './requestTest.ts'; +import {response} from './requestNewTest.ts'; after(() => { console.log = originalLog; @@ -55,45 +55,45 @@ after(() => { // TESTS -test('requestTest rejects an empty description', async () => { +test('requestNewTest rejects an empty description', async () => { const body = await response(['', 'https://example.com/test', 'A reason that is long enough.']); assert.ok((body['response content']['details about your request'] as any).error); - assert.ok(!logged.some(line => line.includes('new test request awaits approval'))); + assert.ok(!logged.some(line => line.includes('request for a new test awaits approval'))); }); -test('requestTest rejects a description longer than 100 characters', async () => { +test('requestNewTest rejects a description longer than 100 characters', async () => { const longWhat = 'x'.repeat(101); const body = await response([longWhat, 'https://example.com/test', 'A reason that is long enough.']); assert.ok((body['response content']['details about your request'] as any).error); - assert.ok(!logged.some(line => line.includes('new test request awaits approval'))); + assert.ok(!logged.some(line => line.includes('request for a new test awaits approval'))); }); -test('requestTest rejects a URL shorter than 12 characters', async () => { +test('requestNewTest rejects a URL shorter than 12 characters', async () => { const body = await response(['Test Page', 'short', 'A reason that is long enough.']); assert.ok((body['response content']['details about your request'] as any).error); - assert.ok(!logged.some(line => line.includes('new test request awaits approval'))); + assert.ok(!logged.some(line => line.includes('request for a new test awaits approval'))); }); -test('requestTest rejects a syntactically invalid URL with the correct length', async () => { +test('requestNewTest rejects a syntactically invalid URL with the correct length', async () => { const body = await response(['Test Page', 'not-a-valid-url', 'A reason that is long enough.']); const details = body['response content']['details about your request'] as any; assert.ok(details.error.includes('invalid URL')); - assert.ok(!logged.some(line => line.includes('new test request awaits approval'))); + assert.ok(!logged.some(line => line.includes('request for a new test awaits approval'))); }); -test('requestTest rejects a private-address URL unless internal targets are allowed', async () => { +test('requestNewTest rejects a private-address URL unless internal targets are allowed', async () => { delete process.env.ALLOW_INTERNAL_TARGETS; try { const body = await response(['Internal Page', 'https://192.168.1.1/page', 'A reason that is long enough.']); const details = body['response content']['details about your request'] as any; assert.ok(details.error.includes('does not resolve to a page that this deployment can test')); - assert.ok(!logged.some(line => line.includes('new test request awaits approval'))); + assert.ok(!logged.some(line => line.includes('request for a new test awaits approval'))); } finally { process.env.ALLOW_INTERNAL_TARGETS = 'true'; } }); -test('requestTest rejects a URL that redirects to a disallowed target, and alerts a manager', async (t) => { +test('requestNewTest rejects a URL that redirects to a disallowed target, and alerts a manager', async (t) => { delete process.env.ALLOW_INTERNAL_TARGETS; t.mock.method(globalThis, 'fetch', async () => ({url: 'https://10.0.0.5/page'}) as Response); try { @@ -109,21 +109,21 @@ test('requestTest rejects a URL that redirects to a disallowed target, and alert } }); -test('requestTest rejects an already-tested page', async () => { +test('requestNewTest rejects an already-tested page', async () => { const body = await response(['Mixed Outcomes Page', 'https://example.com/mixed', 'A reason that is long enough.']); const details = body['response content']['details about your request'] as any; // A report for this description and URL already exists, so processTestRequest - // returns 'retest' and the failure is reported via the disposition, not details.error. + // returns 'reportExists' and the failure is reported via the disposition, not details.error. assert.equal(details.error, undefined); const disposition = body['response content']['disposition of your request'] as any; assert.ok( disposition['what happens next'] .includes('a report about a page with the same description and URL is available.') ); - assert.ok(!logged.some(line => line.includes('new test request awaits approval'))); + assert.ok(!logged.some(line => line.includes('request for a new test awaits approval'))); }); -test('requestTest rejects a page matching a claimed job by description', async () => { +test('requestNewTest rejects a page matching a claimed job by description', async () => { const claimedPath = path.join(fixtureDBDir, 'jobs', 'claimed', 'clm.json'); await fs.writeFile(claimedPath, JSON.stringify({ target: {what: 'Claimed Test Page', url: 'https://example.com/claimed-job'} @@ -143,7 +143,7 @@ test('requestTest rejects a page matching a claimed job by description', async ( } }); -test('requestTest rejects a page matching a queued job by URL', async () => { +test('requestNewTest rejects a page matching a queued job by URL', async () => { const queuedPath = path.join(fixtureDBDir, 'jobs', 'queue', 'que.json'); await fs.writeFile(queuedPath, JSON.stringify({ target: {what: 'Some Other Page', url: 'https://example.com/queued-url'} @@ -163,7 +163,7 @@ test('requestTest rejects a page matching a queued job by URL', async () => { } }); -test('requestTest rejects a duplicate request', async () => { +test('requestNewTest rejects a duplicate request', async () => { await response(['Duplicate API Page', 'https://example.com/dup-api', 'A reason that is long enough.']); const body = await response(['Duplicate API Page', 'https://example.com/dup-api', 'A reason that is long enough.']); const details = body['response content']['details about your request'] as any; @@ -172,13 +172,13 @@ test('requestTest rejects a duplicate request', async () => { assert.ok(disposition['what happens next'].includes('an identical request is already awaiting approval.')); }); -test('requestTest accepts a valid new page request', async () => { +test('requestNewTest accepts a valid new page request', async () => { const body = await response(['Brand New Page', 'https://example.com/brandnew', 'A reason that is long enough.']); const details = body['response content']['details about your request'] as any; assert.equal(details.error, undefined); assert.equal(details['page to be tested'].description, 'Brand New Page'); assert.ok(logged.some(line => - line.startsWith('WARNING (Kilotest: new test request awaits approval)') + line.startsWith('WARNING (Kilotest: request for a new test awaits approval)') && line.includes('Brand New Page') && line.includes('https://example.com/brandnew') // The alert reports the resulting queue size, so a maintainer who has been away can @@ -187,14 +187,14 @@ test('requestTest accepts a valid new page request', async () => { )); }); -test('requestTest includes disposition information for a valid request', async () => { +test('requestNewTest includes disposition information for a valid request', async () => { const body = await response(['Brand New Page', 'https://example.com/brandnew', 'A reason that is long enough.']); const disposition = body['response content']['disposition of your request']; assert.ok(disposition); assert.ok(disposition['what happens next']); }); -test('requestTest rejects a request with a fallback channel when the queue is full', async () => { +test('requestNewTest rejects a request with a fallback channel when the queue is full', async () => { // Fill testRequests.json with 20 pending requests, all for one URL, to reach the cap. const filler = Array.from({length: 20}, (_, i) => ({ timeStamp: '260101T0000', description: `Filler Page ${i}`, reason: 'Filler reason.' diff --git a/api/requestTest.ts b/api/requestNewTest.ts similarity index 91% rename from api/requestTest.ts rename to api/requestNewTest.ts index aef12211..34be1686 100644 --- a/api/requestTest.ts +++ b/api/requestNewTest.ts @@ -1,5 +1,5 @@ /* - requestTest.ts + requestNewTest.ts Processes a request to test an untested page and returns an acknowledgement. */ @@ -8,13 +8,13 @@ import {z} from 'zod'; import {buildRequestDisposition, getResponseMetadata, getThisHost, getToolsFacts} from './util.ts'; import {checkLength, isAllowedRedirectTarget, isAllowedTarget, isURL, processTestRequest} from '../util.ts'; -import {requestTestResponseSchema} from './schemas.ts'; +import {requestNewTestResponseSchema} from './schemas.ts'; import {sendAlert} from '../alerts.ts'; // TYPES // The response content defined by the response schema. -type ResponseContent = z.infer['response content']; +type ResponseContent = z.infer['response content']; // FUNCTIONS @@ -84,7 +84,7 @@ export const response = async (args: string[]) => { howToCheck: 'You can call the listReports tool to learn whether the page has been tested and a report is available.', howWebUserChecks: `A web user can visit ${thisHost}/listReports.html to learn whether the page has been tested and a report is available.` }, - {result: 'retest', reason: 'a report about a page with the same description and URL is available.'} + {result: 'reportExists', reason: 'a report about a page with the same description and URL is available.'} ); } // Create the response content. @@ -95,11 +95,11 @@ export const response = async (args: string[]) => { // Create a response body. const body = { 'tool collection': getToolsFacts(), - 'tool name': 'requestTest', + 'tool name': 'requestNewTest', 'this request': { description: 'Process my request to test a page about which no report is available yet. I have provided a description and the URL of the page and a reason why it should be tested.', method: 'POST', - URL: `${thisHost}/api/requestTest`, + URL: `${thisHost}/api/requestNewTest`, body: { description, URL: url, @@ -113,7 +113,7 @@ export const response = async (args: string[]) => { } }, 'URLs of similar requests for web users': { - 'this request': `${thisHost}/requestTestForm.html`, + 'this request': `${thisHost}/requestNewTestForm.html`, 'closest ancestor request': `${thisHost}/listReports.html` }, 'response metadata': getResponseMetadata(), diff --git a/api/requestRetest.test.ts b/api/requestRetest.test.ts index 502915b9..afc7fffa 100644 --- a/api/requestRetest.test.ts +++ b/api/requestRetest.test.ts @@ -74,14 +74,14 @@ test('requestRetest rejects a nonexistent report', async () => { const body = await response(['251231T0000', 'zzz', 'A reason that is long enough.']); const details = body['response content']['details about your request'] as any; assert.ok(details.error.includes('does not exist')); - assert.ok(!logged.some(line => line.includes('new retest request awaits approval'))); + assert.ok(!logged.some(line => line.includes('request for a retest awaits approval'))); }); test('requestRetest rejects a malformed timestamp or job identifier', async () => { const body = await response(['999999T9999', 'xyz', 'A reason that is long enough.']); const details = body['response content']['details about your request'] as any; assert.ok(details.error.includes('malformed')); - assert.ok(!logged.some(line => line.includes('new retest request awaits approval'))); + assert.ok(!logged.some(line => line.includes('request for a retest awaits approval'))); }); test('requestRetest rejects a superseded report', async () => { @@ -95,14 +95,14 @@ test('requestRetest rejects a superseded report', async () => { assert.ok( disposition['what happens next'].includes('a later report about a page with the same description is available.') ); - assert.ok(!logged.some(line => line.includes('new retest request awaits approval'))); + assert.ok(!logged.some(line => line.includes('request for a retest awaits approval'))); }); test('requestRetest rejects a reason shorter than 20 characters', async () => { const body = await response(['260101T0003', 'ret', 'short']); const details = body['response content']['details about your request'] as any; assert.ok(details.error.includes('reason')); - assert.ok(!logged.some(line => line.includes('new retest request awaits approval'))); + assert.ok(!logged.some(line => line.includes('request for a retest awaits approval'))); }); test('requestRetest rejects a reason longer than 100 characters', async () => { @@ -110,7 +110,7 @@ test('requestRetest rejects a reason longer than 100 characters', async () => { const body = await response(['260101T0003', 'ret', longReason]); const details = body['response content']['details about your request'] as any; assert.ok(details.error.includes('reason')); - assert.ok(!logged.some(line => line.includes('new retest request awaits approval'))); + assert.ok(!logged.some(line => line.includes('request for a retest awaits approval'))); }); test('requestRetest rejects a report matching a claimed job by description', async () => { @@ -170,7 +170,7 @@ test('requestRetest accepts a valid retest request for the latest report of a pa assert.equal(details.error, undefined); assert.equal(details['page to be retested'].description, 'Mixed Outcomes Page'); assert.ok(logged.some(line => - line.startsWith('WARNING (Kilotest: new retest request awaits approval)') + line.startsWith('WARNING (Kilotest: request for a retest awaits approval)') && line.includes('Mixed Outcomes Page') && line.includes('https://example.com/mixed') // The alert reports the resulting queue size, so a maintainer who has been away can diff --git a/api/routes.test.ts b/api/routes.test.ts index 68a32423..4744a301 100644 --- a/api/routes.test.ts +++ b/api/routes.test.ts @@ -25,11 +25,11 @@ test('routes includes all expected operation IDs', () => { 'listIssues', 'listReports', 'listViolators', + 'orderNewTest', 'orderRetest', - 'orderTest', 'requestFeature', - 'requestRetest', - 'requestTest' + 'requestNewTest', + 'requestRetest' ]); }); diff --git a/api/routes.ts b/api/routes.ts index fcebf6f7..6e6d4a05 100644 --- a/api/routes.ts +++ b/api/routes.ts @@ -11,10 +11,10 @@ import { listViolatorsSchema, listDiagnosesSchema, getReportSchema, - requestTestSchema, + requestNewTestSchema, requestRetestSchema, requestFeatureSchema, - orderTestSchema, + orderNewTestSchema, orderRetestSchema, awaitTestSchema, listReportsResponseSchema, @@ -22,10 +22,10 @@ import { listViolatorsResponseSchema, listDiagnosesResponseSchema, getReportResponseSchema, - requestTestResponseSchema, + requestNewTestResponseSchema, requestRetestResponseSchema, requestFeatureResponseSchema, - orderTestResponseSchema, + orderNewTestResponseSchema, orderRetestResponseSchema, awaitTestResponseSchema } from './schemas.ts'; @@ -73,12 +73,12 @@ export const routes = [ responseSchema: getReportResponseSchema }, { - operationId: 'requestTest', + operationId: 'requestNewTest', method: 'post', - path: '/api/requestTest', + path: '/api/requestNewTest', summary: 'Request that a page be tested', - bodySchema: z.object(requestTestSchema), - responseSchema: requestTestResponseSchema + bodySchema: z.object(requestNewTestSchema), + responseSchema: requestNewTestResponseSchema }, { operationId: 'requestRetest', @@ -101,12 +101,12 @@ export const routes = [ responseSchema: requestFeatureResponseSchema }, { - operationId: 'orderTest', + operationId: 'orderNewTest', method: 'post', - path: '/api/orderTest', + path: '/api/orderNewTest', summary: 'Order that a page be tested immediately, without manual approval', - bodySchema: z.object(orderTestSchema), - responseSchema: orderTestResponseSchema + bodySchema: z.object(orderNewTestSchema), + responseSchema: orderNewTestResponseSchema }, { operationId: 'orderRetest', @@ -124,7 +124,7 @@ export const routes = [ operationId: 'awaitTest', method: 'post', path: '/api/awaitTest/{timeStamp}/{jobID}', - summary: 'Wait for a job ordered via orderTest or orderRetest to complete', + summary: 'Wait for a job ordered via orderNewTest or orderRetest to complete', pathParamsSchema: z.object(awaitTestSchema), // No caller-supplied fields beyond the path parameters; an empty body schema (rather // than omitting bodySchema) keeps this route consistent with the "every POST route diff --git a/api/schemas.ts b/api/schemas.ts index 02c59943..57de08cb 100644 --- a/api/schemas.ts +++ b/api/schemas.ts @@ -41,8 +41,8 @@ export const getReportSchema = { jobID: z.string().describe('Job identifier of the report (example: x9z)') }; -// requestTest: POST /api/requestTest -export const requestTestSchema = { +// requestNewTest: POST /api/requestNewTest +export const requestNewTestSchema = { description: z.string().describe('1- to 100-character description of the page conforming to the naming convention used in the listReports output'), URL: z.string().describe('12- to 300-character URL of the page, including the https:// scheme and any query'), reason: z.string().describe('20- to 100-character reason why the page should be tested') @@ -60,8 +60,8 @@ export const requestFeatureSchema = { feature: z.string().describe('20- to 1000-character description of requested feature improvement or new feature') }; -// orderTest: POST /api/orderTest -export const orderTestSchema = { +// orderNewTest: POST /api/orderNewTest +export const orderNewTestSchema = { description: z.string().describe('1- to 100-character description of the page conforming to the naming convention used in the listReports output'), URL: z.string().describe('12- to 300-character URL of the page, including the https:// scheme and any query'), reason: z.string().describe('20- to 100-character reason why the page should be tested') @@ -76,8 +76,8 @@ export const orderRetestSchema = { // awaitTest: POST /api/awaitTest export const awaitTestSchema = { - timeStamp: z.string().describe('Timestamp of the ordered job, from the report identifier returned by orderTest, in YYMMDDTHHmm format (example: 260503T0432)'), - jobID: z.string().describe('Job identifier of the ordered job, from the report identifier returned by orderTest (example: x9z)') + timeStamp: z.string().describe('Timestamp of the ordered job, from the report identifier returned by orderNewTest, in YYMMDDTHHmm format (example: 260503T0432)'), + jobID: z.string().describe('Job identifier of the ordered job, from the report identifier returned by orderNewTest (example: x9z)') }; // listReports takes no input; omitted (mcp.ts already uses inputSchema: {}). @@ -115,8 +115,8 @@ const toolsFactsSchema = z.object({ // Identifies a related request (e.g., the closest ancestor request in the drill-down // hierarchy). method is usually GET (every drill-down tool's ancestor is a read), except -// for orderTest/orderRetest, whose own ancestor is a GET, and awaitTest, whose ancestors -// (orderTest and orderRetest) are both POST (they create a job). +// for orderNewTest/orderRetest, whose own ancestor is a GET, and awaitTest, whose ancestors +// (orderNewTest and orderRetest) are both POST (they create a job). const requestReferenceSchema = z.object({ 'tool name': z.string(), description: z.string(), @@ -126,7 +126,7 @@ const requestReferenceSchema = z.object({ // GET endpoints have no 'body'; POST endpoints get a request-specific body schema. // ancestorSchema overrides the default single-ancestor shape for a tool whose closest -// ancestor is not unique: awaitTest can be reached from either orderTest or orderRetest, +// ancestor is not unique: awaitTest can be reached from either orderNewTest or orderRetest, // and the identifier alone does not say which, so it reports both as an array rather // than arbitrarily picking one. const thisRequestSchema = (method: string, bodySchema?: ZodTypeAny, ancestorSchema: ZodTypeAny = requestReferenceSchema.nullable()) => z.object({ @@ -296,7 +296,7 @@ export const getReportResponseSchema = envelope('GET', z.object({ 'full report': z.unknown().describe('The full raw Testaro/Testilo report JSON, copied verbatim, or an error object if it could not be retrieved.') })); -export const requestTestResponseSchema = envelope( +export const requestNewTestResponseSchema = envelope( 'POST', z.object({ 'details about your request': z.union([ @@ -313,7 +313,7 @@ export const requestTestResponseSchema = envelope( 'how a web user can check for completion': z.string() }).nullable() }), - z.object(requestTestSchema) + z.object(requestNewTestSchema) ); export const requestRetestResponseSchema = envelope( @@ -355,7 +355,7 @@ const reportIdentifierSchema = z.object({ jobID: z.string().describe('Job identifier to pass to listIssues once the report is ready.') }).meta({id: 'ReportIdentifier'}); -export const orderTestResponseSchema = envelope( +export const orderNewTestResponseSchema = envelope( 'POST', z.object({ 'details about your order': z.union([ @@ -373,7 +373,7 @@ export const orderTestResponseSchema = envelope( 'do you want to wait for completion': z.string() }).nullable() }), - z.object(orderTestSchema), + z.object(orderNewTestSchema), z.object({ 'this request': z.null(), 'closest ancestor request': z.string().nullable() @@ -425,7 +425,7 @@ export const awaitTestResponseSchema = envelope( 'this request': z.null(), 'closest ancestor request': z.null() }), - // awaitTest can be reached from either orderTest or orderRetest, and its own + // awaitTest can be reached from either orderNewTest or orderRetest, and its own // parameters (timeStamp/jobID) do not say which one produced them, so both possible // ancestors are reported, rather than arbitrarily naming just one as "the" ancestor. z.array(requestReferenceSchema).length(2) diff --git a/api/util.ts b/api/util.ts index f09771c2..db7696f0 100644 --- a/api/util.ts +++ b/api/util.ts @@ -59,7 +59,7 @@ export const getToolsFacts = () => ({ 'level 4': 'Use the listDiagnoses tool to get a list of diagnoses of how one element on one page exhibited one issue in one report.', }, 'how to generate more findings': { - 'new testing': 'If no report is available yet about a page, use the requestTest tool to request that it be tested.', + 'new testing': 'If no report is available yet about a page, use the requestNewTest tool to request that it be tested.', 'retesting': 'If the listIssues tool shows that the latest report about a page is obsolete, because the page has been revised or for another reason, use the requestRetest tool to request that the page be retested.', 'latency': 'Requests for testing and retesting are usually approved and fulfilled within one day.', '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.' @@ -72,7 +72,7 @@ export const getToolsFacts = () => ({ // processed, given the result processTestRequest returned. requestResult is always a // rejection reason here ('description', 'url', 'queueFull', or an unrecognized value // meaning 'duplicate'); callers pass their own extra branch for a requestResult value -// specific to their own request type ('retest' for requestTest, 'superseded' for +// specific to their own request type ('reportExists' for requestNewTest, 'superseded' for // requestRetest), since that is the one branch not shared between them. export const getRequestFailureReason = ( requestResult: Exclude, @@ -95,7 +95,7 @@ export const getRequestFailureReason = ( }; // Returns the disposition of a test/retest/instant-test request: an 'ok' disposition // describing how to check for completion, or a rejection disposition naming why the -// request was not processed. Shared by requestTest, requestRetest, and orderTest, +// request was not processed. Shared by requestNewTest, requestRetest, and orderNewTest, // whose only differences are the wording of the 3 completion-related sentences and, for // a rejection, the one extra requestResult branch getRequestFailureReason takes. export const buildRequestDisposition = ( diff --git a/api/version.ts b/api/version.ts index ad0abe5e..7c63a1c2 100644 --- a/api/version.ts +++ b/api/version.ts @@ -1,8 +1,12 @@ /* version.ts - Single source of truth for the version of the Kilotest API and MCP tool contract, consumed by mcp.ts and scripts/generate-openapi.ts. + Single source of truth for the version of the Kilotest API and MCP tool contract, consumed by mcp.ts and scripts/generate-openapi.ts. Re-exports package.json's version field verbatim, rather than hand-maintaining a separate literal here, so MCP and OpenAPI consumers see package.json's version exactly, including minor/patch bumps from changes that do not affect the API contract; this is a deliberate choice, not an oversight. */ +// IMPORTS + +import pkg from '../package.json' with {type: 'json'}; + // CONSTANTS -export const version = '2.1.0'; +export const version = pkg.version; diff --git a/docs/SERVICE.md b/docs/SERVICE.md index fb47b86a..8f6a2e44 100644 --- a/docs/SERVICE.md +++ b/docs/SERVICE.md @@ -270,7 +270,7 @@ kilotest.com { } @allowedPOST { method POST - path /mcp /requestRetest.html/* /requestTest.html /requestAction.html /reannotate.html /renewWCAG.html /ai0BalanceForm.html /deleteNotesForm.html /expungeReportsForm.html /hideReportForm.html /metrics.html /pruneReportsForm.html /rewindReportsForm.html /showHiddenReportsForm.html /unhideReportForm.html /api/* /tutorialWebComment.html /tutorialAIComment.html /worker/job /worker/report + path /mcp /requestRetest.html/* /requestNewTest.html /requestAction.html /reannotate.html /renewWCAG.html /ai0BalanceForm.html /deleteNotesForm.html /expungeReportsForm.html /hideReportForm.html /metrics.html /pruneReportsForm.html /rewindReportsForm.html /showHiddenReportsForm.html /unhideReportForm.html /api/* /tutorialWebComment.html /tutorialAIComment.html /worker/job /worker/report } # Respond to OPTIONS requests. @allowedOPTIONS method OPTIONS @@ -395,15 +395,13 @@ Alerts are sent to the maintainer in the following conditions: 5. **Requested full report unavailable**: When a user requests a full report via the API but the report cannot be retrieved (e.g., the report file is missing from disk), an alert is sent with subject `"Kilotest: requested full report unavailable"`, including details on which report was requested and why it could not be retrieved. -6. **Test request received via UI**: When a user submits a test request through the web interface, an alert is sent with subject `"Kilotest: new test request in the UI"` (via `/requestTest.html`) or `"Kilotest: new retest request in the UI"` (via `/requestRetest.html`), including the target URL, description, and the user's stated reason for the request. `test` and `retest` here are the only two literal values Kilotest passes as the request's type; they are hardcoded at each call site, not user-supplied. +6. **Request for a test or retest received**: When a user submits a request to test or retest a page, whether through the web interface (`/requestNewTest.html` or `/requestRetest.html`) or the API (the `requestNewTest` or `requestRetest` operation), an alert is sent with subject `"Kilotest: request for a new test awaits approval"` or `"Kilotest: request for a retest awaits approval"`, including the target URL, description, and the user's stated reason for the request. Both the UI and API paths funnel through the same underlying request-queuing function, so the subject and the rest of this behavior are identical regardless of which path the request arrived through. A request is treated as a duplicate, and no alert is sent for it, only if a request already pending in `db/jobs/testRequests.json`, i.e., submitted but not yet manually approved by the maintainer into an actual job, for the same URL has the identical description; this check ignores the stated reason and the test-versus-retest distinction, and is not limited to a recent time window, it compares against every request still pending approval for that URL. Once the maintainer approves a pending request (turning it into a job via `/enqueue.html`), all pending requests for that URL are cleared, so this check cannot detect a duplicate against a request that has already been approved, is currently running, or has already completed; an identical request submitted after approval alerts again. -7. **Test request received via API**: When a test request is submitted via the API, an alert is sent with subject `"Kilotest: new test request in the API"` (via the `requestTest` API operation) or `"Kilotest: new retest request in the API"` (via the `requestRetest` API operation), including the target URL, description, and the user's stated reason. A request is treated as a duplicate, and no alert is sent for it, only if a request already pending in `db/jobs/testRequests.json`, i.e., submitted but not yet manually approved by the maintainer into an actual job, for the same URL has the identical description; this check ignores the stated reason and the test-versus-retest distinction, and is not limited to a recent time window, it compares against every request still pending approval for that URL. Once the maintainer approves a pending request (turning it into a job via `/enqueue.html`), all pending requests for that URL are cleared, so this check cannot detect a duplicate against a request that has already been approved, is currently running, or has already completed; an identical request submitted after approval alerts again. The check and its dedup key are identical for the UI and API paths, since both funnel through the same underlying request-queuing function. +7. **MCP feature request received**: When a user submits a feature request via the MCP server's `requestFeature` tool, an alert is sent with subject `"MCP feature request received"`, including the text of the feature request, and the request is also appended to `db/featureRequests.json`, the same way tutorial comments are appended to their own files (see item 1), so that a manager can review or delete it later via `/deleteNotesForm.html`. -8. **MCP feature request received**: When a user submits a feature request via the MCP server's `requestFeature` tool, an alert is sent with subject `"MCP feature request received"`, including the text of the feature request, and the request is also appended to `db/featureRequests.json`, the same way tutorial comments are appended to their own files (see item 1), so that a manager can review or delete it later via `/deleteNotesForm.html`. +8. **WAVE balance low**: When Kilotest detects that the account balance for the WAVE API service (used for accessibility testing) has fallen below a threshold, an alert is sent with subject `"Kilotest: WAVE balance low"`, indicating the number of credits remaining. -9. **WAVE balance low**: When Kilotest detects that the account balance for the WAVE API service (used for accessibility testing) has fallen below a threshold, an alert is sent with subject `"Kilotest: WAVE balance low"`, indicating the number of credits remaining. - -10. **AI service balance low**: When Kilotest detects that the account balance for an AI service account (such as Anthropic) has fallen below a threshold, an alert is sent with subject `"Kilotest: AI service [N] balance low"`, indicating the approximate remaining balance in dollars and the estimated cost per job. +9. **AI service balance low**: When Kilotest detects that the account balance for an AI service account (such as Anthropic) has fallen below a threshold, an alert is sent with subject `"Kilotest: AI service [N] balance low"`, indicating the approximate remaining balance in dollars and the estimated cost per job. ### Alert configuration @@ -502,7 +500,7 @@ Kilotest records basic counts of how it is used, so the maintainer can answer qu - The generic `.html` GET dispatch in `index.ts`, once per successfully served web page, recorded under the `pageViews` category by page name (for example `tutorialWeb`, `listReports`), unless the page is a manager-only page (see "Manager page activity" below), in which case it is recorded there instead. - The home page (`/` and `/index.html`), served by its own branch in `index.ts` rather than through the generic `.html` dispatch, recorded under `pageViews` as `index`. -- Each of the 9 MCP tool handlers in `mcp.ts`, once per successful tool call, recorded under the `mcpToolCalls` category by tool name (for example `listReports`, `requestTest`). +- Each of the 9 MCP tool handlers in `mcp.ts`, once per successful tool call, recorded under the `mcpToolCalls` category by tool name (for example `listReports`, `requestNewTest`). - The `/api/*` GET and POST service branches in `index.ts`, once per call, recorded under the `apiOperations` category by operation name. `db/metrics.json` also stores a `since` time stamp, set when the file is first created, so the counts can be read as "since this date" rather than assumed to cover Kilotest's entire history. `getMetrics()` backfills any category absent from an existing `db/metrics.json` (for example one written before a category such as `managerActivity` existed), so an older file remains readable rather than causing every subsequent request to error. diff --git a/env.example b/env.example index f59506c4..033b80c4 100644 --- a/env.example +++ b/env.example @@ -49,10 +49,10 @@ TUTORIAL_AI_COMMENTS_MAX=__placeholder__ FEATURE_REQUESTS_MAX=__placeholder__ # Maximum number of jobs allowed in the job queue (db/jobs/queue) at once. Shared by every # path that enqueues a job: manager approval of a pending test/retest request, and the -# orderTest/orderRetest MCP tools, which enqueue directly. +# orderNewTest/orderRetest MCP tools, which enqueue directly. JOB_QUEUE_MAX=__placeholder__ -# Timing for the awaitTest MCP tool's wait on a job ordered via orderTest/orderRetest. +# Timing for the awaitTest MCP tool's wait on a job ordered via orderNewTest/orderRetest. # Defaults: 2000ms poll interval, 300000ms (5 minutes) maximum wait. The maximum wait # deliberately exceeds the "occasionally up to 4 minutes" callers are told to expect, to # leave margin so a job that is genuinely still running is not mistaken for one that diff --git a/index.test.ts b/index.test.ts index f53cd2de..8abf6b7d 100644 --- a/index.test.ts +++ b/index.test.ts @@ -24,7 +24,7 @@ process.env.TESTARO_WORKERS = JSON.stringify({ // isAllowedTarget performs. Tests of the check itself (below) override this per test. process.env.ALLOW_INTERNAL_TARGETS = 'true'; // Blank the alert configuration unconditionally, so this file sends no real alert -// emails (e.g. from the requestTest/requestRetest/tutorial-comment tests below) even +// emails (e.g. from the requestNewTest/requestRetest/tutorial-comment tests below) even // when run directly (e.g. `npx tsx --test index.test.ts`) rather than via `npm test`, // which normally guards against this by --require-ing test/setup.ts first. dotenv does // not override an already-set env var, so setting these here, before index.ts is ever @@ -459,8 +459,8 @@ test('GET /forbidden-path returns an invalid GET request error', async () => { // TESTS: POST routes -test('POST /requestTest.html with valid data returns an HTML page', async () => { - const res = await formRequest('POST', '/requestTest.html', { +test('POST /requestNewTest.html with valid data returns an HTML page', async () => { + const res = await formRequest('POST', '/requestNewTest.html', { description: `Unique Test Page ${uniqueStamp}`, url: `https://example.com/unique-${uniqueStamp}`, why: 'Because accessibility matters' @@ -469,8 +469,8 @@ test('POST /requestTest.html with valid data returns an HTML page', async () => assert.ok(res.headers['content-type'].includes('text/html')); }); -test('POST /requestTest.html with an already-tested URL returns an error', async () => { - const res = await formRequest('POST', '/requestTest.html', { +test('POST /requestNewTest.html with an already-tested URL returns an error', async () => { + const res = await formRequest('POST', '/requestNewTest.html', { description: 'Mixed Outcomes Page', url: 'https://example.com/mixed', why: 'Because accessibility matters' @@ -479,8 +479,8 @@ test('POST /requestTest.html with an already-tested URL returns an error', async assert.ok(res.body.includes('already been tested')); }); -test('POST /requestTest.html with invalid data returns an error', async () => { - const res = await formRequest('POST', '/requestTest.html', { +test('POST /requestNewTest.html with invalid data returns an error', async () => { + const res = await formRequest('POST', '/requestNewTest.html', { description: '', url: 'not-a-url', why: '' @@ -489,8 +489,8 @@ test('POST /requestTest.html with invalid data returns an error', async () => { assert.ok(res.body.includes('Invalid test request')); }); -test('POST /requestTest.html with an unreadable body returns an error', async () => { - const res = await request('POST', '/requestTest.html', 'rawbody', {'content-type': 'text/plain'}); +test('POST /requestNewTest.html with an unreadable body returns an error', async () => { + const res = await request('POST', '/requestNewTest.html', 'rawbody', {'content-type': 'text/plain'}); assert.equal(res.statusCode, 400); assert.ok(res.body.includes('Unreadable request body')); }); @@ -513,8 +513,8 @@ test('POST /requestRetest.html/invalid/invalid with invalid data returns an erro assert.ok(res.body.includes('Invalid retest request')); }); -test('POST /api/requestTest with valid JSON returns a JSON response', {timeout: 500}, async () => { - const res = await request('POST', '/api/requestTest', { +test('POST /api/requestNewTest with valid JSON returns a JSON response', {timeout: 500}, async () => { + const res = await request('POST', '/api/requestNewTest', { description: `API Test Page ${uniqueStamp}`, URL: `https://example.com/api-test-${uniqueStamp}`, reason: 'Because accessibility matters' @@ -544,9 +544,9 @@ test('POST /api/requestFeature with valid JSON returns a JSON response and recor assert.equal(await getMetricCount('apiOperations', 'requestFeature'), countBefore + 1); }); -test('POST /api/orderTest with valid JSON enqueues a job directly and records an API-operation metric', {timeout: 500}, async () => { - const countBefore = await getMetricCount('apiOperations', 'orderTest'); - const res = await request('POST', '/api/orderTest', { +test('POST /api/orderNewTest with valid JSON enqueues a job directly and records an API-operation metric', {timeout: 500}, async () => { + const countBefore = await getMetricCount('apiOperations', 'orderNewTest'); + const res = await request('POST', '/api/orderNewTest', { description: `API Ordered Page ${uniqueStamp}`, URL: `https://example.com/api-ordered-${uniqueStamp}`, reason: 'Because accessibility matters here' @@ -554,7 +554,7 @@ test('POST /api/orderTest with valid JSON enqueues a job directly and records an assert.equal(res.statusCode, 200); const body = jsonBody(res); assert.ok(body['tool name'] || body['response content']); - assert.equal(await getMetricCount('apiOperations', 'orderTest'), countBefore + 1); + assert.equal(await getMetricCount('apiOperations', 'orderNewTest'), countBefore + 1); const reportIdentifier = body['response content']['disposition of your order']['report identifier']; assert.ok(reportIdentifier); await fs.unlink(path.join(fixtureDBDir, 'jobs', 'queue', `${reportIdentifier.timeStamp}-${reportIdentifier.jobID}.json`)); @@ -736,7 +736,7 @@ test('POST /requestAction.html with invalid auth code returns an error and recor }); test('POST /requestAction.html with valid auth code and rejection (no description) returns HTML and records a managerActivity success', async () => { - await formRequest('POST', '/requestTest.html', { + await formRequest('POST', '/requestNewTest.html', { description: `Reject Test Page ${uniqueStamp}`, url: `https://example.com/reject-${uniqueStamp}`, why: 'Because accessibility matters' @@ -779,7 +779,7 @@ test('GET /fullReport.json/260101T0007/hid returns an error for an unavailable r test('POST /requestAction.html with valid auth code and approval returns HTML', {timeout: 500}, async () => { await fs.writeFile(testRequestsPath, '{}\n'); - await formRequest('POST', '/requestTest.html', { + await formRequest('POST', '/requestNewTest.html', { description: `Approval Test Page ${uniqueStamp}`, url: `https://example.com/approval-${uniqueStamp}`, why: 'Because accessibility matters' @@ -938,8 +938,40 @@ test('GET /targets.html redirects to /listReports.html', async () => { assert.equal(res.headers['location'], '/listReports.html'); }); -test('POST /requestTest.html with non-https URL returns an error', async () => { - const res = await formRequest('POST', '/requestTest.html', { +test('GET /requestTest.html redirects to /requestNewTest.html', async () => { + const res = await request('GET', '/requestTest.html', {}, {followRedirects: false}); + assert.equal(res.statusCode, 301); + assert.equal(res.headers['location'], '/requestNewTest.html'); +}); + +test('GET /requestTestForm.html redirects to /requestNewTestForm.html', async () => { + const res = await request('GET', '/requestTestForm.html', {}, {followRedirects: false}); + assert.equal(res.statusCode, 301); + assert.equal(res.headers['location'], '/requestNewTestForm.html'); +}); + +test('POST /api/requestTest redirects (308, preserving method) to /api/requestNewTest', async () => { + const res = await request('POST', '/api/requestTest', { + description: `Old Path Page ${uniqueStamp}`, + URL: `https://example.com/old-path-${uniqueStamp}`, + reason: 'Because accessibility matters' + }, {followRedirects: false}); + assert.equal(res.statusCode, 308); + assert.equal(res.headers['location'], '/api/requestNewTest'); +}); + +test('POST /api/orderTest redirects (308, preserving method) to /api/orderNewTest', async () => { + const res = await request('POST', '/api/orderTest', { + description: `Old Path Ordered Page ${uniqueStamp}`, + URL: `https://example.com/old-path-ordered-${uniqueStamp}`, + reason: 'Because accessibility matters' + }, {followRedirects: false}); + assert.equal(res.statusCode, 308); + assert.equal(res.headers['location'], '/api/orderNewTest'); +}); + +test('POST /requestNewTest.html with non-https URL returns an error', async () => { + const res = await formRequest('POST', '/requestNewTest.html', { description: 'Test Page', url: 'http://example.com', why: 'Because accessibility matters' @@ -948,10 +980,10 @@ test('POST /requestTest.html with non-https URL returns an error', async () => { assert.ok(res.body.includes('Invalid test request')); }); -test('POST /requestTest.html with a private-address URL returns an error unless internal targets are allowed', async () => { +test('POST /requestNewTest.html with a private-address URL returns an error unless internal targets are allowed', async () => { delete process.env.ALLOW_INTERNAL_TARGETS; try { - const res = await formRequest('POST', '/requestTest.html', { + const res = await formRequest('POST', '/requestNewTest.html', { description: 'Internal Page', url: 'https://192.168.1.1/page', why: 'Because accessibility matters' @@ -1409,14 +1441,14 @@ test('GET /style.css with a read error returns an error page', async () => { } }); -// requestTest.html and requestRetest.html are POST-only routes (see the +// requestNewTest.html and requestRetest.html are POST-only routes (see the // `routes.POST` list above). They also match the generic '*.html*' GET pattern, // but the generic `pageName.endsWith('.html')` branch of the GET dispatcher // excludes any path that is POST-only, since such a path's answer handler // expects POST's argument list and performs no GET-appropriate rendering. -test('GET /requestTest.html without arguments is rejected as an invalid GET request', async () => { - const res = await request('GET', '/requestTest.html'); +test('GET /requestNewTest.html without arguments is rejected as an invalid GET request', async () => { + const res = await request('GET', '/requestNewTest.html'); assert.equal(res.statusCode, 400); }); @@ -1770,18 +1802,18 @@ test('POST /mcp with a valid initialize request returns server info via SSE', as // TESTS: answer error branches -test('POST /requestTest.html with valid format but duplicate URL returns an answer error', {timeout: 500}, async () => { +test('POST /requestNewTest.html with valid format but duplicate URL returns an answer error', {timeout: 500}, async () => { await fs.writeFile(testRequestsPath, '{}\n'); // First request to create the request. processTestRequest treats a request as a // duplicate only when description, URL, AND reason all match an existing pending // request, so the reason must be identical between the two requests here. - await formRequest('POST', '/requestTest.html', { + await formRequest('POST', '/requestNewTest.html', { description: `Dup Test Page ${uniqueStamp}`, url: `https://example.com/dup-${uniqueStamp}`, why: 'Because accessibility matters' }); // Second, identical request should get a duplicate error. - const res = await formRequest('POST', '/requestTest.html', { + const res = await formRequest('POST', '/requestNewTest.html', { description: `Dup Test Page ${uniqueStamp}`, url: `https://example.com/dup-${uniqueStamp}`, why: 'Because accessibility matters' @@ -1809,7 +1841,7 @@ test('POST /requestRetest.html with valid format but duplicate retest returns an test('POST /requestAction.html with valid auth code approves the same target twice without error', {timeout: 500}, async () => { await fs.writeFile(testRequestsPath, '{}\n'); // Create a request. - await formRequest('POST', '/requestTest.html', { + await formRequest('POST', '/requestNewTest.html', { description: `Action Dup Page ${uniqueStamp}`, url: `https://example.com/action-dup-${uniqueStamp}`, why: 'Because accessibility matters' diff --git a/index.ts b/index.ts index 6413db58..e442008c 100644 --- a/index.ts +++ b/index.ts @@ -67,8 +67,8 @@ import {answer as renewWCAG} from './web/renewWCAG/index.ts'; import {answer as renewWCAGForm} from './web/renewWCAGForm/index.ts'; import {answer as requestRetestPage} from './web/requestRetest/index.ts'; import {answer as requestRetestForm} from './web/requestRetestForm/index.ts'; -import {answer as requestTestPage} from './web/requestTest/index.ts'; -import {answer as requestTestForm} from './web/requestTestForm/index.ts'; +import {answer as requestNewTestPage} from './web/requestNewTest/index.ts'; +import {answer as requestNewTestForm} from './web/requestNewTestForm/index.ts'; import {answer as rewindReportsForm} from './web/rewindReportsForm/index.ts'; import {answer as showHiddenReportsForm} from './web/showHiddenReportsForm/index.ts'; import {answer as unhideReportForm} from './web/unhideReportForm/index.ts'; @@ -79,8 +79,8 @@ import {response as listReportsAPI} from './api/listReports.ts'; import {response as listViolatorsAPI} from './api/listViolators.ts'; import {response as requestFeatureAPI} from './api/requestFeature.ts'; import {response as requestRetestAPI} from './api/requestRetest.ts'; -import {response as requestTestAPI} from './api/requestTest.ts'; -import {response as orderTestAPI} from './api/orderTest.ts'; +import {response as requestNewTestAPI} from './api/requestNewTest.ts'; +import {response as orderNewTestAPI} from './api/orderNewTest.ts'; import {response as orderRetestAPI} from './api/orderRetest.ts'; import {response as awaitTestAPI} from './api/awaitTest.ts'; @@ -127,8 +127,8 @@ const answer: { renewWCAGForm: PageHandler; requestRetest: PageHandler; requestRetestForm: PageHandler; - requestTest: PageHandler; - requestTestForm: PageHandler; + requestNewTest: PageHandler; + requestNewTestForm: PageHandler; rewindReportsForm: PageHandler; showHiddenReportsForm: PageHandler; unhideReportForm: PageHandler; @@ -157,8 +157,8 @@ const answer: { renewWCAGForm, requestRetest: requestRetestPage, requestRetestForm, - requestTest: requestTestPage, - requestTestForm, + requestNewTest: requestNewTestPage, + requestNewTestForm, rewindReportsForm, showHiddenReportsForm, unhideReportForm @@ -173,8 +173,8 @@ const apiRespond: { listViolators: ApiResponder; requestFeature: ApiResponder; requestRetest: ApiResponder; - requestTest: ApiResponder; - orderTest: ApiResponder; + requestNewTest: ApiResponder; + orderNewTest: ApiResponder; orderRetest: ApiResponder; awaitTest: ApiResponder; } = { @@ -185,8 +185,8 @@ const apiRespond: { listViolators: listViolatorsAPI, requestFeature: requestFeatureAPI, requestRetest: requestRetestAPI, - requestTest: requestTestAPI, - orderTest: orderTestAPI, + requestNewTest: requestNewTestAPI, + orderNewTest: orderNewTestAPI, orderRetest: orderRetestAPI, awaitTest: awaitTestAPI }; @@ -235,7 +235,7 @@ export const routes = { '/requestAction.html', '/renewWCAG.html', '/requestRetest.html/*', - '/requestTest.html', + '/requestNewTest.html', '/tutorialWebComment.html', '/unhideReportForm.html', '/worker/job', @@ -285,12 +285,18 @@ const noDirectGetPages = new Set(['unhideReportForm']); // engines and other crawlers are still requesting. Redirecting them lets crawlers update // their own indexes, rather than leaving them to repeat the same dead request indefinitely. // TEMPORARY: retire this redirect after 2027-04-01 once crawlers have re-indexed. +// requestTest and requestTestForm were renamed to requestNewTest and requestNewTestForm +// on 2026-09-28 (see the newTest/retest naming-ambiguity fix), to distinguish the +// new-test-only page from bare "test" language used elsewhere. +// TEMPORARY: retire this redirect after 2027-03-28 once crawlers have re-indexed. const renamedPagePrefixes: Record = { diagnoses: 'listDiagnoses', reportIssue: 'listViolators', reportIssues: 'listIssues', rules: 'listRules', - targets: 'listReports' + targets: 'listReports', + requestTest: 'requestNewTest', + requestTestForm: 'requestNewTestForm' }; const jobLock = createLock(); @@ -936,7 +942,7 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse) await serveError({message: 'ERROR: Unreadable request body'}, response, true); } // If the request is a test request: - else if (pageName === 'requestTest.html') { + else if (pageName === 'requestNewTest.html') { const {description, url, why} = postData as {description?: string; url: string; why?: string}; // If the request is valid: if (description && isURL(url) && why && await isAllowedTarget(url)) { @@ -950,7 +956,7 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse) // Serve headers for a response. setHeaders('text/html', pathname, 'ultra'); // Get the answer data. - const answerData = await answer.requestTest(description, url, why); + const answerData = await answer.requestNewTest(description, url, why); // If they are valid: if (answerData.status === 'ok') { // Serve the answer page. @@ -1235,15 +1241,26 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse) // Get the segments of the path after api. const segments = pathTail.split('/'); // If the service is to receive a test request: - if (segments[0] === 'requestTest') { + if (segments[0] === 'requestNewTest') { const {description, URL, reason} = postData as {description: string; URL: string; reason: string}; // Get the response body. - const responseBody = await apiRespond.requestTest([description, URL, reason]); - await recordPageMetric('apiOperations', 'requestTest'); + const responseBody = await apiRespond.requestNewTest([description, URL, reason]); + await recordPageMetric('apiOperations', 'requestNewTest'); // Send it. setHeaders('application/json', null, 'ultra'); response.end(JSON.stringify(responseBody)); } + // Otherwise, if the request uses the old, pre-rename requestTest path name: redirect + // permanently to the new path name. requestTest takes no path parameters (its + // arguments are body fields), so there is no further path to preserve, unlike the + // renamedPagePrefixes GET-page redirects above. 308 (not 301) is used here because + // 301 is defined to let clients rewrite a POST redirect as a GET, which would drop + // this request's body; 308 preserves both the method and the body. + // TEMPORARY: retire this redirect after 2027-03-28 once callers have updated. + else if (segments[0] === 'requestTest') { + response.writeHead(308, {Location: '/api/requestNewTest'}); + response.end(); + } // Otherwise, if the service is to receive a retest request: else if (segments[0] === 'requestRetest') { const {reason} = postData as {reason: string}; @@ -1265,15 +1282,24 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse) response.end(JSON.stringify(responseBody)); } // Otherwise, if the service is to order an immediate, automatically approved test: - else if (segments[0] === 'orderTest') { + else if (segments[0] === 'orderNewTest') { const {description, URL, reason} = postData as {description: string; URL: string; reason: string}; // Get the response body. - const responseBody = await apiRespond.orderTest([description, URL, reason]); - await recordPageMetric('apiOperations', 'orderTest'); + const responseBody = await apiRespond.orderNewTest([description, URL, reason]); + await recordPageMetric('apiOperations', 'orderNewTest'); // Send it. setHeaders('application/json', null, 'ultra'); response.end(JSON.stringify(responseBody)); } + // Otherwise, if the request uses the old, pre-rename orderTest path name: redirect + // permanently to the new path name. orderTest likewise takes no path parameters. + // See the requestTest redirect above for why 308 (not 301) is used for these + // POST-only API redirects. + // TEMPORARY: retire this redirect after 2027-03-28 once callers have updated. + else if (segments[0] === 'orderTest') { + response.writeHead(308, {Location: '/api/orderNewTest'}); + response.end(); + } // Otherwise, if the service is to order an immediate, automatically approved retest: else if (segments[0] === 'orderRetest') { const {reason} = postData as {reason: string}; diff --git a/lhm.plugin.json b/lhm.plugin.json index ab3077fc..30d71dbb 100644 --- a/lhm.plugin.json +++ b/lhm.plugin.json @@ -2297,7 +2297,7 @@ } }, { - "name": "requestTest", + "name": "requestNewTest", "description": "Process my request to test a page about which no report is available yet.", "inputSchema": { "type": "object", diff --git a/linkValidation.test.ts b/linkValidation.test.ts index 6169298a..f5a4cc13 100644 --- a/linkValidation.test.ts +++ b/linkValidation.test.ts @@ -48,8 +48,8 @@ const htmlRoutes = [ '/listViolators.html', '/listRules.html', '/manage.html', - '/requestTest.html', - '/requestTestForm.html', + '/requestNewTest.html', + '/requestNewTestForm.html', '/requestRetest.html', '/requestRetestForm.html', '/reannotate.html', diff --git a/llms-full.txt b/llms-full.txt index 120c8886..a82a03f6 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -63,7 +63,7 @@ Kilotest offers a comprehensive suite of capabilities to users via its web UI: - Get statistics about issues reported in one report: `https://kilotest.com/listIssues.html/{timeStamp}/{jobID}` - Get details about one issue reported in one report: `https://kilotest.com/listViolators.html/{issueID}/{timeStamp}/{jobID}` - Get diagnoses by rule engines of rule violations for one HTML element exhibiting one issue in one report: `https://kilotest.com/listDiagnoses.html/{issueID}/{timeStamp}/{jobID}/{catalogIndex}` -- [Recommend the testing of a not-yet-tested page](https://kilotest.com/requestTestForm.html) +- [Recommend the testing of a not-yet-tested page](https://kilotest.com/requestNewTestForm.html) - Recommend the retesting of a previously tested page: `https://kilotest.com/requestRetestForm.html/{timeStamp}/{jobID}` - [Get statistics about frequently reported issues across all pages](https://kilotest.com/listTopIssues.html) - [Get a list of the operations that Kilotest managers are authorized to request](https://kilotest.com/manage.html) @@ -108,10 +108,10 @@ Kilotest offers these tools: - `timeStamp`: initial segment of report identifier - `jobID`: final segment of report identifier - source of parameters: response to a `listViolators` request -- `requestTest` +- `requestNewTest` - method: `POST` - purpose: request testing of one web page about which no report is available - - path: `/api/requestTest` + - path: `/api/requestNewTest` - payload properties - `description`: description of the page to be tested - `URL`: URL of the page to be tested @@ -132,10 +132,10 @@ Kilotest offers these tools: - path: `/api/requestFeature` - payload properties - `feature`: description and motivation of the feature improvement or the new feature -- `orderTest` +- `orderNewTest` - method: `POST` - purpose: order immediate, automatically approved testing of one web page about which no report is available, instead of waiting for manual approval - - path: `/api/orderTest` + - path: `/api/orderNewTest` - payload properties - `description`: description of the page to be tested - `URL`: URL of the page to be tested @@ -154,12 +154,12 @@ Kilotest offers these tools: - response includes a report identifier (`timeStamp` and `jobID`) once the job is enqueued - `awaitTest` - method: `POST` - - purpose: wait for a job ordered via `orderTest` or `orderRetest` to complete; blocks until the report is ready, the job fails, or a maximum wait time elapses, then returns + - purpose: wait for a job ordered via `orderNewTest` or `orderRetest` to complete; blocks until the report is ready, the job fails, or a maximum wait time elapses, then returns - path: `/api/awaitTest/{timeStamp}/{jobID}` - parameters - - `timeStamp`: initial segment of the report identifier returned by `orderTest` or `orderRetest` - - `jobID`: final segment of the report identifier returned by `orderTest` or `orderRetest` - - source of parameters: response to an `orderTest` or `orderRetest` request + - `timeStamp`: initial segment of the report identifier returned by `orderNewTest` or `orderRetest` + - `jobID`: final segment of the report identifier returned by `orderNewTest` or `orderRetest` + - source of parameters: response to an `orderNewTest` or `orderRetest` request - caveat: testing typically takes about 2 minutes and occasionally up to 4 minutes; sends no interim notices while waiting ## After a test, retest, or feature request @@ -176,7 +176,7 @@ When you request a test or a retest: - check test completion by calling `listReports` and looking for the page and completion date in the list - you advise the user to check in approximately 1 day for the test results on the Kilotest website, starting at `https://kilotest.com/listReports.html` -For immediate testing without waiting for manual approval, use `orderTest` or `orderRetest` instead of `requestTest` or `requestRetest`. Each enqueues the job right away and returns a report identifier along with a question about whether to wait for completion. To wait, call `awaitTest` with that identifier; it blocks until the report is ready, the job fails, or a maximum wait time elapses. To check later instead, call `listIssues` with the same identifier. +For immediate testing without waiting for manual approval, use `orderNewTest` or `orderRetest` instead of `requestNewTest` or `requestRetest`. Each enqueues the job right away and returns a report identifier along with a question about whether to wait for completion. To wait, call `awaitTest` with that identifier; it blocks until the report is ready, the job fails, or a maximum wait time elapses. To check later instead, call `listIssues` with the same identifier. An [OpenAPI specification for Kilotest](https://kilotest.com/openapi.yaml) is available. diff --git a/llms.txt b/llms.txt index 4e2481d9..b203892e 100644 --- a/llms.txt +++ b/llms.txt @@ -9,12 +9,12 @@ Kilotest helps language models and AI platforms to give thorough, accurate and t - List all elements exhibiting one issue in one report: `/api/listViolators/{issueID}/{timeStamp}/{jobID}` (`GET`) - List all diagnoses of one element exhibiting one issue in one report: `/api/listDiagnoses/{catalogIndex}/{issueID}/{timeStamp}/{jobID}` (`GET`) - Get one full report in JSON: `/api/getReport/{timeStamp}/{jobID}` (`GET`) -- Request testing of a web page: `/api/requestTest` (`POST`) +- Request testing of a web page: `/api/requestNewTest` (`POST`) - Request retesting of a web page: `/api/requestRetest/{timeStamp}/{jobID}` (`POST`) - Request adding or improving a feature: `/api/requestFeature` (`POST`) -- Order immediate, automatically approved testing of a web page: `/api/orderTest` (`POST`) +- Order immediate, automatically approved testing of a web page: `/api/orderNewTest` (`POST`) - Order immediate, automatically approved retesting of a web page: `/api/orderRetest/{timeStamp}/{jobID}` (`POST`) -- Wait for a job ordered via orderTest or orderRetest to complete: `/api/awaitTest/{timeStamp}/{jobID}` (`POST`) +- Wait for a job ordered via orderNewTest or orderRetest to complete: `/api/awaitTest/{timeStamp}/{jobID}` (`POST`) ## MCP Server @@ -27,7 +27,7 @@ Kilotest helps language models and AI platforms to give thorough, accurate and t - List all elements exhibiting one issue in one report: `https://kilotest.com/listViolators.html/{issueID}/{timeStamp}/{jobID}` - List all diagnoses of one element exhibiting one issue in one report: `https://kilotest.com/listDiagnoses.html/{issueID}/{timeStamp}/{jobID}/{catalogIndex}` - Get one full report in JSON: `https://kilotest.com/fullReport.json/{timeStamp}/{jobID}` -- [Request testing of a web page](https://kilotest.com/requestTestForm.html) +- [Request testing of a web page](https://kilotest.com/requestNewTestForm.html) - Request retesting of a web page: `https://kilotest.com/requestRetestForm.html/{timeStamp}/{jobID}` - [List the operations that Kilotest managers are authorized to request](https://kilotest.com/manage.html) - [Provide statistics about frequently reported issues across all pages](https://kilotest.com/listTopIssues.html) diff --git a/mcp.test.ts b/mcp.test.ts index 3a9fadd5..787c3869 100644 --- a/mcp.test.ts +++ b/mcp.test.ts @@ -48,10 +48,10 @@ test('createMCPServer registers all 12 tools', () => { 'listViolators', 'listDiagnoses', 'getReport', - 'requestTest', + 'requestNewTest', 'requestRetest', 'requestFeature', - 'orderTest', + 'orderNewTest', 'orderRetest', 'awaitTest' ]); @@ -162,10 +162,10 @@ test('getReport handler returns content and structuredContent for a valid report assert.equal(getToolCallCount('getReport'), countBefore + 1); }); -test('requestTest handler returns content and structuredContent', async () => { +test('requestNewTest handler returns content and structuredContent', async () => { const server = createMCPServer(); - const countBefore = getToolCallCount('requestTest'); - const result = await (server as any)._registeredTools.requestTest.handler({ + const countBefore = getToolCallCount('requestNewTest'); + const result = await (server as any)._registeredTools.requestNewTest.handler({ description: 'Test Page', URL: 'https://example.com/test', reason: 'Because accessibility matters' @@ -173,7 +173,7 @@ test('requestTest handler returns content and structuredContent', async () => { assert.ok(result.content); assert.equal(result.content[0].type, 'text'); assert.ok(result.structuredContent); - assert.equal(getToolCallCount('requestTest'), countBefore + 1); + assert.equal(getToolCallCount('requestNewTest'), countBefore + 1); }); test('requestRetest handler returns content and structuredContent for a valid report', async () => { @@ -202,11 +202,11 @@ test('requestFeature handler returns content and structuredContent', async () => assert.equal(getToolCallCount('requestFeature'), countBefore + 1); }); -test('orderTest handler returns content and structuredContent, and enqueues a job', async () => { +test('orderNewTest handler returns content and structuredContent, and enqueues a job', async () => { const {jobsPath} = await import('./util.ts'); const server = createMCPServer(); - const countBefore = getToolCallCount('orderTest'); - const result = await (server as any)._registeredTools.orderTest.handler({ + const countBefore = getToolCallCount('orderNewTest'); + const result = await (server as any)._registeredTools.orderNewTest.handler({ description: 'MCP Ordered Page', URL: 'https://example.com/mcp-ordered', reason: 'Because accessibility matters here' @@ -214,7 +214,7 @@ test('orderTest handler returns content and structuredContent, and enqueues a jo assert.ok(result.content); assert.equal(result.content[0].type, 'text'); assert.ok(result.structuredContent); - assert.equal(getToolCallCount('orderTest'), countBefore + 1); + assert.equal(getToolCallCount('orderNewTest'), countBefore + 1); const reportIdentifier = result.structuredContent['response content']['disposition of your order']['report identifier']; assert.ok(reportIdentifier); const queuedPath = `${jobsPath()}/queue/${reportIdentifier.timeStamp}-${reportIdentifier.jobID}.json`; @@ -374,10 +374,10 @@ test('handleMCP lists all 12 tools via tools/list', async () => { 'listViolators', 'listDiagnoses', 'getReport', - 'requestTest', + 'requestNewTest', 'requestRetest', 'requestFeature', - 'orderTest', + 'orderNewTest', 'orderRetest', 'awaitTest' ]); diff --git a/mcp.ts b/mcp.ts index bdeb0404..e5dab614 100644 --- a/mcp.ts +++ b/mcp.ts @@ -13,10 +13,10 @@ import * as listReportsAPI from './api/listReports.ts'; import * as listIssuesAPI from './api/listIssues.ts'; import * as listViolatorsAPI from './api/listViolators.ts'; import * as listDiagnosesAPI from './api/listDiagnoses.ts'; -import * as requestTestAPI from './api/requestTest.ts'; +import * as requestNewTestAPI from './api/requestNewTest.ts'; import * as requestRetestAPI from './api/requestRetest.ts'; import * as requestFeatureAPI from './api/requestFeature.ts'; -import * as orderTestAPI from './api/orderTest.ts'; +import * as orderNewTestAPI from './api/orderNewTest.ts'; import * as orderRetestAPI from './api/orderRetest.ts'; import * as awaitTestAPI from './api/awaitTest.ts'; import {version} from './api/version.ts'; @@ -27,10 +27,10 @@ import { listIssuesSchema, listViolatorsSchema, listDiagnosesSchema, - requestTestSchema, + requestNewTestSchema, requestRetestSchema, requestFeatureSchema, - orderTestSchema, + orderNewTestSchema, orderRetestSchema, awaitTestSchema, listReportsResponseSchema, @@ -38,10 +38,10 @@ import { listViolatorsResponseSchema, listDiagnosesResponseSchema, getReportResponseSchema, - requestTestResponseSchema, + requestNewTestResponseSchema, requestRetestResponseSchema, requestFeatureResponseSchema, - orderTestResponseSchema, + orderNewTestResponseSchema, orderRetestResponseSchema, awaitTestResponseSchema } from './api/schemas.ts'; @@ -62,8 +62,8 @@ const kilotestOverview = [ 'Kilotest tests web pages for front-end quality: accessibility, usability, and standards conformity.', 'Kilotest integrates an ensemble of twelve independent rule engines to test a web page and stores the results as a structured report.', 'Results are organized as a hierarchy. A report contains issues. An issue has violators: elements of the tested page reported as exhibiting the issue. A violator has diagnoses: explanations from rule engines of how the element exhibited the issue.', - 'Typical workflow: call the listReports tool to learn whether a report about the page already exists. If it does, drill down with the listIssues, listViolators, and listDiagnoses tools, or retrieve the entire report with the getReport tool. If no report exists, request a test with the requestTest tool. If the latest report is obsolete, request a retest with the requestRetest tool.', - 'For immediate, automatically approved testing, use the orderTest tool (for a new page) or the orderRetest tool (for a page with an obsolete report) instead of requestTest or requestRetest. Each enqueues the job right away, with no manual approval step, and returns a report identifier along with a question about whether to wait for completion. To wait, call the awaitTest tool with that identifier; it blocks until the report is ready, the job fails, or a maximum wait time elapses. To check later instead, call the listIssues tool with the same identifier.', + 'Typical workflow: call the listReports tool to learn whether a report about the page already exists. If it does, drill down with the listIssues, listViolators, and listDiagnoses tools, or retrieve the entire report with the getReport tool. If no report exists, request a test with the requestNewTest tool. If the latest report is obsolete, request a retest with the requestRetest tool.', + 'For immediate, automatically approved testing, use the orderNewTest tool (for a new page) or the orderRetest tool (for a page with an obsolete report) instead of requestNewTest or requestRetest. Each enqueues the job right away, with no manual approval step, and returns a report identifier along with a question about whether to wait for completion. To wait, call the awaitTest tool with that identifier; it blocks until the report is ready, the job fails, or a maximum wait time elapses. To check later instead, call the listIssues tool with the same identifier.', 'More documentation is available at https://kilotest.com/llms.txt and https://kilotest.com/llms-full.txt.' ].join('\n\n'); @@ -81,7 +81,7 @@ export const createMCPServer = (): McpServer => { description: 'Tools that test web pages for front-end quality (accessibility, usability, and standards conformity) and make test results available' }, { - instructions: `${sharedContext} Use the listReports tool to start. If it shows that there is a report available about the page you want facts about, drill down with the listIssues, listViolators, and listDiagnoses tools. If not, use the requestTest tool to request that the page be tested, or the orderTest tool for immediate, automatically approved testing. If the latest report about the page is obsolete, use the requestRetest tool to request that the page be retested, or the orderRetest tool for immediate, automatically approved retesting. For detailed documentation, call the getKilotestOverview tool.` + instructions: `${sharedContext} Use the listReports tool to start. If it shows that there is a report available about the page you want facts about, drill down with the listIssues, listViolators, and listDiagnoses tools. If not, use the requestNewTest tool to request that the page be tested, or the orderNewTest tool for immediate, automatically approved testing. If the latest report about the page is obsolete, use the requestRetest tool to request that the page be retested, or the orderRetest tool for immediate, automatically approved retesting. For detailed documentation, call the getKilotestOverview tool.` }); server.registerTool( 'getKilotestOverview', @@ -202,11 +202,11 @@ export const createMCPServer = (): McpServer => { } ); server.registerTool( - 'requestTest', + 'requestNewTest', { description: toolDoc('Process my request to test a page about which no report is available yet.'), - inputSchema: requestTestSchema, - outputSchema: requestTestResponseSchema, + inputSchema: requestNewTestSchema, + outputSchema: requestNewTestResponseSchema, annotations: { title: toolDoc('Process my request to test a page about which no report is available yet.'), readOnlyHint: false, @@ -216,8 +216,8 @@ export const createMCPServer = (): McpServer => { } }, async ({description, URL, reason}) => { - const result = await requestTestAPI.response([description, URL, reason]); - await recordMetric('mcpToolCalls', 'requestTest'); + const result = await requestNewTestAPI.response([description, URL, reason]); + await recordMetric('mcpToolCalls', 'requestNewTest'); return {content: [{type: 'text', text: JSON.stringify(result)}], structuredContent: result}; } ); @@ -262,11 +262,11 @@ export const createMCPServer = (): McpServer => { } ); server.registerTool( - 'orderTest', + 'orderNewTest', { description: toolDoc('Process my order to test a page about which no report is available yet, immediately and automatically approved, instead of waiting for manual approval. Testing typically takes about 2 minutes and occasionally up to 4 minutes.'), - inputSchema: orderTestSchema, - outputSchema: orderTestResponseSchema, + inputSchema: orderNewTestSchema, + outputSchema: orderNewTestResponseSchema, annotations: { title: toolDoc('Process my order to test a page about which no report is available yet, immediately and automatically approved.'), readOnlyHint: false, @@ -276,8 +276,8 @@ export const createMCPServer = (): McpServer => { } }, async ({description, URL, reason}) => { - const result = await orderTestAPI.response([description, URL, reason]); - await recordMetric('mcpToolCalls', 'orderTest'); + const result = await orderNewTestAPI.response([description, URL, reason]); + await recordMetric('mcpToolCalls', 'orderNewTest'); return {content: [{type: 'text', text: JSON.stringify(result)}], structuredContent: result}; } ); @@ -304,11 +304,11 @@ export const createMCPServer = (): McpServer => { server.registerTool( 'awaitTest', { - description: toolDoc('Wait for a job ordered via the orderTest or orderRetest tool to complete, using the report identifier either tool returned. Blocks until the report is ready, the job fails, or a maximum wait time elapses; sends no interim notices.'), + description: toolDoc('Wait for a job ordered via the orderNewTest or orderRetest tool to complete, using the report identifier either tool returned. Blocks until the report is ready, the job fails, or a maximum wait time elapses; sends no interim notices.'), inputSchema: awaitTestSchema, outputSchema: awaitTestResponseSchema, annotations: { - title: toolDoc('Wait for a job ordered via the orderTest or orderRetest tool to complete.'), + title: toolDoc('Wait for a job ordered via the orderNewTest or orderRetest tool to complete.'), readOnlyHint: true, idempotentHint: false, destructiveHint: false, diff --git a/openapi.yaml b/openapi.yaml index df5e1f02..4d985c1f 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2,7 +2,7 @@ openapi: 3.1.0 info: title: Kilotest API description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 12 rule engines to test web pages for front-end quality (accessibility, usability, and standards conformity). Kilotest acts as a web server for human users, an MCP server for AI platforms, and an API for programmatic access. With this API you can request that a page be tested or retested, discover available reports, incrementally retrieve facts from a report: from what reports are available, to what issues were reported in one report, to what elements violated one issue, to what diagnoses rule engines gave for one violation.' - version: 2.1.0 + version: 57.0.0 license: name: MIT identifier: MIT @@ -810,9 +810,9 @@ paths: - response metadata - response content additionalProperties: false - /api/requestTest: + /api/requestNewTest: post: - operationId: requestTest + operationId: requestNewTest summary: Request that a page be tested requestBody: content: @@ -1181,9 +1181,9 @@ paths: - response metadata - response content additionalProperties: false - /api/orderTest: + /api/orderNewTest: post: - operationId: orderTest + operationId: orderNewTest summary: Order that a page be tested immediately, without manual approval requestBody: content: @@ -1493,22 +1493,22 @@ paths: /api/awaitTest/{timeStamp}/{jobID}: post: operationId: awaitTest - summary: Wait for a job ordered via orderTest or orderRetest to complete + summary: Wait for a job ordered via orderNewTest or orderRetest to complete parameters: - in: path name: timeStamp schema: type: string - description: 'Timestamp of the ordered job, from the report identifier returned by orderTest, in YYMMDDTHHmm format (example: 260503T0432)' + description: 'Timestamp of the ordered job, from the report identifier returned by orderNewTest, in YYMMDDTHHmm format (example: 260503T0432)' required: true - description: 'Timestamp of the ordered job, from the report identifier returned by orderTest, in YYMMDDTHHmm format (example: 260503T0432)' + description: 'Timestamp of the ordered job, from the report identifier returned by orderNewTest, in YYMMDDTHHmm format (example: 260503T0432)' - in: path name: jobID schema: type: string - description: 'Job identifier of the ordered job, from the report identifier returned by orderTest (example: x9z)' + description: 'Job identifier of the ordered job, from the report identifier returned by orderNewTest (example: x9z)' required: true - description: 'Job identifier of the ordered job, from the report identifier returned by orderTest (example: x9z)' + description: 'Job identifier of the ordered job, from the report identifier returned by orderNewTest (example: x9z)' requestBody: content: application/json: @@ -1517,7 +1517,7 @@ paths: properties: {} responses: '200': - description: Wait for a job ordered via orderTest or orderRetest to complete + description: Wait for a job ordered via orderNewTest or orderRetest to complete content: application/json: schema: diff --git a/package.json b/package.json index b1aa49f0..d7340d67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jrpool/kilotest", - "version": "56.0.0", + "version": "57.0.0", "description": "Ensemble testing for front-end web quality", "type": "module", "main": "index.ts", diff --git a/routes.test.ts b/routes.test.ts index 0bc4ac2f..92ac3cbd 100644 --- a/routes.test.ts +++ b/routes.test.ts @@ -42,7 +42,7 @@ test('routes GET includes all known GET paths', () => { test('routes POST includes all known POST paths', () => { const required = [ '/mcp', - '/requestTest.html', + '/requestNewTest.html', '/requestAction.html', '/reannotate.html', '/renewWCAG.html', diff --git a/routing.test.ts b/routing.test.ts index 93bf8cf2..958354c2 100644 --- a/routing.test.ts +++ b/routing.test.ts @@ -62,8 +62,8 @@ const get = (requestPath: string): Promise => new Promise((resolve, reject) // TESTS -test('GET /api/requestTest returns an error, not a test-request response', async () => { - const result = await get('/api/requestTest'); +test('GET /api/requestNewTest returns an error, not a test-request response', async () => { + const result = await get('/api/requestNewTest'); assert.ok(result.body.includes('Invalid service request')); }); diff --git a/server.json b/server.json index b27bbaed..f5fb1762 100644 --- a/server.json +++ b/server.json @@ -6,7 +6,7 @@ "url": "https://github.com/jrpool/kilotest", "source": "github" }, - "version": "2.1.0", + "version": "57.0.0", "remotes": [ { "type": "streamable-http", diff --git a/sitemap.xml b/sitemap.xml index f9164b74..6836cb21 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -3,7 +3,7 @@ https://kilotest.com/ https://kilotest.com/listReports.html https://kilotest.com/listTopIssues.html - https://kilotest.com/requestTestForm.html + https://kilotest.com/requestNewTestForm.html https://kilotest.com/manage.html https://kilotest.com/tutorial.html https://kilotest.com/openapi.yaml diff --git a/smokeTest.ts b/smokeTest.ts index 29dd6470..811631c6 100644 --- a/smokeTest.ts +++ b/smokeTest.ts @@ -51,7 +51,7 @@ const concretePaths: Record> = { '/requestAction.html': '/requestAction.html', '/renewWCAG.html': '/renewWCAG.html', '/requestRetest.html/*': '/requestRetest.html/260101T0001/ct', - '/requestTest.html': '/requestTest.html', + '/requestNewTest.html': '/requestNewTest.html', '/showHiddenReportsForm.html': '/showHiddenReportsForm.html', '/tutorialAIComment.html': '/tutorialAIComment.html', '/tutorialWebComment.html': '/tutorialWebComment.html', @@ -77,7 +77,7 @@ const postBodies: Record = { }, '/renewWCAG.html': {authCode: 'invalid'}, '/requestRetest.html/260101T0001/ct': {why: 'smoke test'}, - '/requestTest.html': {description: 'Smoke Test Page', url: 'https://smoketest.example.com', why: 'smoke test'}, + '/requestNewTest.html': {description: 'Smoke Test Page', url: 'https://smoketest.example.com', why: 'smoke test'}, '/showHiddenReportsForm.html': {authCode: 'invalid'}, '/tutorialAIComment.html': {content: 'smoke test'}, '/unhideReportForm.html': {authCode: 'invalid'}, diff --git a/util.test.ts b/util.test.ts index e841786e..343cfe28 100644 --- a/util.test.ts +++ b/util.test.ts @@ -890,12 +890,12 @@ test('processTestRequest returns a duplicate error for a repeated request', {tim await fs.writeFile(testRequestsPath(), '{}\n'); }); -test('processTestRequest returns a retest error when a report already exists for the page', async () => { +test('processTestRequest returns a reportExists error when a report already exists for the page', async () => { // The fixture database already has a report for this description and URL. const {result} = await processTestRequest( 'because accessibility', {description: 'Mixed Outcomes Page', url: 'https://example.com/mixed'} ); - assert.equal(result, 'retest'); + assert.equal(result, 'reportExists'); }); test('processTestRequest returns a queueFull error once the pending-request queue is full', {timeout: 500}, async () => { @@ -1880,11 +1880,11 @@ test('orderJob enqueues a new-page order directly into the job queue', async () } }); -test('orderJob returns a retest error when a report already exists for the ordered page', async () => { +test('orderJob returns a reportExists error when a report already exists for the ordered page', async () => { const {result} = await orderJob( {description: 'Mixed Outcomes Page', url: 'https://example.com/mixed'}, 'because accessibility matters here' ); - assert.equal(result, 'retest'); + assert.equal(result, 'reportExists'); }); test('orderJob returns "description" for a page matching a claimed job by description', async () => { diff --git a/util.ts b/util.ts index a92623af..e9e62f73 100644 --- a/util.ts +++ b/util.ts @@ -78,9 +78,13 @@ export type TestRequest = { // Test requests by URL. export type TestRequests = Record; -// Test request addition result. +// Test request addition result. 'reportExists' means a report already exists for a +// page a newTest request or order named (an outcome), distinct from the 'retest' +// requestType discriminant elsewhere in this file, which means the caller asked to +// retest a page that already has a report (a request kind); the two must not be +// confused with one another. export type TestRequestResult -= 'url' | 'description' | 'retest' | 'duplicate' | 'superseded' | 'nonreport' | 'queueFull' | 'ok'; += 'url' | 'description' | 'reportExists' | 'duplicate' | 'superseded' | 'nonreport' | 'queueFull' | 'ok'; // Target of a new-test or retest request: a description and URL for a new-test request, // or the timeStamp and jobID of the cited report for a retest request. @@ -1127,7 +1131,7 @@ export const processTestRequest = ( reason: string, target: TestRequestTarget ): Promise => testRequestsLock(async (): Promise => { - let requestType: 'test' | 'retest'; + let requestType: 'newTest' | 'retest'; let description: string; let url: string; // Whether the cited report (for a retest) has been superseded by a later one. @@ -1152,7 +1156,7 @@ export const processTestRequest = ( } // Otherwise, i.e. if the target is a page to test for the first time: else { - requestType = 'test'; + requestType = 'newTest'; ({description, url} = target); } try { @@ -1186,11 +1190,11 @@ export const processTestRequest = ( return {result: 'superseded', description, url}; } // Otherwise, if the request is to test a new page for which a report already exists: - if (requestType === 'test') { + if (requestType === 'newTest') { const reportExtracts = await getReportExtracts(); if (reportExtracts.some(report => report.description === description && report.url === url)) { // Return this. - return {result: 'retest', description, url}; + return {result: 'reportExists', description, url}; } } // Otherwise, i.e. if the request is genuinely new, if the queue of requests awaiting @@ -1215,8 +1219,9 @@ export const processTestRequest = ( // Alert a manager, including the resulting queue size, so a maintainer who has been // away sees at a glance how urgently the queue needs review (approval or rejection) // rather than learning this only once it is already full. + const requestTypeLabel = requestType === 'newTest' ? 'a new test' : 'a retest'; await sendAlert( - `Kilotest: new ${requestType} request awaits approval`, + `Kilotest: request for ${requestTypeLabel} awaits approval`, `Page description: ${description}\nURL: ${url}\nReason: ${plainReason}\n` + `Requests now awaiting approval: ${getPendingTestRequestCount(testRequests)} of ${describeMax(queueMax)}` ); @@ -1230,7 +1235,7 @@ export const processTestRequest = ( }); // Maximum number of jobs allowed in the job queue (db/jobs/queue) at once. This queue is // shared by every path that enqueues a job: manual approval of a pending test/retest -// request (web/enqueue), and any tool (such as orderTest) that enqueues directly. The cap +// request (web/enqueue), and any tool (such as orderNewTest) that enqueues directly. The cap // therefore protects the queue itself, not any one source of jobs; a value of 0 means no // limit. Configurable via JOB_QUEUE_MAX. const jobQueueMax = () => getEnvMax('JOB_QUEUE_MAX', 20); @@ -1308,7 +1313,7 @@ export const orderJob = ( const reportExtracts = await getReportExtracts(); if (reportExtracts.some(report => report.description === description && report.url === url)) { // Return this. - return {result: 'retest', description, url}; + return {result: 'reportExists', description, url}; } } // Otherwise, if the job queue is already full (a cap of 0 means no limit, so the diff --git a/web/listReports/index.html b/web/listReports/index.html index 3ad52cbf..137124ae 100644 --- a/web/listReports/index.html +++ b/web/listReports/index.html @@ -18,7 +18,7 @@

Requested for testing

__requests__

__noRequests__

-

Should Kilotest test a page that is not listed below?

+

Should Kilotest test a page that is not listed below?

Queued for testing

    __queue__ diff --git a/web/listReports/listReports.test.ts b/web/listReports/listReports.test.ts index 689803b6..2e78c177 100644 --- a/web/listReports/listReports.test.ts +++ b/web/listReports/listReports.test.ts @@ -84,7 +84,7 @@ test('listReports includes the page URLs in the report details', async () => { test('listReports includes a link to request testing a new page', async () => { const result: any = await answer(); const html = parse(result.answerPage); - const testLink = html.querySelector('a[href="requestTestForm.html"]'); + const testLink = html.querySelector('a[href="requestNewTestForm.html"]'); assert.ok(testLink); }); diff --git a/web/requestTest/index.html b/web/requestNewTest/index.html similarity index 100% rename from web/requestTest/index.html rename to web/requestNewTest/index.html diff --git a/web/requestTest/index.ts b/web/requestNewTest/index.ts similarity index 97% rename from web/requestTest/index.ts rename to web/requestNewTest/index.ts index 7dfc53d3..45bfca07 100644 --- a/web/requestTest/index.ts +++ b/web/requestNewTest/index.ts @@ -34,7 +34,7 @@ export const answer = async (description: string, url: string, reason: string) = }; } // Otherwise, if a report is available about a page with the same description and URL: - else if (result === 'retest') { + else if (result === 'reportExists') { // Report this. return { status: 'error', diff --git a/web/requestTest/requestTest.test.ts b/web/requestNewTest/requestNewTest.test.ts similarity index 98% rename from web/requestTest/requestTest.test.ts rename to web/requestNewTest/requestNewTest.test.ts index 08d9e15a..736a118f 100644 --- a/web/requestTest/requestTest.test.ts +++ b/web/requestNewTest/requestNewTest.test.ts @@ -1,6 +1,6 @@ /* - requestTest.test.ts - Unit tests for web/requestTest/index.ts, covering the success path of answer. + requestNewTest.test.ts + Unit tests for web/requestNewTest/index.ts, covering the success path of answer. */ // IMPORTS diff --git a/web/requestTestForm/index.html b/web/requestNewTestForm/index.html similarity index 95% rename from web/requestTestForm/index.html rename to web/requestNewTestForm/index.html index 96368346..4d639f36 100644 --- a/web/requestTestForm/index.html +++ b/web/requestNewTestForm/index.html @@ -14,7 +14,7 @@

    Kilotest: Test request

    You may request that a web page be tested by submitting the following form. All answers are required.

    -
    +