@jrpool/kilotest 45.0.6 → 46.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -110,9 +110,13 @@ exports.response = async args => {
110
110
  if (type === 'test' && which) {
111
111
  // For each standard instance of the act:
112
112
  instances.forEach(instance => {
113
- const {count, ordinalSeverity, ruleID, what} = instance;
114
- // If the instance has the issue ID and the catalog index of the violator:
115
- if (instance.issueID === issueID && instance.catalogIndex === catalogIndex) {
113
+ const {count, ordinalSeverity, outcome, ruleID, what} = instance;
114
+ // If the instance reports a violation of the issue by the violator:
115
+ if (
116
+ outcome !== 'cantTell'
117
+ && instance.issueID === issueID
118
+ && instance.catalogIndex === catalogIndex
119
+ ) {
116
120
  // Get the diagnosis.
117
121
  const diagnosis = {
118
122
  'identifier of the violated rule': ruleID !== what ? ruleID : null,
package/api/listIssues.js CHANGED
@@ -75,29 +75,32 @@ exports.response = async args => {
75
75
  const instances = result?.standardResult?.instances ?? [];
76
76
  // For each standard instance of the act:
77
77
  instances.forEach(instance => {
78
- const {catalogIndex, issueID} = instance;
79
- // Get the specification of its issue.
80
- const issueSpec = issueID ? getIssueSpec(issueID) : null;
81
- // If the instance has a non-ignorable and fully classified issue:
82
- if (issueSpec) {
83
- const {summary, weight, why} = issueSpec;
84
- // Ensure the rule-engine ID is in the reporters data.
85
- reporterIDs.add(which);
86
- // If the instance has a catalog index:
87
- if (catalogIndex) {
88
- // Ensure the index of the violator is in the results data.
89
- violatorIndexes.add(catalogIndex);
78
+ const {catalogIndex, issueID, outcome} = instance;
79
+ // If the instance reports a violation:
80
+ if (outcome !== 'cantTell') {
81
+ // Get the specification of its issue.
82
+ const issueSpec = issueID ? getIssueSpec(issueID) : null;
83
+ // If the instance has a non-ignorable and fully classified issue:
84
+ if (issueSpec) {
85
+ const {summary, weight, why} = issueSpec;
86
+ // Ensure the rule-engine ID is in the reporters data.
87
+ reporterIDs.add(which);
88
+ // If the instance has a catalog index:
89
+ if (catalogIndex) {
90
+ // Ensure the index of the violator is in the results data.
91
+ violatorIndexes.add(catalogIndex);
92
+ }
93
+ // Ensure the data about the issue are in the results data.
94
+ issuesData[issueID] ??= {
95
+ id: issueID,
96
+ summary,
97
+ weight,
98
+ why,
99
+ reporterIDs: new Set()
100
+ };
101
+ // Ensure the reporter ID is in the data about the issue.
102
+ issuesData[issueID].reporterIDs.add(which);
90
103
  }
91
- // Ensure the data about the issue are in the results data.
92
- issuesData[issueID] ??= {
93
- id: issueID,
94
- summary,
95
- weight,
96
- why,
97
- reporterIDs: new Set()
98
- };
99
- // Ensure the reporter ID is in the data about the issue.
100
- issuesData[issueID].reporterIDs.add(which);
101
104
  }
102
105
  });
103
106
  }
@@ -81,18 +81,21 @@ exports.response = async args => {
81
81
  instances.forEach(instance => {
82
82
  // If the instance has the issue ID:
83
83
  if (instance.issueID === issueID) {
84
- const {catalogIndex} = instance;
85
- // Ensure the rule-engine ID is in the reporters data.
86
- reporterIDs.add(which);
87
- // If the instance has a catalog index:
88
- if (catalogIndex) {
89
- // Ensure the catalog index is in the violators data.
90
- violators[catalogIndex] ??= {
91
- catalogIndex,
92
- reporters: new Set()
93
- };
94
- // Ensure the reporter ID is in the violator data.
95
- violators[catalogIndex].reporters.add(which);
84
+ const {catalogIndex, outcome} = instance;
85
+ // If the instance reports a violation:
86
+ if (outcome !== 'cantTell') {
87
+ // Ensure the rule-engine ID is in the reporters data.
88
+ reporterIDs.add(which);
89
+ // If the instance has a catalog index:
90
+ if (catalogIndex) {
91
+ // Ensure the catalog index is in the violators data.
92
+ violators[catalogIndex] ??= {
93
+ catalogIndex,
94
+ reporters: new Set()
95
+ };
96
+ // Ensure the reporter ID is in the data about the violator.
97
+ violators[catalogIndex].reporters.add(which);
98
+ }
96
99
  }
97
100
  }
98
101
  });
package/api/util.js CHANGED
@@ -106,7 +106,7 @@ exports.getReportBasics = async (timeStamp, jobID) => {
106
106
  // Return them.
107
107
  return basics;
108
108
  };
109
- // Returns the classification of an issue.
109
+ // Returns the specification of an issue.
110
110
  exports.getIssueSpec = issueID => {
111
111
  // Get the issue specification.
112
112
  const issueSpec = issueSpecs[issueID];
@@ -0,0 +1,42 @@
1
+ /*
2
+ util.test.js
3
+ Tests for api/util.js.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ const {test} = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+ const {getRuleEngineFacts, getRuleEnginesFacts} = require('./util');
11
+
12
+ // TESTS
13
+
14
+ test('getRuleEngineFacts returns the name and sponsor of a known rule engine', () => {
15
+ const facts = getRuleEngineFacts('axe');
16
+ assert.deepEqual(facts, {
17
+ identifier: 'axe',
18
+ name: 'Axe',
19
+ sponsor: 'Deque'
20
+ });
21
+ });
22
+
23
+ test('getRuleEngineFacts returns nulls for name and sponsor of an unknown rule engine', () => {
24
+ const facts = getRuleEngineFacts('nonexistentEngine');
25
+ assert.deepEqual(facts, {
26
+ identifier: 'nonexistentEngine',
27
+ name: null,
28
+ sponsor: null
29
+ });
30
+ });
31
+
32
+ test('getRuleEnginesFacts returns facts sorted alphabetically by name', () => {
33
+ // Names: ibm -> "Accessibility Checker", axe -> "Axe", wave -> "WAVE".
34
+ const facts = getRuleEnginesFacts(new Set(['wave', 'axe', 'ibm']));
35
+ assert.deepEqual(facts.map(fact => fact.identifier), ['ibm', 'axe', 'wave']);
36
+ assert.deepEqual(facts.map(fact => fact.name), ['Accessibility Checker', 'Axe', 'WAVE']);
37
+ });
38
+
39
+ test('getRuleEnginesFacts returns an empty array for an empty set', () => {
40
+ const facts = getRuleEnginesFacts(new Set());
41
+ assert.deepEqual(facts, []);
42
+ });
package/docs/IDEAS.md CHANGED
@@ -5858,3 +5858,70 @@ The gamble is **not** on whether the concept is sound (it is), but on **executio
5858
5858
 
5859
5859
  #### Summary
5860
5860
  Your architectural shift from static digests to interactive FAQ + LLM-driven Q&A is **well-grounded in current practice** and **solves real UX problems**. The risks are manageable with proper scoping and validation. I'd recommend a **phased rollout** starting with FAQs + manual custom-question handling, then adding LLM assistance once you understand user intent patterns.
5861
+
5862
+ ## Testing
5863
+
5864
+ Now let's check the testSuite.js more fully to see how it picks fixtures (confirming the deficiency the user described), and check schemas.js/util.js in api for outcome handling.
5865
+
5866
+ Here is a plan for building a rigorous testing system for Kilotest, informed by how QAI is structured. I read the QAI source (`src/util.test.ts`, `src/requestHandler.test.ts`, `package.json`) and the relevant parts of Kilotest (`api/*.js`, root `util.js`, `db/`, `web/listReports`) to ground this in what actually exists today, rather than in the abstract.
5867
+
5868
+ ### What QAI does that Kilotest should converge toward
5869
+
5870
+ 1. Colocated `*.test.ts` files next to the modules they test, run with the built-in `node:test` runner and `node:assert/strict`, no third-party test framework or mocking library.
5871
+ 2. Coverage enforced by `c8` with 100% thresholds (lines/functions/branches/statements) as a gate, wired into `npm run check` alongside `typecheck`, `lint`, and `hint`.
5872
+ 3. Server-level tests spin up a real ephemeral `http.Server` per test (`server.listen(0, ...)`) using `makeHandler(commentsFilePath, sendAlert)`, i.e. the handler factory takes its data-file path and side-effecting collaborators (like the alert sender) as parameters instead of importing hardcoded module-level constants. That is what makes deterministic, isolated tests possible: each test gets its own temp file/dir (`mkdtempDisposable`) and its own server instance, so there is no shared mutable fixture and no test order dependency.
5873
+ 4. HTML responses are asserted with `node-html-parser`, not regex-only, so tests check structure (`querySelector('title')`, absence of `<script>`/`<img>` after sanitization) as well as text content.
5874
+ 5. Failure paths are tested deliberately, not incidentally: unreadable/unwritable files (via `chmod`), missing fields, oversized/undersized input, duplicate detection, a broken `sendAlert` collaborator, etc. Every branch has a dedicated test, which is what makes 100% coverage meaningful rather than a vanity number.
5875
+ 6. `node:test`'s built-in `testContext.mock.property(...)` is used for surgical monkey-patching (e.g. redirecting a route's file path) instead of a mocking library.
5876
+
5877
+ ### Why Kilotest's current setup cannot get there yet
5878
+
5879
+ - `db/`, `db/reports`, `db/jobs`, etc. are computed once at module load as `path.join(__dirname, 'db')` in the top-level `util.js` (`dbPath`, `reportsPath`, `hiddenReportsPath`, `reportsExtractPath`, `jobsPath`, `recsPath`). Every downstream function (`getReportExtracts`, `getReportBasics`, job-claiming code, etc.) closes over these constants. There is currently no way to point any of this at a fixture directory the way QAI's `makeHandler(commentsFilePath, ...)` accepts a path.
5880
+ - `api/testSuite.js` is an end-to-end script, not a test suite: it talks to a live running server (local or deployed), picks a **random real report** (`Math.floor(reportsBasics.length * Math.random())`), and only asserts loose shape/presence conditions (`!issuesBasics.length`, `!issueBasics['impact on a user']`) because it cannot know in advance what the "correct" content of a real report is. It cannot catch a case where, say, `outcome === 'error'` is silently mapped to the wrong impact label, because it never encodes what the right answer should be.
5881
+ - There is no `test/` or `*.test.js` infrastructure, no `c8`, and no UI test of any kind.
5882
+
5883
+ That correctly diagnoses the deficiency: you cannot write an assertion like "a report whose act has `outcome: 'error'` must be classified as X" against a randomly-selected real report, because you would first have to reverse-engineer what X should be from the code you are trying to test — that is circular.
5884
+
5885
+ ### Proposed plan
5886
+
5887
+ #### Phase 0: Establish the toolchain
5888
+ - Add `c8` as a dev dependency; add an `npm test` script (`c8 --check-coverage node --test`) that actually runs something, replacing the current `"echo \"Error…\""` placeholder. Keep `test:api` for now as a separate, explicitly-labeled smoke/E2E script (see Phase 4) rather than deleting it immediately.
5889
+ - Decide coverage thresholds up front (QAI uses 100%; Kilotest may want to start lower, e.g. 80%, and ratchet up, given the amount of legacy code to backfill). This is a judgment call worth confirming with you before locking in a number in `package.json`.
5890
+ - Since Kilotest is CommonJS (`sourceType: "commonjs"` in `eslint.config.mjs`) and plain JS (not TypeScript), tests will be `*.test.js` using `require('node:test')` / `require('node:assert/strict')` — same runner and assertion library as QAI, just without the TS layer. This keeps the eventual TS migration (if you pursue it) a mechanical follow-up rather than a rewrite.
5891
+
5892
+ #### Phase 1: Make data locations injectable (prerequisite for everything else)
5893
+ This is the load-bearing architectural change. Without it, no module can be tested against crafted fixtures without touching the real `db/`.
5894
+ - Refactor root `util.js` (and anything relying on its exported path constants) so the base data directory is resolved from a parameter or an environment variable read at call time (e.g. `KILOTEST_DB_DIR`, defaulting to today's `path.join(__dirname, 'db')`), rather than baked into module-load-time constants.
5895
+ - Where feasible, prefer explicit parameters over env vars for the innermost functions (mirroring `makeHandler(commentsFilePath, ...)`), and only use an env var at the outermost/server-bootstrap layer, so unit tests can call functions directly with a fixture path and never need to touch process env or the module cache.
5896
+
5897
+ #### Phase 2: Build a crafted fixture corpus
5898
+ - Create `test/fixtures/db/` (or similar) with hand-authored, minimal Testaro reports covering the `outcome` cases you need verified: `success`, `failure`, `error`, mixed-outcome act arrays, an act with a missing/undefined `outcome`, a superseded report, an empty-violations report, etc.
5899
+ - Each fixture should be small enough that the expected API response can be hand-computed and hard-coded into the test as the "known-good" answer, exactly the property the current `test:api` script lacks.
5900
+
5901
+ #### Phase 3: Unit tests, module by module
5902
+ Priority order, since these are the modules you just changed and where correctness of `outcome` handling matters most:
5903
+ 1. `api/util.js` and root `util.js` (pure/near-pure helper functions: `getReportBasics`, `getReportExtracts`, `getToolsFacts`, response-metadata helpers).
5904
+ 2. `api/listIssues.js`, `api/listViolators.js`, `api/listDiagnoses.js` (the modules that already reference `outcome`).
5905
+ 3. `api/listReports.js`, `api/getReport.js`.
5906
+ 4. `api/requestTest.js`, `api/requestRetest.js`, `api/requestFeature.js`, `api/schemas.js` (input validation edge cases: too-short/long fields, malformed bodies).
5907
+ Each test file lives next to its module (`api/listIssues.test.js`) and exercises it directly against Phase 2 fixtures, no server involved.
5908
+
5909
+ #### Phase 4: Rework the API-level integration test
5910
+ Replace `test:api`'s "start from a live server, pick a random real report" approach with a QAI-style handler test:
5911
+ - Start an ephemeral server per test (or a shared one where safe) pointed at the fixture `db/` from Phase 2.
5912
+ - Walk through the same request sequence `testSuite.js` currently performs, but with hard-coded fixture identifiers and asserted exact expected values instead of "does this look non-empty."
5913
+ - Keep a much smaller, separate "smoke test against a real running deployment" script if you still want a pre-deploy sanity check, but do not conflate it with correctness testing.
5914
+
5915
+ #### Phase 5: UI/web testing
5916
+ Kilotest's `web/*` pages are plain HTML + vanilla JS (no framework), analogous to QAI's server-rendered pages. Two complementary layers, matching QAI's toolset:
5917
+ - **Static/structural**: use `node-html-parser` (add as a dev dependency, same as QAI) to assert on the HTML served for each page/form (titles, presence of expected form fields, absence of raw unescaped user content).
5918
+ - **Behavioral**: for pages with client-side JS (`web/listReports/index.js` etc.) that fetch from the API and render results, either (a) extract the rendering logic into pure, testable functions that take API response JSON and return DOM-ready data/markup, so they can be unit-tested with `node:test` + `node:assert` without a browser, and/or (b) introduce a small number of real-browser tests (e.g. Playwright) only for the handful of interactions that cannot be verified any other way (form submission, client-side validation feedback). I would only reach for a browser automation tool if pure-function extraction proves impractical, since it adds real complexity and flakiness risk to a JS-only project.
5919
+
5920
+ #### Phase 6: CI wiring and ratcheting
5921
+ - Add the test/coverage run to `.github/workflows/` (there is already a `.github/workflows/` dir) so `npm test` (and `npm run lint`) gate merges.
5922
+ - Track coverage percentage over time and raise the `c8` threshold as backfill work completes, rather than requiring 100% from day one across legacy code.
5923
+
5924
+ #### On architectural convergence with QAI more broadly
5925
+ Beyond testing, actually adopting the QAI architecture would additionally involve: TypeScript with `--strict`, ESM (`"type": "module"`), the `@stylistic/eslint-plugin` + `@typescript-eslint/parser` lint stack, and the dependency-injection pattern (`makeHandler(path, collaborators)`) throughout, not just at the edges touched for testing. I would treat that as a separate, larger migration decided after the testing foundation above exists, since attempting both at once (rewrite the language/module system *and* build the test suite that is supposed to protect the rewrite) inverts the safety order.
5926
+
5927
+ I have not made any changes yet. Given the phased nature of this, I would suggest reviewing/adjusting each phase before I implement it, starting with Phase 1 (the data-path injection refactor), since every later phase depends on it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrpool/kilotest",
3
- "version": "45.0.6",
3
+ "version": "46.0.1",
4
4
  "description": "Ensemble testing for front-end web quality",
5
5
  "main": "index.js",
6
6
  "engines": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "start": "node index",
11
11
  "lint": "npx eslint && npx markdownlint-cli2",
12
- "test": "echo \"Error: no test specified\" && exit 1",
12
+ "test": "c8 node --test",
13
13
  "test:api": "node api/testSuite",
14
14
  "generate:openapi": "node scripts/generate-openapi"
15
15
  },
@@ -44,6 +44,7 @@
44
44
  "@eslint/json": "*",
45
45
  "@eslint/markdown": "*",
46
46
  "@redocly/cli": "*",
47
+ "c8": "*",
47
48
  "eslint": "*",
48
49
  "globals": "*",
49
50
  "js-yaml": "*",
@@ -52,5 +53,28 @@
52
53
  },
53
54
  "allowScripts": {
54
55
  "fsevents@2.3.2": true
56
+ },
57
+ "c8": {
58
+ "all": true,
59
+ "reporter": [
60
+ "text",
61
+ "html"
62
+ ],
63
+ "include": [
64
+ "index.js",
65
+ "mcp.js",
66
+ "alerts.js",
67
+ "util.js",
68
+ "api/**/*.js"
69
+ ],
70
+ "exclude": [
71
+ "coverage/**",
72
+ "test{,s}/**",
73
+ "test{,-*}.{js,cjs,mjs}",
74
+ "**/*{.,-}test.{js,cjs,mjs}",
75
+ "**/__tests__/**",
76
+ "web/**",
77
+ "scripts/**"
78
+ ]
55
79
  }
56
80
  }