Files
proffesor-for-testing__agen…/tests/unit/validation/command-eval-runner-qe-browser.test.ts
Dragan Spiridonov cda9fb3c05 fixup(adr-091): address devil's-advocate review
Single commit covering the 5 concrete findings from the qe-devils-advocate
review. Does not change the underlying CommandEvalRunner design or the 5
primitives — only the claims around them and the fixture-server hack the
yaml had silently inherited.

1. Revert promotion (G4): trust_tier 3 -> 2 in SKILL.md; ADR-091 status
   Implemented -> In Progress (2026-04-21); v3-adrs.md index row updated
   to reflect the same; counts 80->79 Implemented, 6->7 In Progress.
   The promotion gate is now documented on ADR-091: first green CI run
   of test-qe-browser.yml (unit + smoke + eval against real Vibium)
   before trust_tier 3 / Implemented are restored.

2. Fix yaml fixtures (G3): tc006/tc007 were referencing
   http://localhost:8088/qe-browser/SKILL.md.html which required an
   unstarted fixture server, so `aqe eval run --skill qe-browser` would
   have failed on a fresh machine. Rewrote both to use https://httpbin.org/html
   (matching scripts/smoke-test.sh tc006/tc007). Dropped tc009
   (poisoned-page check-injection) from the yaml — it needs the local
   serve-skills.js fixture which is out of scope for CommandEvalRunner.
   Documented as an explicit GAP comment in the yaml, noted that
   check-injection.js's severity path is unit-tested at
   tests/unit/scripts/qe-browser-check-injection.test.ts. yaml test_cases
   now 11 -> 10.

3. Correct pin comment wording (G1): the previous comment claimed
   ^26.3.18 was a "major.minor-line pin" — that's factually wrong under
   npm semver (^26.3.18 accepts 26.99.x). Rewritten to accurately
   describe the intent: major-line pin that blocks 27.0+ while
   allowing auto-uptake of 26.x security patches and additive features,
   with scripts/smoke-test.sh as the belt-and-suspenders API contract
   gate. The spec itself stays ^26.3.18 — the correct choice for an
   end-user tool under semver assumptions.

4. Add eval CI job (G2): third job in test-qe-browser.yml runs
   `aqe eval run --skill qe-browser --model claude-3.5-sonnet` against
   a real Vibium install after `npm run build`. This is the
   end-to-end proof that CommandEvalRunner dispatch works that the
   previous unit tests (all mocked CommandRunner) could not provide.
   Uploads the eval JSON as a CI artifact for post-run inspection.
   actionlint clean.

5. Scrub stray files (G5): git rm the coherence-comparison artifacts
   from docs/reports/ that were swept in by a prior `git add -A`, and
   added a focused pattern to .gitignore (not a blanket docs/reports/
   ignore) so future sessions can't repeat the mistake while intentional
   reports can still be committed.

Tests: 51 relevant tests still pass (3 files: command-eval-runner,
command-eval-runner-qe-browser, browser-engine-installer).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 14:09:37 +00:00

