What is wrong
A run with zero cases reports success. evalgate run suite.yaml --tags typo-here exits 0 with passed: true when the suite has no threshold.
src/runner.ts, runSuite (lines 557-578):
if (options.filterTags && options.filterTags.length > 0) {
const wanted = new Set(options.filterTags);
cases = cases.filter((c) => (c.tags ?? []).some((t) => wanted.has(t)));
}
...
const total = caseResults.length;
const passedCount = caseResults.filter((r) => r.passed).length;
const meanScore = total === 0 ? 0 : ...;
const thresholdMet = suite.threshold === undefined || meanScore >= suite.threshold;
const passed = thresholdMet && passedCount === total;
With total === 0: thresholdMet is true when no threshold is set, and passedCount === total is 0 === 0, so passed is true. cmdRun in src/cli/index.ts line 169 then returns 0.
Note the guard at line 572 shows the zero-case path was thought about for the mean score, but the pass verdict was not.
The interaction with compare is worse. An empty head run compared against a real baseline gives every case change: "removed" in compareRuns (src/compare.ts line 215), regressions is empty because regressions require both sides (line 209), so regressed is false and cmdCompare returns 0 (line 233). A misspelled tag, a renamed tag, or a suite whose cases all got filtered out turns the gate off completely and the CI job goes green.
That directly contradicts the tagline in HELP (line 106): "the build fails when your prompt gets dumber."
Why it matters
evalgate's entire job is to be the thing that says no. A silent no-op that reports success is the one bug class a CI gate cannot have, and suite.threshold being optional means plenty of real suites hit the vulnerable path. validateSuite in src/suite.ts already refuses an empty cases array (line 353), so an empty run is only reachable through filtering, which makes it easy to miss in testing.
Suggested approach
- In
src/runner.ts, make a zero-case run not pass. The cleanest version is an explicit total > 0 && in the passed expression, with a comment saying why.
- Surface it. A
RunResult with total: 0 should be visibly distinguishable in src/reporters/terminal.ts, not just a table with no rows.
- Decide whether
cmdRun should also warn on stderr when --tags matched nothing, naming the tags it looked for. That message is what turns a confusing red build into an obvious typo fix.
- In
src/compare.ts, consider whether an all-removed comparison should set regressed. It is a separate judgement call, so it is fine to leave it out and say so in the PR, but it should be a decision rather than an oversight.
- Add tests: in
tests/runner.test.ts, a suite run with a non-matching filterTags must not report passed: true; in tests/compare.test.ts, a head run with no cases against a populated baseline.
Comment below if you would like to take this one. I usually reply within a day.
What is wrong
A run with zero cases reports success.
evalgate run suite.yaml --tags typo-hereexits 0 withpassed: truewhen the suite has nothreshold.src/runner.ts,runSuite(lines 557-578):With
total === 0:thresholdMetistruewhen no threshold is set, andpassedCount === totalis0 === 0, sopassedistrue.cmdRuninsrc/cli/index.tsline 169 then returns 0.Note the guard at line 572 shows the zero-case path was thought about for the mean score, but the pass verdict was not.
The interaction with
compareis worse. An empty head run compared against a real baseline gives every casechange: "removed"incompareRuns(src/compare.tsline 215),regressionsis empty because regressions require both sides (line 209), soregressedisfalseandcmdComparereturns 0 (line 233). A misspelled tag, a renamed tag, or a suite whose cases all got filtered out turns the gate off completely and the CI job goes green.That directly contradicts the tagline in
HELP(line 106): "the build fails when your prompt gets dumber."Why it matters
evalgate's entire job is to be the thing that says no. A silent no-op that reports success is the one bug class a CI gate cannot have, and
suite.thresholdbeing optional means plenty of real suites hit the vulnerable path.validateSuiteinsrc/suite.tsalready refuses an emptycasesarray (line 353), so an empty run is only reachable through filtering, which makes it easy to miss in testing.Suggested approach
src/runner.ts, make a zero-case run not pass. The cleanest version is an explicittotal > 0 &&in thepassedexpression, with a comment saying why.RunResultwithtotal: 0should be visibly distinguishable insrc/reporters/terminal.ts, not just a table with no rows.cmdRunshould also warn on stderr when--tagsmatched nothing, naming the tags it looked for. That message is what turns a confusing red build into an obvious typo fix.src/compare.ts, consider whether an all-removedcomparison should setregressed. It is a separate judgement call, so it is fine to leave it out and say so in the PR, but it should be a decision rather than an oversight.tests/runner.test.ts, a suite run with a non-matchingfilterTagsmust not reportpassed: true; intests/compare.test.ts, a head run with no cases against a populated baseline.Comment below if you would like to take this one. I usually reply within a day.