- 总体准确率
+ 总体用例通过率
${pct(overall.accuracy)}
${overall.passed}/${overall.cases} 通过
${generatedAt ? tag(`生成于 ${generatedAt.toLocaleString('zh-CN')}`, 'blue') : ''}
diff --git a/scripts/run-eval.mjs b/scripts/run-eval.mjs
index b7ce514..d9baf83 100644
--- a/scripts/run-eval.mjs
+++ b/scripts/run-eval.mjs
@@ -17,7 +17,7 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { compileEvalCase } from '../eval/fixtures.mjs';
import { simulateContractResult, checkL2Result } from '../eval/resultSim.mjs';
-import { reflectOnAnswer } from '../src/reflection.js';
+import { runL3Case } from '../eval/answerCase.mjs';
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const evalRoot = path.join(projectRoot, 'eval');
@@ -128,47 +128,6 @@ function runL2Case(evalCase) {
};
}
-// ---------------------------------------------------------------------------
-// L3
-// ---------------------------------------------------------------------------
-
-async function runL3Case(evalCase, judge) {
- const reflection = await reflectOnAnswer({
- question: evalCase.question,
- answer: evalCase.answer,
- result: evalCase.result,
- judge,
- });
- const expect = evalCase.expectReflect ?? {};
- const checks = [];
- if (expect.shouldPass !== undefined) {
- checks.push({
- assertion: 'shouldPass',
- ok: reflection.passed === expect.shouldPass,
- expected: expect.shouldPass,
- actual: reflection.passed,
- });
- }
- if (expect.needsReplan !== undefined) {
- checks.push({
- assertion: 'needsReplan',
- ok: reflection.needsReplan === expect.needsReplan,
- expected: expect.needsReplan,
- actual: reflection.needsReplan,
- });
- }
- const judgePending = reflection.degraded && !reflection.judgeUsed;
- return {
- id: evalCase.id,
- description: evalCase.description ?? '',
- passed: checks.every((check) => check.ok),
- judgeAvailable: reflection.judgeUsed,
- judgePending,
- checks,
- reflection,
- };
-}
-
// ---------------------------------------------------------------------------
// baseline / diff
// ---------------------------------------------------------------------------
@@ -234,7 +193,10 @@ export async function runLevel(level, { judge = null } = {}) {
const baseline = readBaseline();
const baselineDiff = diffBaseline(baseline, level, results);
const judgePending = results.filter((result) => result.judgePending).length;
+ // Legacy judgeRan follows reflection.judgeUsed; it does not imply a usable verdict.
const judgeRan = results.filter((result) => result.judgeAvailable).length;
+ const judgeAttempted = results.filter((result) => result.judgeAttempted).length;
+ const judgeCompleted = results.filter((result) => result.judgeCompleted).length;
return {
level,
@@ -245,6 +207,8 @@ export async function runLevel(level, { judge = null } = {}) {
passRate: results.length > 0 ? passed / results.length : 0,
judgePending,
judgeRan,
+ judgeAttempted,
+ judgeCompleted,
},
gateBlockDistribution,
results,
@@ -348,7 +312,8 @@ async function main() {
const r = allReport.levels[lvl];
console.log(`[eval] ${lvl}: ${r.totals.passed}/${r.totals.cases} passed (pass rate ${(r.totals.passRate * 100).toFixed(1)}%)`
+ (r.totals.judgePending ? `, judgePending=${r.totals.judgePending}` : '')
- + (r.totals.judgeRan ? `, judgeRan=${r.totals.judgeRan}` : ''));
+ + (r.totals.judgeAttempted ? `, judgeAttempted=${r.totals.judgeAttempted}` : '')
+ + (r.totals.judgeCompleted ? `, judgeCompleted=${r.totals.judgeCompleted}` : ''));
if (Object.keys(r.gateBlockDistribution).length > 0) {
console.log(`[eval] ${lvl} gate blocks:`, JSON.stringify(r.gateBlockDistribution));
}
@@ -365,7 +330,7 @@ async function main() {
}
}
}
- console.log(`[eval] overall accuracy: ${(report.overall.accuracy * 100).toFixed(1)}% (${report.overall.passed}/${report.overall.cases})`);
+ console.log(`[eval] overall fixture pass rate: ${(report.overall.accuracy * 100).toFixed(1)}% (${report.overall.passed}/${report.overall.cases})`);
console.log(`[eval] report -> ${path.relative(projectRoot, reportPath)}`);
if (updateBaseline) {
diff --git a/tests/evalJudge.browser.test.js b/tests/evalJudge.browser.test.js
new file mode 100644
index 0000000..20250a9
--- /dev/null
+++ b/tests/evalJudge.browser.test.js
@@ -0,0 +1,47 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { runAllLevels } from '../scripts/run-eval.mjs';
+import { TEST_PASSWORD, withSkillServer } from './fixtures/skillServer.js';
+
+test('browser: eval shows degraded judges as pending and only valid judgments as completed', {
+ skip: !process.env.YOLO_EVAL_BROWSER_TEST, timeout: 90000,
+}, async () => {
+ const { chromium, expect } = await import('@playwright/test');
+ await withSkillServer(async ({ baseUrl }) => {
+ const browser = await chromium.launch({ headless: true, channel: process.env.YOLO_BROWSER_CHANNEL });
+ try {
+ const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
+ const errors = [];
+ page.on('pageerror', (error) => errors.push(error.message));
+ let report = await runAllLevels({ judge: async () => 'not-json' });
+ await page.route('**/api/eval', (route) => route.fulfill({ json: report }));
+ await page.goto(`${baseUrl}/eval`);
+ await page.locator('#loginUsername').fill('admin');
+ await page.locator('#loginPassword').fill(TEST_PASSWORD);
+ await page.getByRole('button', { name: '登录', exact: true }).click();
+ const pending = page.locator('.tag-amber', { hasText: 'Judge 待有效判定' });
+ const completed = page.locator('.tag-teal', { hasText: 'Judge 已完成判定' });
+ await expect(pending).toHaveText('Judge 待有效判定 2');
+ await expect(completed).toHaveCount(0);
+ await expect(page.getByText('总体用例通过率', { exact: true })).toBeVisible();
+
+ let calls = 0;
+ report = await runAllLevels({ judge: async () => ++calls === 1
+ ? JSON.stringify({ answersQuestion: true, consistentWithData: true, needsReplan: false, reason: 'ok', hint: '' })
+ : 'not-json' });
+ await page.locator('[data-eval-refresh]').click();
+ await expect(pending).toHaveText('Judge 待有效判定 1');
+ await expect(completed).toHaveText('Judge 已完成判定 1');
+
+ report = await runAllLevels({ judge: async () => JSON.stringify({
+ answersQuestion: true, consistentWithData: true, needsReplan: false, reason: 'ok', hint: '',
+ }) });
+ await page.locator('[data-eval-refresh]').click();
+ await expect(pending).toHaveCount(0);
+ await expect(completed).toHaveText('Judge 已完成判定 2');
+ assert.deepEqual(errors, []);
+ } finally {
+ await browser.close();
+ }
+ });
+});
diff --git a/tests/evalJudge.test.mjs b/tests/evalJudge.test.mjs
new file mode 100644
index 0000000..4f94004
--- /dev/null
+++ b/tests/evalJudge.test.mjs
@@ -0,0 +1,89 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { runLevel, runAllLevels } from '../scripts/run-eval.mjs';
+import { runL3Case } from '../eval/answerCase.mjs';
+
+const validVerdict = JSON.stringify({
+ answersQuestion: true, consistentWithData: true, needsReplan: false,
+ reason: 'Matches the supplied data', hint: '',
+});
+
+for (const { name, judge, attempted, legacyRan } of [
+ { name: 'missing', judge: null, attempted: 0, legacyRan: 0 },
+ { name: 'throws', judge: async () => { throw new Error('offline'); }, attempted: 2, legacyRan: 0 },
+ { name: 'malformed', judge: async () => 'not-json', attempted: 2, legacyRan: 2 },
+ { name: 'empty', judge: async () => '', attempted: 2, legacyRan: 2 },
+ { name: 'null response', judge: async () => null, attempted: 2, legacyRan: 2 },
+ { name: 'unrelated JSON', judge: async () => '{}', attempted: 2, legacyRan: 2 },
+]) {
+ test(`L3 keeps ${name} judgments pending while retaining deterministic fixture checks`, async () => {
+ const report = await runLevel('L3', { judge });
+ assert.equal(report.totals.judgePending, 2);
+ assert.equal(report.totals.judgeAttempted, attempted);
+ assert.equal(report.totals.judgeCompleted, 0);
+ assert.equal(report.totals.judgeRan, legacyRan);
+ assert.equal(report.totals.cases, 2);
+ assert.equal(report.totals.passed, 2);
+ assert.equal(report.totals.failed, 0);
+ assert.equal(report.totals.passRate, 1);
+ assert.deepEqual(report.results.map((r) => r.reflection.passed), [true, false]);
+ for (const result of report.results) {
+ assert.equal(result.judgePending, true);
+ assert.equal(result.judgeAttempted, attempted > 0);
+ assert.equal(result.judgeCompleted, false);
+ assert.equal(result.judgeAvailable, legacyRan > 0);
+ assert.equal(result.reflection.degraded, true);
+ }
+ });
+}
+
+test('a valid judge completes L3 judgments without masking a deterministic numeric failure', async () => {
+ const report = await runLevel('L3', { judge: async () => validVerdict });
+ assert.equal(report.totals.judgePending, 0);
+ assert.equal(report.totals.judgeAttempted, 2);
+ assert.equal(report.totals.judgeCompleted, 2);
+ assert.equal(report.totals.judgeRan, 2);
+ assert.equal(report.totals.passed, 2);
+ assert.deepEqual(report.results.map((r) => r.reflection.passed), [true, false]);
+ assert.ok(report.results.every((r) => r.judgeCompleted && !r.judgePending));
+});
+
+test('a no-data fixture skips judgment without becoming pending or attempted', async () => {
+ const result = await runL3Case({
+ id: 'no-data', question: '销售额', answer: '请补充时间范围',
+ result: { columns: [], rows: [] },
+ expectReflect: { shouldPass: true, needsReplan: false },
+ }, async () => { throw new Error('no-data fixtures must skip the judge'); });
+ assert.equal(result.passed, true);
+ assert.equal(result.judgePending, false);
+ assert.equal(result.judgeAttempted, false);
+ assert.equal(result.judgeCompleted, false);
+ assert.equal(result.judgeAvailable, false);
+});
+
+test('valid negative judgments count as completed and still fail mismatching fixture expectations', async () => {
+ const report = await runLevel('L3', { judge: async () => JSON.stringify({
+ answersQuestion: false, consistentWithData: false, needsReplan: true,
+ reason: 'Off topic', hint: 'Change the query dimension',
+ }) });
+ assert.equal(report.totals.judgeCompleted, 2);
+ assert.equal(report.totals.judgePending, 0);
+ assert.equal(report.totals.passed, 0);
+ assert.equal(report.totals.failed, 2);
+ assert.ok(report.results.every((r) => r.reflection.needsReplan));
+});
+
+test('all-level report keeps fixture totals consistent while L3 judgments are pending', async () => {
+ const report = await runAllLevels({ judge: async () => 'not-json' });
+ assert.equal(report.levels.L3.totals.judgePending, 2);
+ assert.equal(report.levels.L3.totals.judgeCompleted, 0);
+ for (const level of ['L1', 'L2']) {
+ assert.equal(report.levels[level].totals.judgeAttempted, 0);
+ assert.equal(report.levels[level].totals.judgeCompleted, 0);
+ assert.equal(report.levels[level].totals.judgePending, 0);
+ }
+ assert.equal(report.overall.cases, Object.values(report.levels).reduce((sum, r) => sum + r.totals.cases, 0));
+ assert.equal(report.overall.passed, report.overall.cases);
+ assert.equal(report.overall.failed, 0);
+ assert.equal(report.overall.accuracy, 1);
+});
From ece5716f7265180db4e9bafc2e9b3a23ffd8203c Mon Sep 17 00:00:00 2001
From: BigDataDZ <1437100870@qq.com>
Date: Fri, 16 Oct 2026 17:36:11 +0800
Subject: [PATCH 2/2] ci: run eval judgment browser regression
---
.github/workflows/ci.yml | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 73a8b2b..8173a35 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -83,6 +83,10 @@ jobs:
YOLO_SKILL_BROWSER_TEST: '1'
YOLO_SKILL_BROWSER_ARTIFACT_DIR: artifacts/skill-browser
run: node --test tests/skillCatalog.browser.test.js
+ - name: Browser eval judgment status acceptance
+ env:
+ YOLO_EVAL_BROWSER_TEST: '1'
+ run: node --test tests/evalJudge.browser.test.js
- uses: actions/upload-artifact@v4
if: always()
with: