|
| 1 | +#!/usr/bin/env node |
| 2 | +// @ts-check |
| 3 | +import { readFileSync } from 'fs'; |
| 4 | +import { execSync } from 'child_process'; |
| 5 | + |
| 6 | +const THRESHOLD = Number(process.env.THRESHOLD ?? 60); |
| 7 | +const SUMMARY_PATH = 'coverage/coverage-summary.json'; |
| 8 | +const REPO = process.env.GITHUB_REPOSITORY; // e.g. "microsoft/vscode-cmake-tools" |
| 9 | +const RUN_URL = `https://github.com/${REPO}/actions/runs/${process.env.GITHUB_RUN_ID}`; |
| 10 | + |
| 11 | +// ── 1. Read Istanbul JSON summary ──────────────────────────────────────────── |
| 12 | +let summary; |
| 13 | +try { |
| 14 | + summary = JSON.parse(readFileSync(SUMMARY_PATH, 'utf8')); |
| 15 | +} catch { |
| 16 | + console.log('No coverage summary found — skipping issue creation.'); |
| 17 | + process.exit(0); |
| 18 | +} |
| 19 | + |
| 20 | +// ── 2. Find files below threshold ──────────────────────────────────────────── |
| 21 | +const belowThreshold = []; |
| 22 | + |
| 23 | +for (const [file, metrics] of Object.entries(summary)) { |
| 24 | + if (file === 'total') continue; |
| 25 | + if (!file.startsWith('src/')) continue; |
| 26 | + |
| 27 | + const linePct = metrics.lines.pct ?? 0; |
| 28 | + const branchPct = metrics.branches.pct ?? 0; |
| 29 | + const fnPct = metrics.functions.pct ?? 0; |
| 30 | + |
| 31 | + if (linePct < THRESHOLD || fnPct < THRESHOLD) { |
| 32 | + belowThreshold.push({ file, linePct, branchPct, fnPct }); |
| 33 | + } |
| 34 | +} |
| 35 | + |
| 36 | +const total = summary.total ?? {}; |
| 37 | +const totalLines = total.lines?.pct ?? 0; |
| 38 | + |
| 39 | +console.log(`\nTotal line coverage: ${totalLines}% (threshold: ${THRESHOLD}%)`); |
| 40 | +console.log(`Files below threshold: ${belowThreshold.length}`); |
| 41 | + |
| 42 | +if (belowThreshold.length === 0 && totalLines >= THRESHOLD) { |
| 43 | + console.log('✅ Coverage is above threshold — no issue needed.'); |
| 44 | + process.exit(0); |
| 45 | +} |
| 46 | + |
| 47 | +// ── 3. Build the issue body (this is the Copilot agent's instruction set) ──── |
| 48 | +belowThreshold.sort((a, b) => a.linePct - b.linePct); // worst files first |
| 49 | + |
| 50 | +const tableRows = belowThreshold |
| 51 | + .slice(0, 20) // cap at 20 files per issue to keep it focused |
| 52 | + .map(f => `| \`${f.file}\` | ${f.linePct}% | ${f.branchPct}% | ${f.fnPct}% |`) |
| 53 | + .join('\n'); |
| 54 | + |
| 55 | +const fileList = belowThreshold |
| 56 | + .slice(0, 20) |
| 57 | + .map(f => `- \`${f.file}\` — ${f.linePct}% line coverage`) |
| 58 | + .join('\n'); |
| 59 | + |
| 60 | +const issueBody = `\ |
| 61 | +## Coverage below ${THRESHOLD}% threshold |
| 62 | +
|
| 63 | +> **This issue is the instruction set for the GitHub Copilot coding agent.** |
| 64 | +> Copilot: read this entire body before writing a single line of code. |
| 65 | +
|
| 66 | +**Coverage run:** ${RUN_URL} |
| 67 | +**Total line coverage:** ${totalLines}% — threshold is ${THRESHOLD}% |
| 68 | +
|
| 69 | +### Files requiring new tests |
| 70 | +
|
| 71 | +| File | Lines | Branches | Functions | |
| 72 | +|------|-------|----------|-----------| |
| 73 | +${tableRows} |
| 74 | +
|
| 75 | +--- |
| 76 | +
|
| 77 | +### Agent instructions |
| 78 | +
|
| 79 | +You are improving test coverage in \`microsoft/vscode-cmake-tools\`. |
| 80 | +Read \`.github/copilot-test-coverage.md\` before starting — it contains the |
| 81 | +mandatory self-audit protocol and test quality rules for this repo. |
| 82 | +
|
| 83 | +**Files to cover (worst first):** |
| 84 | +${fileList} |
| 85 | +
|
| 86 | +For each file: |
| 87 | +1. Read the source file fully before writing any test |
| 88 | +2. Identify the module's exported API surface |
| 89 | +3. Write tests in \`test/unit-tests/\` that cover the uncovered branches |
| 90 | +4. Run the self-audit steps from \`copilot-test-coverage.md\` |
| 91 | +5. Only open the PR after every self-audit step passes |
| 92 | +
|
| 93 | +### Self-audit checklist (must be checked before opening PR) |
| 94 | +
|
| 95 | +- [ ] \`yarn backendTests\` passes with no new failures |
| 96 | +- [ ] \`yarn unitTests\` passes with no new failures |
| 97 | +- [ ] Each file listed above improved by ≥ 10 percentage points OR reached ≥ ${THRESHOLD}% line coverage |
| 98 | +- [ ] No test uses \`assert.ok(true)\` or is an empty stub |
| 99 | +- [ ] Test names describe behavior: \`'expandString handles undefined variable'\` not \`'test 1'\` |
| 100 | +- [ ] No test depends on another test's side effects |
| 101 | +- [ ] Presets-mode and kits/variants mode both exercised where the source branches on \`useCMakePresets\` |
| 102 | +- [ ] Single-config and multi-config generator paths both tested where relevant |
| 103 | +- [ ] \`yarn lint\` passes |
| 104 | +- [ ] \`CHANGELOG.md\` has an entry under \`Improvements:\` |
| 105 | +
|
| 106 | +### Constraints |
| 107 | +
|
| 108 | +- Tests go in \`test/unit-tests/\` — use Mocha \`suite\`/\`test\` with \`assert\` |
| 109 | +- Import source under test via the \`@cmt/\` path alias |
| 110 | +- Do **not** open the PR as a draft if the self-audit fails — fix it first |
| 111 | +- Do **not** touch source files outside \`test/\` |
| 112 | +`; |
| 113 | + |
| 114 | +// ── 4. Open the issue via gh CLI ────────────────────────────────────────────── |
| 115 | +const title = `chore: improve test coverage -- ${belowThreshold.length} files below ${THRESHOLD}% (run ${new Date().toISOString().slice(0, 10)})`; |
| 116 | + |
| 117 | +const cmd = [ |
| 118 | + 'gh', 'issue', 'create', |
| 119 | + '--repo', REPO, |
| 120 | + '--title', JSON.stringify(title), |
| 121 | + '--body', JSON.stringify(issueBody), |
| 122 | + '--label', 'test-coverage', |
| 123 | +].join(' '); |
| 124 | + |
| 125 | +console.log(`\nOpening issue: ${title}`); |
| 126 | +execSync(cmd, { stdio: 'inherit' }); |
0 commit comments