mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
cda9fb3c05
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>
187 lines
6.6 KiB
TypeScript
187 lines
6.6 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|
||
});
|