Files
ComfyAssets-ComfyUI_PromptM…/tools/vendor/legacy-coverage.mjs
T
Vito 52a00370ea test(system): prove Diagnostics & logs against the concept and every legacy System capability's owner (#329, PR 3) (#387)
* fix(logs): declare the compressed length so a log download finishes

The gzip middleware compressed a text body but kept the handler's declared
Content-Length (the uncompressed size), so a browser waited for bytes that
never came: downloading any log over 1 KB hung, in the workspace and on the
admin page. The middleware now drops a declared length before compressing.

Part of #329

* feat(system): show Diagnostics & logs natively and retire the hand-off panel

System › Diagnostics & logs is native (D109, D110): the concept's
Diagnostics surface with the live database, source, Viewer.js and renderer
lines, Run checks (the shared Health report), the diagnostic report, Inspect
and the confirmed test link; and the Logs surface with the minimum-level
viewer, Lines 1-500, statistics, the log download, the runtime level and
console setting saved by Save preferences, the log files with their
downloads, Review log cleanup (the rotated_logs maintenance plan, no backup
box, Listed files) and a confirmed Clear all logs.

D94's hand-off panel, SECTION_HANDOFFS and the undelivered runtime adapter
are gone. The browser server logs into its own temporary root.

Part of #329

* test(system): compare Diagnostics & logs with the concept

System › Diagnostics & logs and its log cleanup review are compared with the
concept (app.js 694–695 and maintenancePlan("rotated logs")) in both themes at
1600x1100 and 390x844, with D109 and D110 (and D100 for the review) as the only
mapped differences, a 320-wide select check and a mutation check.

The fixture gains a concept seed mode (the concept's four log lines, a 1.4 MB
active log with three rotated copies, console logging off), and the plan
dialog's measureReview and mapPlanConcept move unchanged into
concept-inventory.mjs for Maintenance and this review.

The comparison found that the Database line read a report key that does not
exist (database_info.size_bytes) and so always showed "0 bytes"; it now reads
database_size_bytes, the key get_database_info() returns.

Part of #329

* test(system): prove every legacy System capability has an owner

TESTING.md gains "Legacy System capability coverage": every System capability
of the legacy admin and gallery pages with its anchor on main, owner, status
(covered, kept in legacy or approved removal), native control and the exact
title of the test that proves it.

tests/vendor/legacy-coverage.test.mjs (with tools/vendor/legacy-coverage.mjs)
fails on a row with no owner, an unknown status, a malformed native control, a
control on a row that is not covered, or a test title its file does not
declare; a title only in a comment does not count. workspace-system.spec's
"every covered legacy System capability has its native control" opens each
covered row's section and finds the control by role and accessible name.

Versus the design's table: the anchors are re-checked against main, "Run
health checks" is on Maintenance (D123), not Overview, Diagnostics run is
proven by "Run checks opens the shared Health report", and the export rows
repeat their test title instead of "(same as above)".

Part of #329

* fix(system): confirm Clear all logs on a fresh file list and record coverage

Review fixes for the Diagnostics & logs section:
- Clear all logs reads the log files again before it confirms, so the
  dialog names the copies it deletes now (a list it cannot read gives the
  general sentence, never an old count); Refresh reads the files too.
- Runtime values that cannot be read stay disabled and say so.
- Create test link holds its button until the write answered, so a second
  press cannot write a second record.
- The Lines field names its range.
- The browser server builds its logger in its own root, so no log file is
  ever opened in another folder; the fixture closes every directory scan
  and replaces a link instead of writing through it.
- system.mjs drops the unused section fallback; frontend coverage floors
  recorded with coverage:update (system-diagnostics.mjs 97.5% lines).

Part of #329

* fix(system): read the database size the report sends

The Database line read database_info.size_bytes, a key the report does
not have (get_database_info answers database_size_bytes), so it always
read "Readable · 0 bytes · N images". The browser test now requires a
non-zero size. Found by PR 3's concept comparison.

Part of #329

* test(system): require a running test and a control for every covered capability

Review fixes for the legacy coverage proof:

- A title declared only with test.skip, .fixme or .fail no longer proves a
  row (.only and .slow still do).
- Every covered row names its native control, except the rows in
  CONTROL_EXEMPTIONS, each with its reason (today only the gallery per-image
  auto-tag row, owned by the Images selection bar). The browser test asserts
  that the covered rows without a control are exactly the exemptions.
- JavaScript comments are stripped across the whole source, wherever a block
  comment starts, skipping strings, template literals and regular
  expressions. A Python def test_ counts only at function or method
  indentation and outside comments and docstrings.
- A control name several rows share ("Review plan", "Review options",
  "Refresh") is looked up inside the row the table names (· in "<row>").
