/ src / verify.ts
verify.ts
 1  /**
 2   * Verification: runs validation and optional smoke test.
 3   *
 4   * The smoke test is intentionally kept as a stub — full browser-based
 5   * smoke testing requires a running browser session and is better suited
 6   * to the `opencli test` command or CI pipelines.
 7   */
 8  
 9  import { validateClisWithTarget, renderValidationReport, type ValidationReport } from './validate.js';
10  import { spawn } from 'node:child_process';
11  import * as fs from 'node:fs';
12  import * as path from 'node:path';
13  
14  export interface VerifyOptions {
15    builtinClis: string;
16    userClis: string;
17    target?: string;
18    smoke?: boolean;
19  }
20  
21  export interface VerifyReport {
22    ok: boolean;
23    validation: ValidationReport;
24    smoke: null | {
25      requested: boolean;
26      executed: boolean;
27      ok: boolean;
28      summary: string;
29    };
30  }
31  
32  export async function verifyClis(opts: VerifyOptions): Promise<VerifyReport> {
33    const report = validateClisWithTarget([opts.builtinClis, opts.userClis], opts.target);
34    let smoke: VerifyReport['smoke'] = null;
35    if (opts.smoke) {
36      smoke = await runSmokeTests(opts.builtinClis);
37    }
38    return { ok: report.ok && (smoke?.ok ?? true), validation: report, smoke };
39  }
40  
41  export function renderVerifyReport(report: VerifyReport): string {
42    const base = renderValidationReport(report.validation);
43    if (!report.smoke) return base;
44    const status = report.smoke.ok ? 'PASS' : 'FAIL';
45    const mode = report.smoke.executed ? 'executed' : 'skipped';
46    return `${base}\nSmoke: ${status} (${mode}) — ${report.smoke.summary}`;
47  }
48  
49  async function runSmokeTests(builtinClis: string): Promise<NonNullable<VerifyReport['smoke']>> {
50    const projectRoot = path.resolve(builtinClis, '..', '..');
51    const smokeDir = path.join(projectRoot, 'tests', 'smoke');
52  
53    if (!fs.existsSync(smokeDir)) {
54      return {
55        requested: true,
56        executed: false,
57        ok: false,
58        summary: 'Smoke tests are unavailable in this package/environment.',
59      };
60    }
61  
62    const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
63    return new Promise((resolve) => {
64      const child = spawn(npx, ['vitest', 'run', 'tests/smoke/', '--reporter=dot'], {
65        cwd: projectRoot,
66        env: { ...process.env },
67        stdio: ['ignore', 'pipe', 'pipe'],
68      });
69  
70      let stderr = '';
71      child.stderr.on('data', (chunk) => {
72        stderr += chunk.toString();
73      });
74  
75      child.on('error', (error) => {
76        resolve({
77          requested: true,
78          executed: false,
79          ok: false,
80          summary: `Failed to start smoke tests: ${error.message}`,
81        });
82      });
83  
84      child.on('close', (code) => {
85        resolve({
86          requested: true,
87          executed: true,
88          ok: code === 0,
89          summary: code === 0 ? 'tests/smoke passed' : (stderr.trim() || `vitest exited with code ${code}`),
90        });
91      });
92    });
93  }