@jrpool/kilotest 60.5.0 → 60.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,7 +40,8 @@
40
40
  "Exec(sqlite3)",
41
41
  "Exec(npm update)",
42
42
  "Exec(npm ls)",
43
- "Exec(pkill)"
43
+ "Exec(pkill)",
44
+ "Exec(python3)"
44
45
  ]
45
46
  }
46
47
  }
package/docs/SERVICE.md CHANGED
@@ -259,7 +259,7 @@ kilotest.com {
259
259
  # Specify the only paths of forwardable requests.
260
260
  @allowedGET {
261
261
  method GET
262
- path /mcp / /index.html /robots.txt /openapi.yaml /openapi.json /swagger.yaml /swagger.json /api-docs /llms.txt /llms-full.txt /capability.md *.html* /fullReport.json/* /api/* /tutorialWeb/images/* /tutorialAI/images/* /qai /qai/comments /favicon.* /style.css /sitemap.xml
262
+ path /mcp / /index.html /robots.txt /openapi.yaml /openapi.json /swagger.yaml /swagger.json /api-docs /llms.txt /llms-full.txt /capability.md *.html* /fullReport.json/* /api/* /tutorialWeb/images/* /tutorialAI/images/* /media/* /qai /qai/comments /favicon.* /style.css /sitemap.xml
263
263
  }
264
264
  # UptimeRobot's uptime check is the only known HEAD caller, and it targets only the site
265
265
  # root (see "Health monitoring" below), so HEAD is forwarded only for that path, not the
package/index.test.ts CHANGED
@@ -451,6 +451,32 @@ test('GET /tutorialAI/images/nonexistent.png returns an error page', async () =>
451
451
  assert.ok(res.body.includes('Image not found'));
452
452
  });
453
453
 
454
+ test('GET /media/demo-chatgpt.mp4 serves the video', async () => {
455
+ const res = await request('GET', '/media/demo-chatgpt.mp4');
456
+ assert.equal(res.statusCode, 200);
457
+ assert.ok(res.headers['content-type'].includes('video/mp4'));
458
+ });
459
+
460
+ test('GET /media with an unknown extension serves octet-stream', async () => {
461
+ // Create a temporary media file with an unknown extension.
462
+ const mediaPath = path.join(import.meta.dirname, 'media', 'test.xyz');
463
+ await fs.writeFile(mediaPath, 'fake media data');
464
+ try {
465
+ const res = await request('GET', '/media/test.xyz');
466
+ assert.equal(res.statusCode, 200);
467
+ assert.ok(res.headers['content-type'].includes('application/octet-stream'));
468
+ }
469
+ finally {
470
+ await fs.unlink(mediaPath).catch(() => {});
471
+ }
472
+ });
473
+
474
+ test('GET /media/nonexistent.mp4 returns an error page', async () => {
475
+ const res = await request('GET', '/media/nonexistent.mp4');
476
+ assert.equal(res.statusCode, 400);
477
+ assert.ok(res.body.includes('Media file not found'));
478
+ });
479
+
454
480
  test('GET /nonexistent.html returns an abuse error', {timeout: 500}, async () => {
455
481
  const res = await request('GET', '/nonexistent.html');
456
482
  assert.equal(res.statusCode, 400);
package/index.ts CHANGED
@@ -42,6 +42,7 @@ import fs from 'node:fs/promises';
42
42
  import {handleComment as handleTutorialWebComment} from './web/tutorialWeb/index.ts';
43
43
  import {answer as tutorialWeb} from './web/tutorialWeb/index.ts';
44
44
  import {answer as accessibility} from './web/accessibility/index.ts';
45
+ import {answer as demoChatgpt} from './web/demo-chatgpt/index.ts';
45
46
  import {answer as home} from './web/home/index.ts';
46
47
  import {answer as privacy} from './web/privacy/index.ts';
47
48
  import {answer as terms} from './web/terms/index.ts';
@@ -111,6 +112,7 @@ const answer: {
111
112
  accessibility: PageHandler;
112
113
  ai0BalanceForm: PageHandler;
113
114
  deleteNotesForm: PageHandler;
115
+ 'demo-chatgpt': PageHandler;
114
116
  enqueue: PageHandler;
115
117
  enqueueForm: PageHandler;
116
118
  expungeReportsForm: PageHandler;
@@ -144,6 +146,7 @@ const answer: {
144
146
  accessibility,
145
147
  ai0BalanceForm,
146
148
  deleteNotesForm,
149
+ 'demo-chatgpt': demoChatgpt,
147
150
  enqueue,
148
151
  enqueueForm,
149
152
  expungeReportsForm,
@@ -217,6 +220,7 @@ export const routes = {
217
220
  '/llms-full.txt',
218
221
  '/llms.txt',
219
222
  '/mcp',
223
+ '/media/*',
220
224
  '/openapi.json',
221
225
  '/openapi.yaml',
222
226
  '/qai',
@@ -914,6 +918,27 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
914
918
  await serveError({message: 'Invalid service request'}, response, false);
915
919
  }
916
920
  }
921
+ // Otherwise, if it is for a media file (e.g. a demonstration video):
922
+ else if (pathname.startsWith('/media/')) {
923
+ const mediaFile = pathname.slice('/media/'.length);
924
+ const mediaPath = path.join(import.meta.dirname, 'media', mediaFile);
925
+ try {
926
+ const media = await fs.readFile(mediaPath);
927
+ const ext = path.extname(mediaFile).toLowerCase();
928
+ const mimeTypes: Record<string, string> = {
929
+ '.mp4': 'video/mp4',
930
+ '.webm': 'video/webm',
931
+ '.mov': 'video/quicktime',
932
+ '.m4v': 'video/mp4'
933
+ };
934
+ const mimeType = mimeTypes[ext] || 'application/octet-stream';
935
+ setHeaders(mimeType, null, 'low');
936
+ response.end(media);
937
+ }
938
+ catch {
939
+ await serveError({message: 'ERROR: Media file not found'}, response, true);
940
+ }
941
+ }
917
942
  // Otherwise, if it is for a tutorial image:
918
943
  else if (pathname.startsWith('/tutorialWeb/images/') || pathname.startsWith('/tutorialAI/images/')) {
919
944
  let imgFile: string;
package/lhm.plugin.json CHANGED
@@ -4423,5 +4423,5 @@
4423
4423
  }
4424
4424
  }
4425
4425
  ],
4426
- "version": "60.5.0"
4426
+ "version": "60.6.0"
4427
4427
  }
@@ -35,6 +35,7 @@ validPathPatterns.add('/api/*');
35
35
  validPathPatterns.add('/fullReport.json/*');
36
36
  validPathPatterns.add('/tutorialWeb/images/*');
37
37
  validPathPatterns.add('/tutorialAI/images/*');
38
+ validPathPatterns.add('/media/*');
38
39
 
39
40
  // Specific page routes (those that end in .html)
40
41
  const htmlRoutes = [
Binary file
package/openapi-3.0.yaml CHANGED
@@ -2,7 +2,7 @@ openapi: 3.0.3
2
2
  info:
3
3
  title: Kilotest API
4
4
  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 web standards, including HTML, CSS, ARIA, and WCAG). 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.'
5
- version: 60.5.0
5
+ version: 60.6.0
6
6
  license:
7
7
  name: MIT
8
8
  url: https://github.com/jrpool/kilotest/blob/main/LICENSE
package/openapi.yaml CHANGED
@@ -2,7 +2,7 @@ openapi: 3.1.0
2
2
  info:
3
3
  title: Kilotest API
4
4
  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 web standards, including HTML, CSS, ARIA, and WCAG). 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.'
5
- version: 60.5.0
5
+ version: 60.6.0
6
6
  license:
7
7
  name: MIT
8
8
  identifier: MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrpool/kilotest",
3
- "version": "60.5.0",
3
+ "version": "60.6.0",
4
4
  "description": "Ensemble testing for front-end web quality",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/server.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "https://github.com/jrpool/kilotest",
7
7
  "source": "github"
8
8
  },
9
- "version": "60.5.0",
9
+ "version": "60.6.0",
10
10
  "remotes": [
11
11
  {
12
12
  "type": "streamable-http",
package/sitemap.xml CHANGED
@@ -9,6 +9,7 @@
9
9
  <url><loc>https://kilotest.com/privacy.html</loc></url>
10
10
  <url><loc>https://kilotest.com/terms.html</loc></url>
11
11
  <url><loc>https://kilotest.com/accessibility.html</loc></url>
12
+ <url><loc>https://kilotest.com/demo-chatgpt.html</loc></url>
12
13
  <url><loc>https://kilotest.com/openapi.yaml</loc></url>
13
14
  <url><loc>https://kilotest.com/llms.txt</loc></url>
14
15
  <url><loc>https://kilotest.com/llms-full.txt</loc></url>
package/smokeTest.ts CHANGED
@@ -25,6 +25,7 @@ const concretePaths: Record<string, Record<string, string>> = {
25
25
  '/llms-full.txt': '/llms-full.txt',
26
26
  '/llms.txt': '/llms.txt',
27
27
  '/mcp': '/mcp',
28
+ '/media/*': '/media/demo-chatgpt.mp4',
28
29
  '/openapi.json': '/openapi.json',
29
30
  '/openapi.yaml': '/openapi.yaml',
30
31
  '/qai': '/qai',
@@ -15,7 +15,7 @@
15
15
  <h2>Unremediated issues</h2>
16
16
  <p>Kilotest has reported these lowest-priority issues:</p>
17
17
  <ul>
18
- <li><strong>large visible-element count</strong> (<code>visibleBulk</code>) on the list-pages page: justified by the existence of more than 100 tested pages</li>
18
+ <li><strong>large visible-element count</strong> (<code>visibleBulk</code>) on the list-pages page: justified by the existence of more than 200 tested pages</li>
19
19
  <li><strong>inconsistent heading, link, or button styles</strong> (<code>incnsistentStyles</code>) on the list-pages page: under investigation</li>
20
20
  </ul>
21
21
  <p>The testing of Kilotest pages is incomplete, so additional unremediated issues may remain.</p>
@@ -0,0 +1,18 @@
1
+ /*
2
+ demo.test.ts
3
+ Unit tests for web/demo/index.ts, covering the answer export.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import {test} from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import {answer} from './index.ts';
11
+
12
+ // TESTS
13
+
14
+ test('answer returns the demonstration page', async () => {
15
+ const result = await answer();
16
+ assert.equal(result.status, 'ok');
17
+ assert.ok(result.answerPage.includes('ChatGPT demonstration'));
18
+ });
@@ -0,0 +1,279 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+ <head>
4
+ __head__
5
+ <meta name="description" content="Kilotest demonstration: a step-through walkthrough of ChatGPT using the Kilotest tools">
6
+ <title>ChatGPT demonstration | Kilotest</title>
7
+ </head>
8
+ <body class="styled">
9
+ __header__
10
+ <main class="page">
11
+ <h1>ChatGPT demonstration</h1>
12
+ <p>This page shows how ChatGPT uses Kilotest to answer questions about the front-end quality of web pages. Each of the five scenarios below reproduces a real interaction. Step through them at your own pace with the Previous and Next buttons, or press Play to advance automatically. The same interaction is also available as a <a href="/media/demo-chatgpt.mp4">video recording</a>.</p>
13
+ <div class="demo" id="demo">
14
+ <div class="demo-controls">
15
+ <button type="button" id="demo-play" aria-pressed="false"><span aria-hidden="true">▶</span> Play</button>
16
+ <button type="button" id="demo-prev"><span aria-hidden="true">◀</span> Previous</button>
17
+ <button type="button" id="demo-next">Next <span aria-hidden="true">▶</span></button>
18
+ <span id="demo-status" role="status">Scene 1 of 5</span>
19
+ </div>
20
+ <div class="demo-stage" id="demo-stage" aria-live="polite">
21
+
22
+ <section class="frame" data-scene="1">
23
+ <h2>1. Which of its own pages has Kilotest tested?</h2>
24
+ <h3>Prompt 1</h3>
25
+ <div class="msg user"><p><span class="mention">@ Kilotest</span> Name all pages of Kilotest that Kilotest has tested. I need only their names and not their URLs, test dates, test results, or any other information.</p></div>
26
+ <div class="msg tool"><p>Used Kilotest — worked for a few seconds</p></div>
27
+ <h3>Response 1</h3>
28
+ <div class="msg assistant"><p>The assistant at first listed every tested page, not only the Kilotest pages.</p></div>
29
+ <h3>Corrective prompt 1</h3>
30
+ <div class="msg user"><p>You answered the wrong question. I asked you to name all pages of Kilotest that Kilotest has tested. I did not ask you to name all pages that Kilotest has tested.</p></div>
31
+ <div class="msg tool"><p>Used Kilotest — worked for 13s</p></div>
32
+ <h3>Corrected response 1</h3>
33
+ <div class="msg assistant"><ul><li>Kilotest</li><li>Kilotest list diagnoses</li><li>Kilotest list issues</li><li>Kilotest list pages</li><li>Kilotest list violators</li></ul></div>
34
+ </section>
35
+
36
+ <section class="frame" data-scene="2" hidden>
37
+ <h2>2. What issues were reported in a page&rsquo;s latest report?</h2>
38
+ <h3>Prompt 2</h3>
39
+ <div class="msg user"><p><span class="mention">@ Kilotest</span> Name the issues reported in the latest report about the Human Rights Watch contact page. I need only their names and not their descriptions, priorities, or any other information.</p></div>
40
+ <div class="msg tool"><p>Used Kilotest — worked for 9s</p></div>
41
+ <h3>Response 2</h3>
42
+ <div class="msg assistant">
43
+ <ul class="issue-list">
44
+ <li>abbr title missing</li>
45
+ <li>all-capital text</li>
46
+ <li>aria reference invalid</li>
47
+ <li>attribute invalid</li>
48
+ <li>attribute obsolete</li>
49
+ <li>attribute value invalid</li>
50
+ <li>autocomplete invalid</li>
51
+ <li>box size absolute</li>
52
+ <li>button name not visible</li>
53
+ <li>code invalid</li>
54
+ <li>contrast poor</li>
55
+ <li>double br use dubious</li>
56
+ <li>element focusability and operability conflict</li>
57
+ <li>fieldset missing?</li>
58
+ <li>focus indication improvable</li>
59
+ <li>focusable element hidden</li>
60
+ <li>forms not distinctly named</li>
61
+ <li>heading level skipped</li>
62
+ <li>heading structure illogical</li>
63
+ <li>hovering changes content</li>
64
+ <li>ID empty</li>
65
+ <li>ID not unique</li>
66
+ <li>iframe not named</li>
67
+ <li>image not named</li>
68
+ <li>inconsistent heading, link, or button styles</li>
69
+ <li>itemid invalid</li>
70
+ <li>label location dubious</li>
71
+ <li>large visible-element count</li>
72
+ <li>line height low</li>
73
+ <li>link destination not URL</li>
74
+ <li>link href missing</li>
75
+ <li>link indication poor</li>
76
+ <li>link not named</li>
77
+ <li>links named identically</li>
78
+ <li>links not grouped as nav</li>
79
+ <li>list-like br use dubious</li>
80
+ <li>menu navigation nonstandard</li>
81
+ <li>parent invalid</li>
82
+ <li>requirement invalid</li>
83
+ <li>role assigned instead of implicit</li>
84
+ <li>role redundant</li>
85
+ <li>small targets too near to each other</li>
86
+ <li>summary has explicit role</li>
87
+ <li>svg image not named</li>
88
+ <li>tab-opening link action unstated?</li>
89
+ <li>target small</li>
90
+ <li>target very small</li>
91
+ <li>text contrast improvable</li>
92
+ <li>text contrast not testable</li>
93
+ <li>title attribute invalid</li>
94
+ <li>z-index not zero</li>
95
+ </ul>
96
+ </div>
97
+ </section>
98
+
99
+ <section class="frame" data-scene="3" hidden>
100
+ <h2>3. Which issue did the most rule engines report, and what did they find?</h2>
101
+ <h3>Prompt 3</h3>
102
+ <div class="msg user"><p><span class="mention">@ Kilotest</span> In the latest report about the Wikimedia Foundation (home) page, tell me which highest-priority issue was reported by the largest number of rule engines. For that issue, give me the XPath of the element that was reported as a violator by the largest number of rule engines. Finally, for that element and that issue, give me a one-sentence summary of the diagnoses. I do not need an issue explanation, additional information about the element, the number of diagnoses, their full texts, or the number or names of the rule engines that issued them.</p></div>
103
+ <div class="msg tool"><p>Used Kilotest — worked for 21s</p></div>
104
+ <h3>Response 3</h3>
105
+ <div class="msg assistant">
106
+ <p><strong>Highest-priority issue:</strong> Link not named</p>
107
+ <p><strong>XPath(s) tied for the most rule-engine reports:</strong></p>
108
+ <ul>
109
+ <li><code>/html/body/div[2]/footer[1]/div[1]/div[2]/div[1]/figure[1]/a[1]</code></li>
110
+ <li><code>/html/body/div[2]/footer[1]/div[1]/div[2]/div[1]/figure[2]/a[1]</code></li>
111
+ </ul>
112
+ <p><strong>Diagnosis summary:</strong> Both links lack an accessible name, such as discernible text or an equivalent text alternative, making their purpose unclear to screen-reader users.</p>
113
+ </div>
114
+ </section>
115
+
116
+ <section class="frame" data-scene="4" hidden>
117
+ <h2>4. How do I get an untested page tested?</h2>
118
+ <h3>Prompt 4</h3>
119
+ <div class="msg user"><p><span class="mention">@ Kilotest</span> Request a Kilotest test of Example12345 (<a href="https://example12345.info">https://example12345.info</a>) and let me know whether the request was accepted. If it was, tell me how long I should expect to wait before the results will be available.</p></div>
120
+ <div class="msg tool"><p>Used Kilotest — worked for 12s</p></div>
121
+ <h3>Response 4</h3>
122
+ <div class="msg assistant"><p>Kilotest <strong>did not accept the request</strong> to test Example12345 (<code>https://example12345.info</code>).</p>
123
+ <p>The reason given was that the URL does not resolve to a page that this Kilotest deployment can test.</p>
124
+ <p>Since the request was rejected, there is no expected wait time for results.</p></div>
125
+ </section>
126
+
127
+ <section class="frame" data-scene="5" hidden>
128
+ <h2>5. Can a page be retested after it has been remediated?</h2>
129
+ <h3>Prompt 5</h3>
130
+ <div class="msg user"><p><span class="mention">@ Kilotest</span> Order a Kilotest retest of the Example page. When the report is completed, compare it with the previous report about that page and give me two lists: one naming any eliminated issues and the other naming any newly reported issues. I do not need the report dates, the issue counts, issue explanations, or any other information. If the order is rejected, or if the report is not completed within 4 minutes, stop waiting and tell me what happened.</p></div>
131
+ <div class="msg tool"><p>Used Kilotest — worked for 1m 12s</p></div>
132
+ <h3>Response 5</h3>
133
+ <div class="msg assistant">
134
+ <p>Kilotest accepted the retest order, and the report was completed within 4 minutes.</p>
135
+ <p><strong>Eliminated issues</strong></p>
136
+ <p>– None</p>
137
+ <p><strong>Newly reported issues</strong></p>
138
+ <p>– None</p>
139
+ </div>
140
+ </section>
141
+
142
+ </div>
143
+ </div>
144
+ <p>Each scenario corresponds to a real capability of the Kilotest tools: discovering tested pages, listing issues, drilling down to elements and diagnoses, requesting tests, and ordering retests. The responses above reproduce real Kilotest report data.</p>
145
+ </main>
146
+ <script>
147
+ const frames = Array.from(document.querySelectorAll('#demo-stage .frame'));
148
+ const playBtn = document.getElementById('demo-play');
149
+ const prevBtn = document.getElementById('demo-prev');
150
+ const nextBtn = document.getElementById('demo-next');
151
+ const status = document.getElementById('demo-status');
152
+ let current = 0;
153
+ let timer = null;
154
+
155
+ function show(index) {
156
+ current = Math.max(0, Math.min(frames.length - 1, index));
157
+ frames.forEach((frame, i) => { frame.hidden = i !== current; });
158
+ status.textContent = 'Scene ' + (current + 1) + ' of ' + frames.length;
159
+ prevBtn.disabled = current === 0;
160
+ nextBtn.disabled = current === frames.length - 1;
161
+ }
162
+
163
+ function stop() {
164
+ if (timer) { clearInterval(timer); timer = null; }
165
+ playBtn.innerHTML = '<span aria-hidden="true">▶</span> Play';
166
+ playBtn.setAttribute('aria-pressed', 'false');
167
+ }
168
+
169
+ playBtn.addEventListener('click', function () {
170
+ if (timer) { stop(); return; }
171
+ if (current === frames.length - 1) { show(0); }
172
+ playBtn.innerHTML = '<span aria-hidden="true">⏸</span> Pause';
173
+ playBtn.setAttribute('aria-pressed', 'true');
174
+ timer = setInterval(function () {
175
+ if (current >= frames.length - 1) { stop(); return; }
176
+ show(current + 1);
177
+ }, 6000);
178
+ });
179
+
180
+ prevBtn.addEventListener('click', function () { stop(); show(current - 1); });
181
+ nextBtn.addEventListener('click', function () { stop(); show(current + 1); });
182
+ show(0);
183
+ </script>
184
+ <style>
185
+ .demo-controls {
186
+ display: flex;
187
+ flex-wrap: wrap;
188
+ gap: 0.5rem;
189
+ align-items: center;
190
+ margin-bottom: 1rem;
191
+ }
192
+ .demo-controls button {
193
+ min-height: 44px;
194
+ padding: 0.5rem 1rem;
195
+ font-size: 1rem;
196
+ font-family: inherit;
197
+ border: solid 1px var(--line);
198
+ border-radius: 0.375rem;
199
+ background: var(--card);
200
+ color: inherit;
201
+ cursor: pointer;
202
+ }
203
+ .demo-controls button:hover {
204
+ border-color: var(--accent);
205
+ }
206
+ .demo-controls button:disabled {
207
+ opacity: 0.45;
208
+ cursor: default;
209
+ }
210
+ .demo-controls button:disabled:hover {
211
+ border-color: var(--line);
212
+ }
213
+ #demo-status {
214
+ font-size: 0.9rem;
215
+ color: var(--muted);
216
+ }
217
+ .demo-stage .frame {
218
+ border: solid 1px var(--line);
219
+ border-radius: 0.5rem;
220
+ padding: 1rem 1.25rem;
221
+ background: var(--card);
222
+ }
223
+ .demo-stage .frame h2 {
224
+ margin-top: 0;
225
+ font-size: 1.15rem;
226
+ }
227
+ .demo-stage .frame h3 {
228
+ margin: 1rem 0 0.25rem;
229
+ font-size: 0.95rem;
230
+ color: var(--accent);
231
+ }
232
+ .msg {
233
+ margin: 0.75rem 0;
234
+ max-width: 52rem;
235
+ }
236
+ .msg p {
237
+ margin: 0;
238
+ }
239
+ .msg.user {
240
+ display: flex;
241
+ justify-content: flex-end;
242
+ }
243
+ .msg.user p {
244
+ max-width: 42rem;
245
+ padding: 0.55rem 1.1rem;
246
+ background: #e9e9e9;
247
+ border-radius: 1.4rem;
248
+ }
249
+ .msg.tool {
250
+ font-size: 0.9rem;
251
+ color: #888;
252
+ }
253
+ .msg.tool p {
254
+ padding: 0.2rem 0;
255
+ }
256
+ .msg.tool p::after {
257
+ content: " \203A";
258
+ }
259
+ .mention {
260
+ display: inline-block;
261
+ padding: 0.05rem 0.5rem;
262
+ margin-right: 0.25rem;
263
+ font-size: 0.85rem;
264
+ background: #ddf0e8;
265
+ border-radius: 0.4rem;
266
+ }
267
+ .msg.assistant {
268
+ padding: 0.6rem 0;
269
+ }
270
+ .issue-list {
271
+ columns: 14rem 3;
272
+ column-gap: 2rem;
273
+ font-size: 0.9rem;
274
+ margin: 0.5rem 0 0;
275
+ }
276
+ </style>
277
+ __footer__
278
+ </body>
279
+ </html>
@@ -0,0 +1,19 @@
1
+ /*
2
+ index.ts
3
+ Serves the Kilotest demonstration page.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import {populateTemplate} from '../../util.ts';
9
+
10
+ // FUNCTIONS
11
+
12
+ // Returns the demonstration page.
13
+ export const answer = async () => {
14
+ const answerPage = await populateTemplate(import.meta.dirname, {});
15
+ return {
16
+ status: 'ok',
17
+ answerPage
18
+ };
19
+ };
@@ -111,6 +111,8 @@
111
111
  <li>Toggle the <q>Ask/Allow</q> buttons to show <q>Allow</q>.</li>
112
112
  </ol>
113
113
  </details>
114
+ <h2>Demonstration</h2>
115
+ <p>A <a href="/demo-chatgpt.html">step-through demonstration</a> shows ChatGPT using Kilotest to discover tested pages, inspect reported issues, and request tests and retests.</p>
114
116
  <h2 id="other">Other AI platforms</h2>
115
117
  <p>You can connect Kilotest to some other AI platforms, too. The instructions above are limited to consumer platforms that are either free or relatively inexpensive (US$20 per month or less). Business platforms such as ChatGPT Business, and developer platforms such as Devin Desktop, also permit you to enable Kilotest.</p>
116
118
  <p>Feel free to submit a comment below about empowering any AI platform to use Kilotest.</p>