- The diagnostics comparison records each select's chosen option, so a wrong
  default level is caught.

Part of #329
2026-10-27 19:15:37 +00:00

216 lines
9.7 KiB
JavaScript

// The legacy System capability coverage table (TESTING.md "Legacy System capability coverage", #249 System S12 #329):
// every capability of the legacy admin and gallery pages names its owner, a status, the native control that replaces it
// and a declared test. tests/vendor/legacy-coverage.test.mjs checks each row against the test files; the browser spec
// `every covered legacy System capability has its native control` finds each covered row's control on its System page.
import { readFileSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
export const ROOT = join(dirname(fileURLToPath(import.meta.url)), '../..');
export const HEADING = '### Legacy System capability coverage';
export const COLUMNS = Object.freeze(['Capability', 'Legacy anchor', 'Owner', 'Status', 'Native control', 'Test']);
export const STATUSES = Object.freeze(['covered', 'kept in legacy', 'approved removal']);
export const SECTIONS = Object.freeze(['overview', 'appearance', 'sources', 'integrations', 'tagging', 'maintenance', 'backups',
'diagnostics']);
export const ROLES = Object.freeze(['button', 'combobox', 'switch', 'slider', 'spinbutton', 'textbox', 'group', 'status', 'term']);
// Where a Test cell's file is looked up, in order.
export const TEST_FOLDERS = Object.freeze(['tests/browser', 'tests/js', 'tests/vendor', 'tests']);
const NONE = '—';
/**
* Covered rows whose replacement is not on a System section, so they name no native control (`—`), each with why. Every
* other covered row must name one.
*/
export const CONTROL_EXEMPTIONS = Object.freeze({
'Gallery per-image auto-tag / review': 'owned by the Images selection bar (Auto-tag selected), not a System section',
});
/** Split a Markdown table row into its cells (a `|` inside backticks is not a separator). */
function cells(line) {
const out = [];
let cell = '';
let code = false;
for (const char of line.trim().replace(/^\|/, '').replace(/\|$/, '')) {
if (char === '`') code = !code;
if (char === '|' && !code) {
out.push(cell.trim());
cell = '';
} else {
cell += char;
}
}
out.push(cell.trim());
return out;
}
/** The table's rows as objects keyed by COLUMNS, from the section that starts at HEADING. */
export function parseCoverageTable(markdown) {
const lines = markdown.split('\n');
const start = lines.findIndex(line => line.trim() === HEADING);
if (start === -1) throw new Error(`TESTING.md has no "${HEADING}" section`);
const header = lines.findIndex((line, index) => index > start && line.trim().startsWith('|'));
if (header === -1 || lines.slice(start + 1, header).some(line => /^#{1,3} /.test(line))) {
throw new Error('The coverage section has no table');
}
const columns = cells(lines[header]);
if (columns.join('|') !== COLUMNS.join('|')) throw new Error(`The coverage table's columns are ${columns.join(', ')}`);
const rows = [];
for (let index = header + 2; index < lines.length && lines[index].trim().startsWith('|'); index += 1) {
const values = cells(lines[index]);
rows.push({ line: index + 1, ...Object.fromEntries(COLUMNS.map((column, at) => [column, values[at] ?? ''])) });
}
return rows;
}
/**
* A Native control cell: `` `<section>` · <role> · "<name>" ``, optionally ` · in "<row title>"` when the name is shared
* by several rows of the section → {section, role, name, row}; `—` → null; else it throws.
*/
export function parseControl(cell) {
if (cell === NONE) return null;
const match = /^`([a-z]+)` · ([a-z]+) · "([^"]+)"(?: · in "([^"]+)")?$/.exec(cell);
if (!match) throw new Error(`malformed native control "${cell}"`);
const [, section, role, name, row = null] = match;
if (!SECTIONS.includes(section)) throw new Error(`unknown System section "${section}"`);
if (!ROLES.includes(role)) throw new Error(`unknown role "${role}"`);
return { section, role, name, row };
}
/** A Test cell: `<title> (<file>)` → {title, file}. */
export function parseTest(cell) {
const match = /^(.+) \(([\w.-]+\.(?:mjs|js|py))\)$/.exec(cell);
if (!match) throw new Error(`malformed test cell "${cell}"`);
return { title: match[1], file: match[2] };
}
// Modifiers that still run the test; `.skip`, `.fixme` and `.fail` do not prove the capability.
const RUNS = new Set(['', 'only', 'slow']);
// After these characters (or at the start) a `/` opens a regular expression, not a division.
const REGEX_BEFORE = /[(,=:[!&|?{};+\-*%<>~^]$/;
/** Where the string, template literal or regular expression opened by `source[start]` ends (its closing index), or -1. */
function literalEnd(source, start) {
const quote = source[start];
let inClass = false;
for (let end = start + 1; end < source.length; end += 1) {
const at = source[end];
if (at === '\\') end += 1;
else if (quote === '/' && at === '[') inClass = true;
else if (quote === '/' && at === ']') inClass = false;
else if (at === quote && !inClass) return end;
// A quoted string or a regular expression never spans lines.
else if (at === '\n' && quote !== '`') return -1;
}
return -1;
}
/**
* JavaScript `source` with every comment blanked (newlines kept), wherever it starts on a line. Strings, template
* literals and regular expressions are skipped whole, so a `//` or `/*` inside them is not a comment.
*/
export function stripJsComments(source) {
let out = '';
let index = 0;
while (index < source.length) {
const char = source[index];
const next = source[index + 1];
if (char === '/' && next === '/') {
while (index < source.length && source[index] !== '\n') index += 1;
continue;
}
if (char === '/' && next === '*') {
const close = source.indexOf('*/', index + 2);
const stop = close === -1 ? source.length : close + 2;
out += source.slice(index, stop).replace(/[^\n]/g, ' ');
index = stop;
continue;
}
const opens = char === "'" || char === '"' || char === '`' || (char === '/' && REGEX_BEFORE.test(out.trimEnd() || '('));
const end = opens ? literalEnd(source, index) : -1;
if (end === -1) {
out += char;
index += 1;
} else {
out += source.slice(index, end + 1);
index = end + 1;
}
}
return out;
}
/** Python `source` with its comments and triple-quoted strings (docstrings) blanked, newlines kept. */
export function stripPythonComments(source) {
return source.replace(/("""|''')[\s\S]*?\1|#[^\n]*/g, text => text.replace(/[^\n]/g, ' '));
}
/**
* The tests a source declares, title → whether it runs: a `test('…'` or `it('…'` call at a line's start (`.only` and
* `.slow` run; `.skip`, `.fixme` and `.fail` do not) in JavaScript, a `def test_…` at function or method indentation in
* Python. Comments, and Python docstrings, never count.
*/
export function declarations(source, file) {
const found = new Map();
const add = (title, runs) => found.set(title, found.get(title) || runs);
if (file.endsWith('.py')) {
for (const match of stripPythonComments(source).matchAll(/^(?: {4})?(?:async[ \t]+)?def[ \t]+(test_\w+)[ \t]*\(/gm)) {
add(match[1], true);
}
return found;
}
const declaration = /^[ \t]*(?:test|it)(?:\.(only|skip|fixme|fail|slow))?\(\s*(['"`])((?:\\.|(?!\2)[^\\])*)\2/gm;
for (const match of stripJsComments(source).matchAll(declaration)) add(match[3].replace(/\\(.)/g, '$1'), RUNS.has(match[1] ?? ''));
return found;
}
/** The titles of the tests a source declares and runs. */
export function declaredTitles(source, file) {
return new Set([...declarations(source, file)].filter(([, runs]) => runs).map(([title]) => title));
}
/** The repository path of a Test cell's file, or null. */
export function testFile(file, root = ROOT) {
for (const folder of TEST_FOLDERS) {
const path = join(root, folder, file);
if (existsSync(path)) return path;
}
return null;
}
/** Every problem of one row (empty: the row is sound). `locate(file)` finds a test file's path, `read(path)` reads it. */
export function rowProblems(row, { root = ROOT, locate = file => testFile(file, root), read = path => readFileSync(path, 'utf8') } = {}) {
const problems = [];
if (!row.Capability) problems.push('no capability');
if (!row['Legacy anchor']) problems.push('no legacy anchor');
if (!row.Owner) problems.push('no owner');
if (!STATUSES.includes(row.Status)) problems.push(`unknown status "${row.Status}"`);
let control = null;
try {
control = parseControl(row['Native control']);
} catch (error) {
problems.push(error.message);
}
if (control && row.Status !== 'covered') problems.push(`a ${row.Status} row names a native control`);
if (row.Status === 'covered' && row['Native control'] === NONE && !Object.hasOwn(CONTROL_EXEMPTIONS, row.Capability)) {
problems.push('a covered row names no native control');
}
let test;
try {
test = parseTest(row.Test);
} catch (error) {
problems.push(error.message);
return problems;
}
const path = locate(test.file);
if (!path) problems.push(`no test file ${test.file}`);
else {
const runs = declarations(read(path), test.file).get(test.title);
if (runs === undefined) problems.push(`${test.file} declares no test "${test.title}"`);
else if (!runs) problems.push(`${test.file} only skips "${test.title}"`);
}
return problems;
}
/** The table read from TESTING.md. */
export function readCoverageTable(root = ROOT) {
return parseCoverageTable(readFileSync(join(root, 'TESTING.md'), 'utf8'));
}