desktop coverage: every CLI command mapped to its window place (cli/lib/desktop-coverage.ts), docs/desktop-coverage.md generated from it, guard test fails on a gap or an unjustified not-for-desktop

This commit is contained in:
CRTkafa 2026-10-21 05:02:20 +07:00
parent a95aa6dd49
commit dc6bb088a1
4 changed files with 371 additions and 0 deletions

View file

@ -0,0 +1,86 @@
// docs/desktop-coverage.md from cli/lib/desktop-coverage.ts, checked against the code: a command is "covered" only
// when every method it names exists (serve.ts describeMethods(), or a #[tauri::command] of src-tauri) and the window
// calls it (the method quoted in apps/desktop/src, or listed in TOOLBOX, which the More tools tab renders).
// `bun tools/gen-desktop-coverage.ts` writes the page; `--check` exits 1 when it is stale. tests/desktop-coverage.test.ts
// runs analyze() too.
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { COMMANDS, PLAIN } from '../cli/commands';
import { DESKTOP_COVERAGE, OWNER_STEPS, TOOLBOX } from '../cli/lib/desktop-coverage';
import { describeMethods } from '../cli/serve';
const ROOT = join(import.meta.dir, '..');
const OUT = join(ROOT, 'docs/desktop-coverage.md');
function files(dir: string, out: string[] = []): string[] {
for (const f of readdirSync(dir)) {
const p = join(dir, f);
if (statSync(p).isDirectory()) { if (f !== 'dev' && f !== 'i18n') files(p, out); } else if (/\.(ts|tsx|rs)$/.test(f)) out.push(p);
}
return out;
}
export interface Row { command: string; status: 'covered' | 'gap' | 'not-for-desktop'; where: string; methods: string[]; owner?: string; problems: string[] }
export function analyze(): { rows: Row[]; extra: string[] } {
const src = files(join(ROOT, 'apps/desktop/src')).map(f => readFileSync(f, 'utf8')).join('\n');
const rust = files(join(ROOT, 'apps/desktop/src-tauri/src')).map(f => readFileSync(f, 'utf8')).join('\n');
const hostCmds = new Set([...rust.matchAll(/#\[tauri::command\][\s\S]{0,200}?fn ([a-z_]+)/g)].map(m => m[1]!));
const methods = new Set(describeMethods().map(m => m.method));
const toolboxShown = /\bTOOLBOX\b/.test(src);
const inToolbox = new Set(TOOLBOX.flatMap(g => g.methods));
const reachable = (m: string): string | null => {
if (m.startsWith('host:')) {
const c = m.slice(5);
if (!hostCmds.has(c)) return `${m}: no such tauri command`;
return src.includes(`'${c}'`) ? null : `${m}: never invoked by the window`;
}
if (!methods.has(m)) return `${m}: no such serve method`;
if (src.includes(`'${m}'`) || src.includes(`"${m}"`)) return null;
if (toolboxShown && inToolbox.has(m)) return null;
return `${m}: no window place calls it`;
};
const all = [...Object.keys(PLAIN), ...Object.keys(COMMANDS)];
const rows: Row[] = all.map(command => {
const c = DESKTOP_COVERAGE[command];
if (!c) return { command, status: 'gap', where: '', methods: [], problems: ['no entry in cli/lib/desktop-coverage.ts'] };
if ('not' in c) return { command, status: 'not-for-desktop', where: c.not, methods: [], problems: c.not.trim() ? [] : ['not-for-desktop without a reason'] };
const problems = c.methods.map(reachable).filter((p): p is string => !!p);
if (!c.methods.length && !(c.owner && toolboxShown && OWNER_STEPS.length)) problems.push('no method and no owner step');
return { command, status: problems.length ? 'gap' : 'covered', where: c.where, methods: c.methods, ...(c.owner ? { owner: c.owner } : {}), problems };
});
return { rows, extra: Object.keys(DESKTOP_COVERAGE).filter(k => !all.includes(k)) };
}
export function render(rows: Row[]): string {
const n = (s: Row['status']): number => rows.filter(r => r.status === s).length;
const cell = (s: string): string => s.replace(/\|/g, '\\|');
return [
'# Desktop coverage',
'',
'Generated by `bun tools/gen-desktop-coverage.ts` from `cli/lib/desktop-coverage.ts`; do not edit by hand.',
'Every CLI command (cli/commands.ts) and the desktop place that runs it. "covered" means each method listed exists',
'(serve.ts, or a host command `host:`) and the window calls it; tests/desktop-coverage.test.ts fails on a gap.',
'Owner-only parts are owner actions in the window or owner steps with their terminal command (More tools).',
'',
`**${rows.length} commands:** ${n('covered')} covered, ${n('gap')} gaps, ${n('not-for-desktop')} not for the desktop.`,
'',
'| command | status | where in the window | methods | owner step |',
'|---|---|---|---|---|',
...rows.map(r => `| \`${r.command}\` | ${r.status} | ${cell(r.where || r.problems.join('; '))} | ${r.methods.map(m => `\`${m}\``).join(', ')} | ${cell(r.owner ?? '')} |`),
'',
].join('\n');
}
if (import.meta.main) {
const { rows } = analyze();
const md = render(rows);
if (process.argv.includes('--check')) {
let cur = '';
try { cur = readFileSync(OUT, 'utf8'); } catch { /* missing */ }
if (cur !== md) { console.error('docs/desktop-coverage.md is stale: run bun tools/gen-desktop-coverage.ts'); process.exit(1); }
} else writeFileSync(OUT, md);
const gaps = rows.filter(r => r.status === 'gap');
for (const g of gaps) console.log(`gap: ${g.command}: ${g.problems.join('; ')}`);
console.log(`${rows.length} commands, ${rows.filter(r => r.status === 'covered').length} covered, ${gaps.length} gaps`);
}