Files
Manav Arya Singh f59fc8040d fix(ci): partial CI report on extraction failure (preserves stack via cause)
Replaces the (now-closed) PR #76 with a tighter version of the same idea.

Before: a Playwright crash, DNS miss, or ad-walled URL would surface as
an unhandled exception inside `runCi()` — the CI artifact directory ended
up empty, and downstream jobs (artifact upload, status comments,
dashboards) had nothing to point at.

Now:

  1. Wrap only the actual throw site (`extractDesignLanguage(url)`).
  2. On failure, write a partial `ci-report.md` with the message + full
     stack as a fenced block, plus a `ci-summary.json` keyed identically
     to the success path (`{url, score, grade, driftVerdict, timestamp}`)
     with two extra failure fields (`extractionFailed: true`, `error`).
  3. Surface the original error via `cause` on the returned object so
     callers can re-raise with a real stack — none of this string-prepending
     wrapping that loses the trace (which was the main issue with #76).
  4. Append the partial report to `GITHUB_STEP_SUMMARY` if present so
     GitHub Actions runs still get a step summary on the failure.

Tests (tests/ci.test.js, 4 new):

  - returns shouldFail=true without throwing
  - writes ci-report.md + ci-summary.json on failure
  - summary preserves the success-path schema + adds extractionFailed flag
  - failure is a structured return, never an exception

Test exercises the path with an unreachable host (DNS resolves fast).

371/371 tests pass.

Closes #76 conceptually (PR closed per code review at
https://github.com/Manavarya09/design-extract/pull/76#issuecomment-4406699448).
2026-05-08 17:20:21 +04:00

84 lines
3.6 KiB
JavaScript
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.
// CI failure-path tests.
//
// Covers the v12.6.x ci.js fix: when extractDesignLanguage() throws —
// Playwright crash, network timeout, DNS miss, ad-walled URL — the runner
// must NOT propagate the raw error. It must:
// - write a partial ci-report.md (so artifact uploads aren't empty)
// - write a ci-summary.json with extractionFailed=true and the message
// - return { shouldFail: true, cause } so the original stack survives
//
// We exercise this by pointing runCi at an unresolvable host. DNS fails
// fast (~50500ms) so the test stays under a few seconds.
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync, readFileSync, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { runCi } from '../src/ci.js';
// Async-aware: must `await fn(dir)` so the cleanup doesn't run before the
// inner runCi() finishes writing files. (Same helper in formatters.test.js
// is sync-only and got copy-pasted incorrectly the first time around.)
async function withTempDir(fn) {
const dir = mkdtempSync(join(tmpdir(), 'dl-ci-test-'));
try { return await fn(dir); } finally { rmSync(dir, { recursive: true, force: true }); }
}
const UNREACHABLE = 'https://this-host-does-not-exist-XYZ.invalid';
describe('runCi — extraction-failure path', () => {
it('returns shouldFail=true without throwing when extraction blows up', async () => {
await withTempDir(async (dir) => {
const r = await runCi(UNREACHABLE, { out: dir });
assert.equal(r.shouldFail, true);
assert.ok(r.cause, 'must surface original error via cause');
assert.ok(r.cause.message, 'cause must carry a message');
});
});
it('writes a ci-report.md and ci-summary.json even on failure', async () => {
await withTempDir(async (dir) => {
await runCi(UNREACHABLE, { out: dir });
const mdPath = join(dir, 'ci-report.md');
const sumPath = join(dir, 'ci-summary.json');
assert.ok(existsSync(mdPath), 'ci-report.md missing on failure');
assert.ok(existsSync(sumPath), 'ci-summary.json missing on failure');
const md = readFileSync(mdPath, 'utf-8');
assert.match(md, /## designlang/);
assert.match(md, /failed/);
// The failure block must contain a fenced code block (the stack).
assert.match(md, /```[\s\S]*?```/);
});
});
it('summary preserves the success-path schema + adds extractionFailed flag', async () => {
await withTempDir(async (dir) => {
const r = await runCi(UNREACHABLE, { out: dir });
const sum = r.summary;
// Schema must match the success path: url, score, grade, driftVerdict, timestamp.
for (const key of ['url', 'score', 'grade', 'driftVerdict', 'timestamp']) {
assert.ok(key in sum, `summary missing key: ${key}`);
}
// Plus the failure-only flags.
assert.equal(sum.extractionFailed, true);
assert.equal(typeof sum.error, 'string');
assert.equal(sum.score, null, 'score should be null on failure');
assert.equal(sum.grade, null, 'grade should be null on failure');
assert.equal(sum.driftVerdict, 'unknown');
assert.equal(sum.url, UNREACHABLE);
});
});
it('does not throw — failure must be a structured return value', async () => {
await withTempDir(async (dir) => {
// assert.doesNotThrow doesn't wait on async, so we wrap manually.
let threw = null;
try { await runCi(UNREACHABLE, { out: dir }); } catch (e) { threw = e; }
assert.equal(threw, null, 'runCi must return on extraction failure, not throw');
});
});
});