Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion packages/cli/src/commands/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,7 @@ function reportWithFindings(overrides: Partial<CheckReport> = {}): CheckReport {
return {
ok: true,
strict: false,
browserSkipped: false,
lint: { ...emptySection(), filesScanned: 0 },
runtime: emptySection(),
layout: {
Expand Down Expand Up @@ -1005,6 +1006,7 @@ describe("check pipeline", () => {
const envelope = JSON.parse(output);
expect(envelope).toMatchObject({
ok: true,
browserSkipped: false,
lint: { ok: true },
runtime: { ok: true },
layout: { ok: true },
Expand All @@ -1015,7 +1017,7 @@ describe("check pipeline", () => {
});
});

it("short-circuits on lint errors without launching a browser", async () => {
it("short-circuits on lint errors without launching a browser, and flags the skipped sections", async () => {
const lint = lintWith(
"error",
"root_missing_composition_id",
Expand All @@ -1027,6 +1029,32 @@ describe("check pipeline", () => {
expect(checkExitCode(report)).toBe(1);
expect(report.lint.findings).toHaveLength(1);
expect(browser).not.toHaveBeenCalled();
// PRINFRA-700: runtime/layout/motion/contrast report the same ok:true/
// zero-findings shape whether the browser ran clean or never launched at
// all — browserSkipped is the only thing that tells the two apart.
expect(report.browserSkipped).toBe(true);
expect(report.runtime).toMatchObject({ ok: true, errorCount: 0, findings: [] });
expect(report.layout).toMatchObject({ ok: true, errorCount: 0, findings: [], duration: 0 });
expect(report.motion).toMatchObject({ ok: true, errorCount: 0, findings: [] });
expect(report.contrast).toMatchObject({ ok: true, errorCount: 0, findings: [] });
});

it("marks browserSkipped false once a browser session actually runs", async () => {
const { report } = await runScenario(fakeDriver());
expect(report.browserSkipped).toBe(false);
});

it("marks browserSkipped true when the browser session throws before producing results", async () => {
const { deps } = dependencies(fakeDriver());
deps.runBrowserCheck = vi.fn(async () => {
throw new Error("Chrome launch failed");
});
const report = await runCheckPipeline(PROJECT, DEFAULT_CHECK_OPTIONS, deps);

expect(report.ok).toBe(false);
expect(report.browserSkipped).toBe(true);
expect(report.runtime.findings).toHaveLength(1);
expect(report.layout).toMatchObject({ ok: true, errorCount: 0, findings: [] });
});

it("gates AA contrast failures and --no-contrast skips the pass", async () => {
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/commands/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,12 @@ function nonNegativeNumber(value: unknown, fallback: number): number {

function printHumanReport(report: CheckReport): void {
printSection("Lint", report.lint);
if (report.browserSkipped) {
console.log();
console.log(
` ${c.warn("⚠")} Browser session never ran — layout, motion, and contrast below are empty placeholders, not a clean pass.`,
);
}
printSection("Runtime", report.runtime);
printLayoutSection("Layout", report.layout);
printSection("Motion", report.motion);
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/utils/checkPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,7 @@ export async function runAuditGrid(
contrastPassed: contrast.passed,
screenshots: collected.screenshots,
timings: { launchSettleMs: 0, seekLoopMs, contrastMs: collected.contrastMs },
skipped: false,
};
}

Expand Down Expand Up @@ -1375,6 +1376,7 @@ function buildReport(
const report: CheckReport = {
ok: errorCount === 0 && (!options.strict || warningCount === 0),
strict: options.strict,
browserSkipped: browser.skipped,
lint,
runtime,
layout,
Expand Down Expand Up @@ -1499,6 +1501,7 @@ function emptyBrowserResult(): CheckBrowserResult {
contrastPassed: 0,
screenshots: [],
timings: { launchSettleMs: 0, seekLoopMs: 0, contrastMs: 0 },
skipped: true,
};
}

Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/utils/checkTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,13 @@ export interface CheckBrowserResult {
contrastPassed: number;
screenshots: CheckScreenshot[];
timings: CheckTimings;
/** True when no browser session produced these results — lint blocked the
* run, the lint step itself crashed, or the browser check threw — as opposed
* to a session that ran and simply found nothing. Without this, `layout`/
* `motion`/`contrast` report the exact same `ok:true`/zero-findings shape
* either way (`runtime` may instead carry a diagnostic finding for the
* crash/throw triggers — it isn't always empty). */
skipped: boolean;
}

/** The seek-grid audit loop, injected into checkBrowser so it never imports checkPipeline back. */
Expand All @@ -264,6 +271,10 @@ export interface CheckSection<T extends CheckFinding = CheckFinding> {
export interface CheckReport {
ok: boolean;
strict: boolean;
/** Mirrors `CheckBrowserResult.skipped` — true when `runtime`/`layout`/
* `motion`/`contrast` below reflect no browser session having run, not a
* session that ran and found nothing. */
browserSkipped: boolean;
lint: CheckSection & { filesScanned: number };
runtime: CheckSection;
layout: CheckSection<AnchoredLayoutIssue> & {
Expand Down
2 changes: 1 addition & 1 deletion skills-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"files": 7
},
"hyperframes-cli": {
"hash": "06b31056db460774",
"hash": "6f3b574d0efe76d1",
"files": 11
},
"hyperframes-core": {
Expand Down
4 changes: 2 additions & 2 deletions skills/hyperframes-cli/references/lint-validate-inspect.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Lints `index.html` and all files in `compositions/`. Reports errors (must fix),
```bash
npx hyperframes check # current directory: the full browser gate
npx hyperframes check ./my-project # specific project
npx hyperframes check --json # agent-readable envelope {ok, lint, runtime, layout, motion, contrast, snapshots}
npx hyperframes check --json # agent-readable envelope {ok, browserSkipped, lint, runtime, layout, motion, contrast, snapshots}
npx hyperframes check --snapshots # also write overview frames (annotated) + per-finding crops
npx hyperframes check --samples 15 # denser timeline sweep (default 9)
npx hyperframes check --at 1.5,4,7.25 # explicit hero-frame timestamps
Expand All @@ -41,7 +41,7 @@ npx hyperframes check --no-contrast # skip the WCAG audit while iterating
npx hyperframes check --strict # exit non-zero on warnings too (default: only errors)
```

One command, one Chrome boot. `check` runs the linter first and skips the browser entirely when lint reports errors. Then it loads the bundled composition once, wires runtime listeners before navigation, and sweeps one seek grid running every audit per sample:
One command, one Chrome boot. `check` runs the linter first and skips the browser entirely when lint reports errors. When that happens (or the browser session fails to launch), `layout`/`motion`/`contrast` report the same clean `ok:true`/zero-findings shape a genuinely passing session would (`runtime` may too, unless the failure itself left a diagnostic finding there) — check the top-level `browserSkipped` field, not just those sections, before trusting a "clean" result. Otherwise it loads the bundled composition once, wires runtime listeners before navigation, and sweeps one seek grid running every audit per sample:

- **Runtime**: JavaScript console errors, unhandled exceptions, failed network requests (media-file `ERR_ABORTED` filtered out), HTTP 4xx/5xx.
- **Layout**: text extending outside its container or the canvas, text clipped by its own box, held text overlaps and occlusion (with an approximate covered fraction), children escaping clipping containers.
Expand Down
Loading