(check: EvalCheck, outcome: Outcome)
| 64 | |
| 65 | /** Evaluate a task's checks against an observed outcome. Pure. */ |
| 66 | export function evaluateChecks(check: EvalCheck, outcome: Outcome): CheckResult { |
| 67 | const reasons: string[] = []; |
| 68 | const existing = new Set(outcome.existingFiles); |
| 69 | |
| 70 | if (outcome.error) { |
| 71 | reasons.push(`run error: ${outcome.error}`); |
| 72 | } |
| 73 | |
| 74 | for (const p of check.filesExist ?? []) { |
| 75 | if (!existing.has(p)) reasons.push(`expected file missing: ${p}`); |
| 76 | } |
| 77 | |
| 78 | for (const fc of check.fileChecks ?? []) { |
| 79 | const content = outcome.fileContents[fc.path]; |
| 80 | if (content == null) { |
| 81 | reasons.push(`file not found for content check: ${fc.path}`); |
| 82 | continue; |
| 83 | } |
| 84 | if (fc.contains != null && !content.includes(fc.contains)) { |
| 85 | reasons.push(`${fc.path} does not contain "${fc.contains}"`); |
| 86 | } |
| 87 | if (fc.matches != null) { |
| 88 | let re: RegExp | null = null; |
| 89 | try { re = new RegExp(fc.matches); } catch { reasons.push(`invalid regex for ${fc.path}: ${fc.matches}`); } |
| 90 | if (re && !re.test(content)) reasons.push(`${fc.path} does not match /${fc.matches}/`); |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | if (check.command) { |
| 95 | const expectZero = check.command.expectExitZero ?? true; |
| 96 | const code = outcome.commandExitCode; |
| 97 | if (expectZero && code !== 0) reasons.push(`command "${check.command.command}" exited ${code ?? 'null'} (expected 0)`); |
| 98 | if (!expectZero && code === 0) reasons.push(`command "${check.command.command}" exited 0 (expected non-zero)`); |
| 99 | } |
| 100 | |
| 101 | return { passed: reasons.length === 0, reasons }; |
| 102 | } |
| 103 | |
| 104 | export interface TaskRunResult { |
| 105 | id: string; |
no test coverage detected