mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-06 00:44:07 +01:00
fix: craft check skips mount-node shells and script bodies; audit capture order (GSTA-92)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
bed645a20e
commit
216987e080
7 files changed
+44
-22
No files matched your search
+3
-3
@@ -5,9 +5,9 @@
|
||||
### Changed
|
||||
- **The archetype the user picks on the /design-shotgun board now reaches /design-html.** Captions name the archetype id (`classic-hero`, `long-form-story`, `minimal-conversion`, `comparison`). On approval, approved.json also gets the chosen `archetype` and its `sections` outline, added with the Edit tool so no free text reaches a shell command. /design-html reads both in Case A, and the craft section uses them as given instead of choosing again or asking.
|
||||
- **Accepted craft exceptions persist.** /design-html's finalized.json records each accepted `CRAFT_CHECK` finding in `craft_exceptions` (`id`, `count`, `reason`) and carries forward earlier ones. A later run passes that file with `--accepted`, so a finding the user already accepted is reported as accepted instead of flagged again. A higher count is still flagged as a new issue.
|
||||
- **/design-review audits a Persuade page or a redesign for craft, or on request** (`sections/craft-audit.md`, read in Phases 1-6; design-review is now carved on every host, so the audit is one lazy section, not inlined text). It runs `bin/gstack-design-craft` on each page's served HTML (or the repo file when no app runs) and files each `CRAFT_CHECK` line as a category 9 finding (no `CRAFT_RESULT` line means the check was unavailable, and a framework mount-node shell counts as framework source; the saved page holds only inline CSS and JS, so tells in linked files are judged by eye; it also holds session data, so it is deleted in the block that checks it, whatever the exit code, and never copied to the report; `--accepted` takes that screen's finalized.json, only for a page /design-html finalized, and never covers `needs-proof` or `lorem`; on a live page a claim without proof is filed and asked about, never marked or cut); checks what the tool cannot see (claims only from the user, realistic sample content, the ship floor, a designed 404, cookie consent where required, the motion rules); files a failed Persuade check (one primary action, named objections, proof beside each claim, a real risk reversal, an index/noindex decision) as High impact; and in Phase 8 breaks impact ties with Elaya's fix order and reads the installed Tailwind major version before touching its config. The audit's rules render from /design-html's craft section, so the build and the audit cannot drift.
|
||||
- **/design-review audits a Persuade page or a redesign for craft, or on request** (`sections/craft-audit.md`, read in Phases 1-6; design-review is now carved on every host, so the audit is one lazy section, not inlined text). It runs `bin/gstack-design-craft` on each page's served HTML (or the repo file when no app runs) and files each `CRAFT_CHECK` line as a category 9 finding (no `CRAFT_RESULT` line means the check was unavailable, and the check prints none for a framework mount-node shell, a document whose body has no visible text, exiting 2; the saved page holds only inline CSS and JS, so tells in linked files are judged by eye; it also holds session data, so it is deleted in the block that checks it, whatever the exit code, and never copied to the report; `--accepted` takes that screen's finalized.json, only for a page /design-html finalized, and never covers `needs-proof` or `lorem`; on a live page a claim without proof is filed and asked about, never marked or cut); checks what the tool cannot see (claims only from the user, realistic sample content, the ship floor, a designed 404, cookie consent where required, the motion rules); files a failed Persuade check (one primary action, named objections, proof beside each claim, a real risk reversal, an index/noindex decision) as High impact; and in Phase 8 breaks impact ties with Elaya's fix order and reads the installed Tailwind major version before touching its config. The audit's rules render from /design-html's craft section, so the build and the audit cannot drift.
|
||||
- **/plan-design-review**: a Persuade plan gets the same five checks in Pass 1, Pass 4 rejects unsourced proof, and the runner checklist names them.
|
||||
- **The craft tells are catalog entries.** Placeholder people and brands, lorem, round sample numbers, `[Needs proof:` markers, "Oops" errors, scroll-listener reveals, dead `href="#"`, `alert()`, images without alt, `100vh`, and a missing title, description, favicon, skip link or `aria-current` live in `lib/design-catalog.ts` as `checkerOnly` entries: one id, one short line and the fix, reported by the craft check and kept out of Methodology category 9, `review/design-checklist.md` and the consultation bullets, so no skill carries their prose. `lib/design-craft.ts` prints only catalog ids, with each entry's `fix` text; `transition: all` now reports under the existing `layout-transition` id instead of `transition-all`.
|
||||
- **The craft tells are catalog entries.** Placeholder people and brands, lorem, round sample numbers, `[Needs proof:` markers, "Oops" errors, scroll-listener reveals, dead `href="#"`, `alert()`, images without alt, `100vh`, and a missing title, description, favicon, skip link or `aria-current` live in `lib/design-catalog.ts` as `checkerOnly` entries: one id, one short line and the fix, reported by the craft check and kept out of Methodology category 9, `review/design-checklist.md` and the consultation bullets, so no skill carries their prose. `lib/design-craft.ts` prints only catalog ids, with each entry's `fix` text; `transition: all` now reports under the existing `layout-transition` id instead of `transition-all`. Code rules skip script bodies, so a hydration payload that repeats the markup is not counted twice, and scroll listeners are read from scripts and inline handlers.
|
||||
|
||||
### Added
|
||||
- **`gstack-design-craft --accepted <finalized.json>`**: a finding whose id an exception covers, at no more than the accepted count, prints `CRAFT_ACCEPTED: <id> <count> <reason>` and doesn't fail the check. `CRAFT_RESULT` adds the accepted count. Exit codes: 0 when only accepted findings remain, 1 for unaccepted ones, and 2 for an unknown argument or a missing, unreadable or malformed exceptions file. A bad exceptions file still gets the full check with nothing accepted, after a warning, so a broken record can't switch the craft check off. A finalized.json with no `craft_exceptions` field, written before this change, means no exceptions.
|
||||
@@ -18,7 +18,7 @@
|
||||
- `test/helpers/placeholder-allowlist.ts` lists `<path-to-page.html>` (path grammar), the saved page the craft audit passes to the check.
|
||||
- The design-review and plan-design-review paid cases and judges that read a design resolver now also select on `scripts/resolvers/design-craft.ts`, and `design-html-slop-gate` on `lib/design-catalog.ts`, where the craft check's ids and fixes now live.
|
||||
- `test/design-craft.test.ts`: every id the check prints is a grep-detectable catalog entry, and the fix it prints is the catalog's. design-review gets its first carve guard (skeleton, union and parity ratio at their measured sizes).
|
||||
- `test/design-craft.test.ts` also pins that `checkerOnly` entries reach no rendered list while the craft check prints each one, that a change to `scripts/resolvers/design-craft.ts` selects the design-review and plan-design-review cases, and the exception cells a stale record, an over-count tell without `--accepted` and a malformed record on disk reach. New `test/design-craft-resolvers.test.ts`: `{{DESIGN_CRAFT_RULE}}` renders one paragraph with its bullets and fails generation on a missing or ambiguous lead, and both skills carry the same five Persuade checks; the generated craft audit keeps its outcome, exception and session-data rules (and the design-review carve guard checks the last one).
|
||||
- `test/design-craft.test.ts` also pins that `checkerOnly` entries reach no rendered list while the craft check prints each one, that a change to `scripts/resolvers/design-craft.ts` selects the design-review and plan-design-review cases, and the exception cells a stale record, an over-count tell without `--accepted` and a malformed record on disk reach. New `test/design-craft-resolvers.test.ts`: `{{DESIGN_CRAFT_RULE}}` renders one paragraph with its bullets and fails generation on a missing or ambiguous lead, and both skills carry the same five Persuade checks; the generated craft audit keeps its outcome, exception and session-data rules (and the design-review carve guard checks the last one). `test/design-craft.test.ts` also covers the mount-node shell exit, a hydration payload counted once, and the needs-proof and stale-record exception cells.
|
||||
|
||||
### Gate integrity
|
||||
- Size budgets raised to measured sizes only (Garry 2026-10-25 "Do it" (GSTA-92)): the SKILL.md pins of design-review on 12 hosts (+578 Claude, +608 on the 11 external hosts: the Section index the carve test requires and the STOP pointer) and plan-design-review on the 11 external hosts (+1,082: the five Persuade plan checks). The craft tells are catalog entries the craft check reports with their fix, kept out of every rendered skill list, so design-consultation and the review checklist do not grow. No other threshold, budget, timeout, retry or trial count changed.
|
||||
|
||||
+10
-3
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
|
||||
// gstack-design-craft — free craft check for a generated page (/design-html Step 4).
|
||||
// gstack-design-craft — free craft check for a page (/design-html Step 4,
|
||||
// /design-review craft audit).
|
||||
//
|
||||
// Usage: gstack-design-craft <file.html> [--accepted <finalized.json>]
|
||||
//
|
||||
@@ -9,14 +10,15 @@
|
||||
// exception covers (same id, count at most the accepted count) prints as
|
||||
// "CRAFT_ACCEPTED: <id> <count> <reason>" and does not fail the check; the
|
||||
// result line then adds " (<k> accepted)" or ", <k> accepted".
|
||||
// Exit 2: usage error, a non-HTML file, or an unreadable or empty file, with
|
||||
// Exit 2: usage error, a non-HTML file, an unreadable or empty file, or a
|
||||
// framework mount-node shell (a document whose body has no visible text), with
|
||||
// nothing checked; or an unreadable or malformed exceptions file, after a
|
||||
// warning and a full check with no exceptions accepted, so a bad record never
|
||||
// hides the page's tells. Head and navigation rules run only on a full HTML
|
||||
// document; a fragment gets the copy and code rules alone.
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { acceptFindings, checkCraft, formatCraft, parseCraftExceptions, type CraftException } from '../lib/design-craft';
|
||||
import { acceptFindings, checkCraft, formatCraft, parseCraftExceptions, visibleText, type CraftException } from '../lib/design-craft';
|
||||
|
||||
const USAGE = 'Usage: gstack-design-craft <file.html> [--accepted <finalized.json>]';
|
||||
const [file, flag, acceptedFile, ...rest] = process.argv.slice(2);
|
||||
@@ -39,6 +41,11 @@ if (source.trim() === '') {
|
||||
console.error(`gstack-design-craft: ${file} is empty; nothing was checked`);
|
||||
process.exit(2);
|
||||
}
|
||||
const body = source.match(/<body\b[\s\S]*/i)?.[0];
|
||||
if (/<head\b/i.test(source) && body !== undefined && visibleText(body).trim() === '') {
|
||||
console.error(`gstack-design-craft: ${file} has no visible text (a framework mount-node shell?); nothing was checked, check the rendered page`);
|
||||
process.exit(2);
|
||||
}
|
||||
let exceptions: CraftException[] = [];
|
||||
let badExceptions = false;
|
||||
if (acceptedFile) {
|
||||
|
||||
@@ -6,7 +6,7 @@ Run this in Phases 1-6, on every audited page, when a page is Persuade (the clas
|
||||
|
||||
### 1. Run the craft check on each page
|
||||
|
||||
With a URL, save the page in the script that opens it. Aside: add `await fs.writeFile(path.join(pwd, "{page}.page.html"), await pg.evaluate(() => document.documentElement.outerHTML));`. Fallback engine: `_TMP=$(mktemp -d "${TMPDIR:-/tmp}/gstack-craft.XXXXXX"); $B js 'document.documentElement.outerHTML' --out "$_TMP/{page}.page.html" --raw`. With no running app, use the page's HTML file in the repo. A saved page holds session data: whatever the exit code, `rm -rf` it (or `$_TMP`) in the block that checks it, and never copy it to the report directory.
|
||||
With a URL, save the page in the script that opens it. Aside: add `await fs.writeFile(path.join(pwd, ".{page}.html"), await pg.evaluate(() => document.documentElement.outerHTML));`. Fallback engine: `_TMP=$(mktemp -d "${TMPDIR:-/tmp}/gstack-craft.XXXXXX"); $B js 'document.documentElement.outerHTML' --out "$_TMP/{page}.page.html" --raw`. A saved page holds session data: whatever the exit code, `rm -rf` it (or `${_TMP:?}`) in the block that checks it, and never copy it to the report directory. With no running app, use the page's HTML file in the repo.
|
||||
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-design-craft <path-to-page.html>
|
||||
|
||||
@@ -4,7 +4,7 @@ Run this in Phases 1-6, on every audited page, when a page is Persuade (the clas
|
||||
|
||||
### 1. Run the craft check on each page
|
||||
|
||||
With a URL, save the page in the script that opens it. Aside: add `await fs.writeFile(path.join(pwd, "{page}.page.html"), await pg.evaluate(() => document.documentElement.outerHTML));`. Fallback engine: `_TMP=$(mktemp -d "${TMPDIR:-/tmp}/gstack-craft.XXXXXX"); $B js 'document.documentElement.outerHTML' --out "$_TMP/{page}.page.html" --raw`. With no running app, use the page's HTML file in the repo. A saved page holds session data: whatever the exit code, `rm -rf` it (or `$_TMP`) in the block that checks it, and never copy it to the report directory.
|
||||
With a URL, save the page in the script that opens it. Aside: add `await fs.writeFile(path.join(pwd, ".{page}.html"), await pg.evaluate(() => document.documentElement.outerHTML));`. Fallback engine: `_TMP=$(mktemp -d "${TMPDIR:-/tmp}/gstack-craft.XXXXXX"); $B js 'document.documentElement.outerHTML' --out "$_TMP/{page}.page.html" --raw`. A saved page holds session data: whatever the exit code, `rm -rf` it (or `${_TMP:?}`) in the block that checks it, and never copy it to the report directory. With no running app, use the page's HTML file in the repo.
|
||||
|
||||
```bash
|
||||
{{BIN_DIR}}/gstack-design-craft <path-to-page.html>
|
||||
|
||||
+16
-13
@@ -1,9 +1,11 @@
|
||||
/**
|
||||
* Free craft check for a generated page (design-html Step 4). Counts the tells
|
||||
* that make a page read as generated or unfinished: placeholder content, AI
|
||||
* cliché copy, scroll listeners, 100vh, dead links, and the missing ship-floor
|
||||
* Free craft check for a page (/design-html Step 4, /design-review craft
|
||||
* audit). Counts the tells that make a page read as generated or unfinished:
|
||||
* placeholder content, AI cliché copy, scroll listeners, 100vh, dead links, and the missing ship-floor
|
||||
* tags. Copy checks read the visible text only (script, style and tags
|
||||
* removed); code checks read the source. Head checks run only on a full
|
||||
* removed); code checks read the source without script bodies, so a
|
||||
* hydration payload that repeats the markup is not counted twice, and script
|
||||
* checks read script bodies and inline handlers. Head checks run only on a full
|
||||
* document. Adapted from Elaya's landing-page-design and redesign-skill
|
||||
* (MIT); see NOTICE.md. Every id it prints is a lib/design-catalog.ts entry
|
||||
* and the fix beside it is that entry's `fix` (test-enforced): buzzwords
|
||||
@@ -49,7 +51,7 @@ const COUNT_RULES: CountRule[] = [
|
||||
{ id: 'round-number', on: 'text', pattern: /(?<![\d.])(?:99\.9+|[1-9]0|100)%\s+(?:uptime|faster|more|less|fewer|better|growth|increase|reduction|of (?:teams|users|customers|companies))\b|\b(?:\d+0|\d+(?:,\d{3})+)\+?\s+(?:happy |active )?(?:users|customers|teams|companies)\b|(?<![\d.])10x\s+(?:faster|more|better)\b/gi },
|
||||
{ id: 'needs-proof', on: 'text', pattern: /\[Needs proof:/g },
|
||||
{ id: 'oops', on: 'text', pattern: /\boops\b/gi },
|
||||
{ id: 'scroll-listener', on: 'source', pattern: /addEventListener\(\s*['"`]scroll['"`]/g },
|
||||
{ id: 'scroll-listener', on: 'script', pattern: /addEventListener\(\s*['"`]scroll['"`]/g },
|
||||
{ id: 'layout-transition', on: 'source', pattern: /\btransition\s*:\s*all\b|\btransition-all\b/g },
|
||||
{ id: 'dead-link', on: 'source', pattern: /\bhref\s*=\s*["']#["']/g },
|
||||
{ id: 'alert', on: 'script', pattern: /(?<![\w.$])(?:window\.)?alert\s*\(/g },
|
||||
@@ -87,14 +89,15 @@ export function visibleText(html: string): string {
|
||||
export function checkCraft(html: string): CraftFinding[] {
|
||||
const text = visibleText(html);
|
||||
const scripts = [...html.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi), ...html.matchAll(/\bon[a-z]+\s*=\s*"([^"]*)"/gi)].map(m => m[1]).join('\n');
|
||||
const source = html.replace(/(<script\b[^>]*>)[\s\S]*?<\/script>/gi, '$1</script>');
|
||||
const findings: CraftFinding[] = [];
|
||||
for (const rule of COUNT_RULES) {
|
||||
const count = (rule.on === 'text' ? text : rule.on === 'script' ? scripts : html).match(rule.pattern)?.length ?? 0;
|
||||
const count = (rule.on === 'text' ? text : rule.on === 'script' ? scripts : source).match(rule.pattern)?.length ?? 0;
|
||||
if (count > 0) findings.push(finding(rule.id, count));
|
||||
}
|
||||
const vh = [...html.matchAll(/\b((?:min-|max-)?height)\s*:\s*100vh\b(\s*!important)?([^}]*)/g)]
|
||||
const vh = [...source.matchAll(/\b((?:min-|max-)?height)\s*:\s*100vh\b(\s*!important)?([^}]*)/g)]
|
||||
.filter(m => m[2] || !new RegExp(`(?<![\\w-])${m[1]}\\s*:\\s*100[dsl]vh\\b`).test(m[3])).length
|
||||
+ (html.match(/\b(?:min-)?h-screen\b/g)?.length ?? 0);
|
||||
+ (source.match(/\b(?:min-)?h-screen\b/g)?.length ?? 0);
|
||||
if (vh > 0) findings.push(finding('vh-height', vh));
|
||||
if (!/<head\b/i.test(html)) return findings;
|
||||
for (const rule of HEAD_RULES) {
|
||||
@@ -136,11 +139,11 @@ export function parseCraftExceptions(json: string): CraftException[] {
|
||||
export function acceptFindings(findings: CraftFinding[], exceptions: CraftException[]): { issues: CraftFinding[]; accepted: CraftException[] } {
|
||||
const issues: CraftFinding[] = [];
|
||||
const accepted: CraftException[] = [];
|
||||
for (const finding of findings) {
|
||||
const exception = exceptions.filter(e => e.id === finding.id).sort((a, b) => b.count - a.count)[0];
|
||||
if (!exception) issues.push(finding);
|
||||
else if (finding.count <= exception.count) accepted.push({ id: finding.id, count: finding.count, reason: exception.reason });
|
||||
else issues.push({ ...finding, fix: `${finding.fix} (${exception.count} accepted earlier: ${exception.reason})` });
|
||||
for (const f of findings) {
|
||||
const exception = exceptions.filter(e => e.id === f.id).sort((a, b) => b.count - a.count)[0];
|
||||
if (!exception) issues.push(f);
|
||||
else if (f.count <= exception.count) accepted.push({ id: f.id, count: f.count, reason: exception.reason });
|
||||
else issues.push({ ...f, fix: `${f.fix} (${exception.count} accepted earlier: ${exception.reason})` });
|
||||
}
|
||||
return { issues, accepted };
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export const DESIGN_CHECKLIST_SLOP_HEADING = 'AI Slop Detection';
|
||||
|
||||
const TIER_ORDER: Record<DesignSlopEntry['confidence'], number> = { HIGH: 0, MEDIUM: 1, LOW: 2 };
|
||||
|
||||
/** Category 1: slop entries a code reader can grep for, plus the legacy blacklist lines. */
|
||||
/** Category 1: rendered (non-checkerOnly) slop entries a code reader can grep for, plus the legacy blacklist lines. */
|
||||
export function checklistSlopEntries(): DesignSlopEntry[] {
|
||||
// Array.prototype.sort is stable, so catalog order survives within a tier.
|
||||
return RENDERED
|
||||
|
||||
@@ -124,6 +124,12 @@ describe('design-craft check', () => {
|
||||
expect(checkCraft('<section><h2>Pricing</h2><a href="/buy">Start free trial</a></section>')).toEqual([]);
|
||||
});
|
||||
|
||||
test('markup rules skip script bodies, so a hydration payload that repeats the markup counts once', () => {
|
||||
const page = '<div class="min-h-screen transition-all"><a href="#">x</a><img src="a.png"></div><script>self.__next_f.push([1,"<div class=\\"min-h-screen transition-all\\"><a href=\\"#\\">x</a><img src=\\"a.png\\"></div>"])</script>';
|
||||
expect(checkCraft(page).map(f => `${f.id} ${f.count}`)).toEqual(['layout-transition 1', 'dead-link 1', 'img-no-alt 1', 'vh-height 1']);
|
||||
expect(checkCraft('<p>Hi</p><button onclick="window.addEventListener(\'scroll\', f)">Go</button>').map(f => f.id)).toEqual(['scroll-listener']);
|
||||
});
|
||||
|
||||
test('the CLI prints CRAFT_CHECK lines and exits 1 on tells, 0 when clean, 2 on usage', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-craft-'));
|
||||
try {
|
||||
@@ -141,6 +147,10 @@ describe('design-craft check', () => {
|
||||
expect(run(path.join(dir, 'empty.html')).code).toBe(2);
|
||||
fs.writeFileSync(path.join(dir, 'page.tsx'), 'export const P = () => <p>Acme</p>;');
|
||||
expect(run(path.join(dir, 'page.tsx'))).toMatchObject({ code: 2, out: '' });
|
||||
fs.writeFileSync(path.join(dir, 'index.html'), '<!doctype html><html><head><title>App</title></head><body>\n<div id="root"></div>\n<script type="module" src="/src/main.tsx"></script>\n</body></html>');
|
||||
const shell = run(path.join(dir, 'index.html'));
|
||||
expect(shell).toMatchObject({ code: 2, out: '' });
|
||||
expect(shell.err).toContain('mount-node shell');
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -158,6 +168,7 @@ describe('accepted craft exceptions (finalized.json craft_exceptions)', () => {
|
||||
expect(accepted).toEqual([{ id: 'placeholder-brand', count: 1, reason: 'Acme is the real customer' }, { id: 'scroll-listener', count: 1, reason: 'unused' }]);
|
||||
expect(issues.find(f => f.id === 'round-number')?.fix).toEndWith('(2 accepted earlier: supplied figures)');
|
||||
expect(issues).toHaveLength(findings.length - 2);
|
||||
expect(acceptFindings([{ id: 'needs-proof', count: 1, fix: 'f' }], [{ id: 'needs-proof', count: 1, reason: 'awaiting logo' }])).toEqual({ issues: [], accepted: [{ id: 'needs-proof', count: 1, reason: 'awaiting logo' }] });
|
||||
expect(formatCraft([], accepted)).toBe('CRAFT_ACCEPTED: placeholder-brand 1 Acme is the real customer\nCRAFT_ACCEPTED: scroll-listener 1 unused\nCRAFT_RESULT: clean (2 accepted)');
|
||||
const carried = [{ id: 'round-number', count: 1, reason: 'first run' }, { id: 'round-number', count: 3, reason: 'supplied figures' }];
|
||||
for (const list of [carried, [...carried].reverse()]) {
|
||||
@@ -222,6 +233,7 @@ describe('accepted craft exceptions (finalized.json craft_exceptions)', () => {
|
||||
fs.writeFileSync(page, CLEAN);
|
||||
fs.writeFileSync(record, JSON.stringify({ craft_exceptions: [{ id: 'placeholder-brand', count: 1, reason: 'Acme is the customer' }] }));
|
||||
expect(run(page, '--accepted', record)).toMatchObject({ code: 0, out: 'CRAFT_RESULT: clean\n' });
|
||||
expect(run(page)).toMatchObject({ code: 0, out: 'CRAFT_RESULT: clean\n', err: '' });
|
||||
expect(acceptFindings([], [{ id: 'oops', count: 1, reason: 'x' }])).toEqual({ issues: [], accepted: [] });
|
||||
fs.writeFileSync(page, CLEAN.replace('Halvorsen Freight', 'Acme and Nexus'));
|
||||
expect(run(page)).toMatchObject({ code: 1, out: 'CRAFT_CHECK: placeholder-brand 2 use a supplied customer, or a fitting name in sample data\nCRAFT_RESULT: 1 issue(s)\n' });
|
||||
|
||||
Reference in new issue
Block a user