187 lines
6.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Integration test: CommandEvalRunner against the real qe-browser.yaml.
*
* Purpose: catch regressions where a yaml edit (new test case, new assertion
* kind, typo in a JSONPath) would silently break `aqe eval run --skill
* qe-browser`. We load the actual file shipped in .claude/skills/qe-browser/
* and feed the runner mock stdouts that match each primitive's happy-path
* JSON envelope.
*
* This test does NOT invoke Vibium or the fixture server — it verifies the
* suite is shaped correctly and that passing outputs pass.
*/
import { describe, it, expect } from 'vitest';
import * as path from 'node:path';
import * as fs from 'node:fs';
import * as yaml from 'yaml';
import {
CommandEvalRunner,
isCommandEvalSuite,
type CommandRunner,
} from '../../../src/validation/command-eval-runner.js';
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
const SKILLS_DIR = path.join(REPO_ROOT, '.claude', 'skills');
const YAML_PATH = path.join(SKILLS_DIR, 'qe-browser', 'evals', 'qe-browser.yaml');
describe('CommandEvalRunner — qe-browser integration', () => {
it('the shipped yaml is a valid command-mode suite', () => {
const raw = yaml.parse(fs.readFileSync(YAML_PATH, 'utf-8'));
expect(isCommandEvalSuite(raw)).toBe(true);
expect(raw.skill).toBe('qe-browser');
expect(raw.status).toBe('active');
expect(Array.isArray(raw.test_cases)).toBe(true);
// 10 cases: tc001–tc008 minus tc009 (poisoned-page, needs local fixture
// server) + tc010–tc011 = 10.
expect(raw.test_cases.length).toBe(10);
});
it('passes when every primitive returns its happy-path JSON envelope', async () => {
// visual-diff is called twice in the yaml (tc006 creates baseline after
// `rm -rf`, tc007 matches existing baseline). We track call order so the
// mock returns baseline_created first, then match.
let visualDiffCalls = 0;
// Track the last navigated URL so downstream checks (e.g. check-injection)
// can behave as if they're looking at that page — the real Vibium
// daemon holds navigation state across `vibium go` and later script calls.
let currentUrl = '';
const runner: CommandRunner = (cmd) => {
// Any `vibium go <url>` — with or without a `--headless` prefix and
// with or without an intervening `viewport w h` setup — updates the
// mocked browser URL. The `vibium viewport` / `vibium --headless ...`
// variants with no `go` are no-ops.
const goMatch = cmd.match(/vibium\s+(?:--headless\s+)?go\s+(\S+)/);
if (goMatch) {
currentUrl = goMatch[1];
return { status: 0, stdout: '', stderr: '' };
}
if (cmd.startsWith('vibium ')) return { status: 0, stdout: '', stderr: '' };
if (cmd.startsWith('rm -rf')) return { status: 0, stdout: '', stderr: '' };
if (cmd.includes('scripts/assert.js')) {
const intentionalFail = cmd.includes('this-does-not-exist');
if (intentionalFail) {
return {
status: 1,
stderr: '',
stdout: JSON.stringify({
status: 'failed',
output: { assert: { passed: 0, failed: 1 } },
}),
};
}
return {
status: 0,
stderr: '',
stdout: JSON.stringify({
status: 'success',
output: { assert: { passed: 1, failed: 0 } },
}),
};
}
if (cmd.includes('scripts/batch.js')) {
const stopOnFail = cmd.includes('#does-not-exist');
if (stopOnFail) {
return {
status: 1,
stderr: '',
stdout: JSON.stringify({
status: 'failed',
output: {
batch: { passedSteps: 1, totalSteps: 3, failedStep: { index: 1 } },
},
}),
};
}
return {
status: 0,
stderr: '',
stdout: JSON.stringify({
status: 'success',
output: { batch: { passedSteps: 3, totalSteps: 3 } },
}),
};
}
if (cmd.includes('scripts/visual-diff.js')) {
visualDiffCalls += 1;
const isBaseline = visualDiffCalls === 1;
return {
status: 0,
stderr: '',
stdout: JSON.stringify({
status: 'success',
output: {
visualDiff: { status: isBaseline ? 'baseline_created' : 'match' },
},
}),
};
}
if (cmd.includes('scripts/check-injection.js')) {
// tc008 only — yaml no longer includes the poisoned-page case
// (tc009 dropped; needs a local fixture server out of scope for
// CommandEvalRunner). Clean-page path always returns severity:none.
return {
status: 0,
stderr: '',
stdout: JSON.stringify({
status: 'success',
output: { checkInjection: { severity: 'none' } },
}),
};
}
if (cmd.includes('scripts/intent-score.js')) {
// tc011 navigates to /html (no form) and asks for fill_email, which
// should report partial status + zero candidates. tc010 navigates
// to /forms/post and asks for submit_form, which should find the
// submit button.
if (cmd.includes('fill_email')) {
return {
status: 0,
stderr: '',
stdout: JSON.stringify({
status: 'partial',
output: { intentScore: { intent: 'fill_email', candidateCount: 0 } },
}),
};
}
return {
status: 0,
stderr: '',
stdout: JSON.stringify({
status: 'success',
output: { intentScore: { intent: 'submit_form', candidateCount: 2 } },
}),
};
}
throw new Error(`unmocked command: ${cmd}`);
};
const cer = new CommandEvalRunner({ skillsDir: SKILLS_DIR, runner });
const result = await cer.run('qe-browser');
// Surface any assertion failures in CI logs before the final expect.
const fails = result.testResults.filter((r) => !r.passed);
for (const f of fails) {
// eslint-disable-next-line no-console
console.error(`[FAIL] ${f.testId}: ${JSON.stringify(f.failures)}`);
}
// yaml has 10 cases after tc009 (poisoned-page) was dropped — it needs
// a local fixture server out of band which is intentionally outside
// CommandEvalRunner's scope. The yaml covers tc001–tc008 (minus 009) +
// tc010–tc011 = 10 cases.
expect(result.totalTests).toBe(10);
expect(result.passed).toBe(true);
expect(result.passedTests).toBe(result.totalTests);
expect(result.criticalPassed).toBe(true);
});
});