mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-18 00:56:30 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e095c53819 | ||
|
|
3fcd656aaf | ||
|
|
6d973cdea6 |
@@ -9,3 +9,15 @@ The `skill/reference/ios.md` and `skill/reference/android.md` platform reference
|
||||
**Original work:** https://github.com/ehmo/platform-design-skills
|
||||
**Original license:** MIT
|
||||
**Author:** ehmo
|
||||
|
||||
## Static HTML parser bundle
|
||||
|
||||
`cli/engine/vendor/static-html-parsers.mjs` is a generated bundle of the parser packages the static-HTML detector needs at runtime. Skill and plugin installs copy that file with the detector; they do not install these packages from npm.
|
||||
|
||||
| Package | License |
|
||||
|---|---|
|
||||
| htmlparser2 | MIT |
|
||||
| css-select | BSD-2-Clause |
|
||||
| css-tree | MIT |
|
||||
| domutils | BSD-2-Clause |
|
||||
| source-map-js (via css-tree) | BSD-3-Clause |
|
||||
|
||||
@@ -423,10 +423,11 @@ async function detectCli() {
|
||||
}
|
||||
}
|
||||
else process.stderr.write(formatFindings(allFindings, false) + '\n');
|
||||
process.exit(primary.length > 0 ? 2 : 0);
|
||||
} else if (jsonMode) {
|
||||
process.stdout.write('[]\n');
|
||||
}
|
||||
if (jsonMode) process.stdout.write('[]\n');
|
||||
process.exit(0);
|
||||
if (globalThis.__impeccableStaticHtmlDegraded) process.exit(1);
|
||||
process.exit(primary.length > 0 ? 2 : 0);
|
||||
}
|
||||
|
||||
export { formatFindings, handleStdin, confirm, printUsage, detectCli };
|
||||
|
||||
@@ -122,12 +122,8 @@ async function detectHtml(filePath, options = {}) {
|
||||
ruleId: 'import-static-parser',
|
||||
target: filePath,
|
||||
}, async () => {
|
||||
const [htmlparser2, cssSelect, csstree, domutils] = await Promise.all([
|
||||
import('htmlparser2'),
|
||||
import('css-select'),
|
||||
import('css-tree'),
|
||||
import('domutils'),
|
||||
]);
|
||||
const parsers = await import(new URL('../../vendor/static-html-parsers.mjs', import.meta.url).href);
|
||||
const { htmlparser2, cssSelect, csstree, domutils } = parsers;
|
||||
return {
|
||||
parseDocument: htmlparser2.parseDocument,
|
||||
selectAll: cssSelect.selectAll,
|
||||
@@ -137,21 +133,20 @@ async function detectHtml(filePath, options = {}) {
|
||||
domutils,
|
||||
};
|
||||
});
|
||||
} catch (err) {
|
||||
if (!globalThis.__impeccableStaticHtmlWarned) {
|
||||
globalThis.__impeccableStaticHtmlWarned = true;
|
||||
|
||||
process.stderr.write(
|
||||
'impeccable detect: DEGRADED - HTML parser modules unavailable ' +
|
||||
'(htmlparser2, css-select, css-tree, domutils).\n' +
|
||||
'Falling back to regex matching. Custom properties, selector matching and computed ' +
|
||||
'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n'
|
||||
);
|
||||
} catch {
|
||||
if (!globalThis.__impeccableStaticHtmlWarned) {
|
||||
globalThis.__impeccableStaticHtmlWarned = true;
|
||||
process.stderr.write(
|
||||
'impeccable detect: DEGRADED - HTML parser modules unavailable ' +
|
||||
'(htmlparser2, css-select, css-tree, domutils).\n' +
|
||||
'Falling back to regex matching. Custom properties, selector matching and computed ' +
|
||||
'contrast are NOT evaluated; findings are an undercount, not a clean bill of health.\n',
|
||||
);
|
||||
}
|
||||
globalThis.__impeccableStaticHtmlDegraded = true;
|
||||
return detectText(html, filePath, options);
|
||||
}
|
||||
|
||||
return detectText(html, filePath, options);
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(filePath);
|
||||
const fileDir = path.dirname(resolvedPath);
|
||||
const root = profileStep(profile, {
|
||||
|
||||
+9592
File diff suppressed because one or more lines are too long
+3
-2
@@ -42,9 +42,10 @@
|
||||
"scripts": {
|
||||
"build:skills": "bun run scripts/build.js --skip-root-sync",
|
||||
"build:skills:release": "bun run scripts/build.js",
|
||||
"build": "bun run build:skills && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
|
||||
"build:release": "bun run build:skills:release && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
|
||||
"build": "node scripts/build-static-html-parsers.js --check && bun run build:skills && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
|
||||
"build:release": "node scripts/build-static-html-parsers.js --check && bun run build:skills:release && mkdir -p build/_data && rm -rf build/_data/dist && cp -R dist build/_data/dist",
|
||||
"build:browser": "node scripts/build-browser-detector.js",
|
||||
"build:static-html-parsers": "node scripts/build-static-html-parsers.js",
|
||||
"build:extension": "node scripts/build-extension.js",
|
||||
"clean": "rm -rf dist build",
|
||||
"rebuild": "bun run clean && bun run build",
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Generates cli/engine/vendor/static-html-parsers.mjs
|
||||
* by bundling htmlparser2, css-select, css-tree, and domutils for skill/plugin installs.
|
||||
*
|
||||
* Run: node scripts/build-static-html-parsers.js
|
||||
* Check: node scripts/build-static-html-parsers.js --check
|
||||
*/
|
||||
|
||||
import { createHash } from 'node:crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
const ENTRY = path.join(__dirname, 'lib/static-html-parsers.entry.mjs');
|
||||
const OUT_DIR = path.join(ROOT, 'cli/engine/vendor');
|
||||
const OUTPUT = path.join(OUT_DIR, 'static-html-parsers.mjs');
|
||||
const PARSER_PACKAGES = ['htmlparser2', 'css-select', 'css-tree', 'domutils'];
|
||||
const DIGEST_RE = /^\s*\* Source digest: ([0-9a-f]+)\s*$/m;
|
||||
|
||||
function sourceDigest() {
|
||||
const hash = createHash('sha256');
|
||||
hash.update(fs.readFileSync(ENTRY));
|
||||
hash.update('\n');
|
||||
for (const name of PARSER_PACKAGES) {
|
||||
const pkgPath = path.join(ROOT, 'node_modules', name, 'package.json');
|
||||
const version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
|
||||
hash.update(`${name}@${version}\n`);
|
||||
}
|
||||
return hash.digest('hex').slice(0, 16);
|
||||
}
|
||||
|
||||
function header(digest) {
|
||||
return `/**
|
||||
* GENERATED -- do not edit. Source: scripts/lib/static-html-parsers.entry.mjs
|
||||
* Rebuild: node scripts/build-static-html-parsers.js
|
||||
* Source digest: ${digest}
|
||||
*
|
||||
* Bundles htmlparser2, css-select, css-tree, and domutils for skill/plugin installs.
|
||||
* Third-party licenses: see NOTICE.md.
|
||||
*/
|
||||
`;
|
||||
}
|
||||
|
||||
function generate(outfile) {
|
||||
fs.mkdirSync(path.dirname(outfile), { recursive: true });
|
||||
const result = spawnSync(
|
||||
'bun',
|
||||
['build', ENTRY, '--outfile', outfile, '--target', 'node', '--format', 'esm'],
|
||||
{ cwd: ROOT, encoding: 'utf8' },
|
||||
);
|
||||
if (result.status !== 0) {
|
||||
process.stderr.write(result.stderr || result.stdout || 'bun build failed\n');
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
const output = header(sourceDigest()) + fs.readFileSync(outfile, 'utf8');
|
||||
fs.writeFileSync(outfile, output);
|
||||
return output;
|
||||
}
|
||||
|
||||
if (process.argv.includes('--check')) {
|
||||
const committed = fs.readFileSync(OUTPUT, 'utf8');
|
||||
const found = committed.match(DIGEST_RE)?.[1];
|
||||
const expected = sourceDigest();
|
||||
if (found !== expected) {
|
||||
process.stderr.write(
|
||||
'cli/engine/vendor/static-html-parsers.mjs is stale. Run: node scripts/build-static-html-parsers.js\n',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
generate(OUTPUT);
|
||||
console.log(`Generated ${path.relative(ROOT, OUTPUT)} (${(fs.statSync(OUTPUT).size / 1024).toFixed(1)} KB)`);
|
||||
@@ -0,0 +1,6 @@
|
||||
export * as htmlparser2 from 'htmlparser2';
|
||||
export * as cssSelect from 'css-select';
|
||||
export * as domutils from 'domutils';
|
||||
import parse from 'css-tree/parser';
|
||||
import generate from 'css-tree/generator';
|
||||
export const csstree = { parse, generate };
|
||||
@@ -25,7 +25,7 @@ export const SUITES = {
|
||||
description: 'Build, provider transforms, CLI helpers, context, and storage unit tests.',
|
||||
triggers: [
|
||||
...COMMON_INFRA_PATTERNS,
|
||||
/^scripts\/(?!benchmark-detector|build-browser-detector|build-extension)/,
|
||||
/^scripts\/(?!benchmark-detector|build-browser-detector|build-static-html-parsers|build-extension|lib\/static-html-parsers\.entry)/,
|
||||
/^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|comp-diff|comp-spec|build-phase|font-match|data\/font-index|concept-seed|generate-image|context|context-signals|critique-storage|design-parser|doctor|hook|impeccable-paths|is-generated|lib\/(artifact-schema|png|raster|image-metrics|font-fingerprint|font-index|hero-checks|composition-catalog|concept-catalog|provider|staleness|staleness-deep|staleness-notice|surface-briefs|target-slug|template-extensions)|pin|surface-brief))/,
|
||||
/^README(\.npm)?\.md$/,
|
||||
/^cli\/bin\//,
|
||||
@@ -92,7 +92,8 @@ export const SUITES = {
|
||||
...COMMON_INFRA_PATTERNS,
|
||||
/^cli\/engine\//,
|
||||
/^extension\/(background|content|detector|devtools|popup|manifest\.json)/,
|
||||
/^scripts\/(benchmark-detector|build-browser-detector|build-extension)\.js$/,
|
||||
/^scripts\/(benchmark-detector|build-browser-detector|build-static-html-parsers|build-extension)\.js$/,
|
||||
/^scripts\/lib\/static-html-parsers\.entry\.mjs$/,
|
||||
/^site\/(pages\/detector|public\/antipattern|data\/anti-patterns-catalog\.js)/,
|
||||
/^tests\/fixtures\/antipatterns/,
|
||||
],
|
||||
@@ -116,6 +117,7 @@ export const SUITES = {
|
||||
'tests/detect-cli-design-contamination.test.mjs',
|
||||
'tests/detect-cli-design-monorepo.test.mjs',
|
||||
'tests/detect-cli-stdin-dispatch.test.mjs',
|
||||
'tests/detect-static-html-skill-install.test.mjs',
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -132,9 +132,9 @@ Preserve semantics, accessibility, performance, responsiveness, project conventi
|
||||
|
||||
## 7. Inspect and finish
|
||||
|
||||
Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. On the web, capture with the harness's native or browser-canvas screenshot path. Never run `npx playwright install` or otherwise download a browser engine: the zip can sit at 100% while extract stalls, and the finish never starts. When no native browser tool is exposed, drive the user's installed Chrome, Chromium, or Edge with a short custom script, and fail fast if none is present. When the harness reports the user's actual viewport (an in-app browser's size, a named resolution), add that width to the set: the width that breaks is the one the user sees first. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. On a comp-led build, run `node {{scripts_path}}/comp-diff.mjs --comp <approved comp> --build .impeccable/review/desktop.png --spec .impeccable/build/spec.json --out-dir .impeccable/review/diff/final` and read its region rows and paired crops as the critique: the side-by-side is the view the build thread never has on its own, and a region it scores missing or contradicted is a fix whatever the page looks like from memory. Never judge fidelity from one full-page thumbnail; it hides exactly the failures that matter. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary.
|
||||
Inspect the surface's target sizes in one batched screenshot round: desktop and mobile on the web; on a native platform (`ios` / `android` / `adaptive`), the shipped device classes per OS, captured from the simulator or emulator the way the platform reference's Verifying the build section describes. When the harness reports the user's actual viewport (an in-app browser's size, a named resolution), add that width to the set: the width that breaks is the one the user sees first. Critique the render against the user's request and the direction contract, fix material gaps, and confirm with one final round; two rounds is the ceiling, and fixes batch between them rather than earning per-tweak screenshots. On a comp-led build, run `node {{scripts_path}}/comp-diff.mjs --comp <approved comp> --build .impeccable/review/desktop.png --spec .impeccable/build/spec.json --out-dir .impeccable/review/diff/final` and read its region rows and paired crops as the critique: the side-by-side is the view the build thread never has on its own, and a region it scores missing or contradicted is a fix whatever the page looks like from memory. Never judge fidelity from one full-page thumbnail; it hides exactly the failures that matter. On a Persuade surface, verify the mode did its job: a first-time visitor should know what this is, why it matters, and what to do within seconds, in the form's own vocabulary.
|
||||
|
||||
A capture is evidence only when it is valid, and you validate before you send. Settle or disable entrance motion first: an element hidden by animation timing reads as a missing element and gets fixed into a regression. Capture full-page shots from the document top. Capture the comp comparison at the comp's own pixel dimensions. Then open every file once and confirm it shows what its name claims: no black or blank regions, no wrong section behind a right filename, no half-loaded state, no leftover page from another process on the same localhost port. A malformed capture sent onward costs the whole round; the reviewer answers it with `disposition: recapture` and nothing it reviewed binds. <!-- rule:skill-capture-validity -->
|
||||
A capture is evidence only when it is valid, and you validate before you send. Settle or disable entrance motion first: an element hidden by animation timing reads as a missing element and gets fixed into a regression. Capture full-page shots from the document top. Capture the comp comparison at the comp's own pixel dimensions. Then open every file once and confirm it shows what its name claims: no black or blank regions, no wrong section behind a right filename, no half-loaded state. A malformed capture sent onward costs the whole round; the reviewer answers it with `disposition: recapture` and nothing it reviewed binds. <!-- rule:skill-capture-validity -->
|
||||
|
||||
After the second inspection round the build thread's polishing is over: no further defect hunts, micro-edit scripts, or rebuilds here; whatever remains ships through the handoffs, where a fresh context does the finding better and cheaper. On the web, where this harness runs no design hook, run `node {{scripts_path}}/detect.mjs --json` on the changed targets once here, fix what is mechanical, and pass the remaining findings to the reviewer; a hookless web build that skips this ships every tell the hook exists to catch. A native platform skips the detector entirely: it reads HTML and CSS and has no verdict on native code, so the reviewer's floor check is the only slop gate and the input packet says so. Capture the screenshots into `.impeccable/review/`, one file per captured viewport (on the web, `desktop.png` and `mobile.png`, plus `user-<width>.png` whenever the user's viewport joined the inspected set; on native, one per device class, such as `phone.png` and `tablet.png`, suffixed per OS on adaptive), creating that directory when the harness does not; the paths you pass the reviewer are its spec, every viewport you inspected is named required in the packet, and that directory is where it looks when a passed path is missing.
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { after, describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const engineDir = path.join(root, 'cli', 'engine');
|
||||
const skillDetect = path.join(root, 'skill', 'scripts', 'detect.mjs');
|
||||
const configDep = path.join(root, 'cli', 'lib', 'impeccable-config.mjs');
|
||||
|
||||
const CSS = ':root{--grad:linear-gradient(90deg,#7C3AED,#EC4899)}\n.hero h1{background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent}';
|
||||
const HTML = '<html><head><link rel=stylesheet href=s.css></head><body><div class=hero><h1>Hi</h1></div></body></html>';
|
||||
|
||||
function copyDir(src, dest) {
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const destPath = path.join(dest, entry.name);
|
||||
if (entry.isDirectory()) copyDir(srcPath, destPath);
|
||||
else fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
|
||||
function copyDetectorExternalDeps(scriptsDir) {
|
||||
fs.mkdirSync(path.join(scriptsDir, 'lib'), { recursive: true });
|
||||
fs.copyFileSync(configDep, path.join(scriptsDir, 'lib', 'impeccable-config.mjs'));
|
||||
}
|
||||
|
||||
function writeFixture(dir) {
|
||||
fs.writeFileSync(path.join(dir, 's.css'), CSS);
|
||||
fs.writeFileSync(path.join(dir, 'p.html'), HTML);
|
||||
}
|
||||
|
||||
function runDetect(cwd, detectRel, htmlRel = 'p.html') {
|
||||
const detectPath = path.join(cwd, detectRel);
|
||||
return spawnSync(
|
||||
process.execPath,
|
||||
[detectPath, '--json', '--no-config', '--no-design-system', htmlRel],
|
||||
{ cwd, encoding: 'utf8' },
|
||||
);
|
||||
}
|
||||
|
||||
function assertFullEngine(result) {
|
||||
assert.doesNotMatch(result.stderr, /DEGRADED/);
|
||||
const findings = JSON.parse(result.stdout);
|
||||
assert.ok(findings.some((item) => item.antipattern === 'gradient-text'));
|
||||
assert.equal(result.status, 2);
|
||||
}
|
||||
|
||||
function assertDegraded(result) {
|
||||
assert.match(result.stderr, /DEGRADED/);
|
||||
assert.equal(result.status, 1);
|
||||
JSON.parse(result.stdout);
|
||||
}
|
||||
|
||||
const tempDirs = [];
|
||||
|
||||
after(() => {
|
||||
for (const dir of tempDirs) fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('static HTML parsers in skill/plugin installs', () => {
|
||||
it('resolves parsers from a skill-shaped scripts/ tree with no node_modules', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-skill-detect-'));
|
||||
tempDirs.push(tmp);
|
||||
fs.mkdirSync(path.join(tmp, 'scripts'), { recursive: true });
|
||||
fs.copyFileSync(skillDetect, path.join(tmp, 'scripts', 'detect.mjs'));
|
||||
copyDir(engineDir, path.join(tmp, 'scripts', 'detector'));
|
||||
copyDetectorExternalDeps(path.join(tmp, 'scripts'));
|
||||
writeFixture(tmp);
|
||||
|
||||
assertFullEngine(runDetect(tmp, path.join('scripts', 'detect.mjs')));
|
||||
});
|
||||
|
||||
it('resolves parsers from a plugin cache skills/impeccable/scripts/ tree', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-plugin-detect-'));
|
||||
tempDirs.push(tmp);
|
||||
const scriptsDir = path.join(tmp, 'skills', 'impeccable', 'scripts');
|
||||
fs.mkdirSync(scriptsDir, { recursive: true });
|
||||
fs.copyFileSync(skillDetect, path.join(scriptsDir, 'detect.mjs'));
|
||||
copyDir(engineDir, path.join(scriptsDir, 'detector'));
|
||||
copyDetectorExternalDeps(scriptsDir);
|
||||
writeFixture(tmp);
|
||||
|
||||
assertFullEngine(runDetect(tmp, path.join('skills', 'impeccable', 'scripts', 'detect.mjs')));
|
||||
});
|
||||
|
||||
it('exits 1 when the vendor bundle is missing', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-skill-degraded-'));
|
||||
tempDirs.push(tmp);
|
||||
fs.mkdirSync(path.join(tmp, 'scripts'), { recursive: true });
|
||||
fs.copyFileSync(skillDetect, path.join(tmp, 'scripts', 'detect.mjs'));
|
||||
copyDir(engineDir, path.join(tmp, 'scripts', 'detector'));
|
||||
copyDetectorExternalDeps(path.join(tmp, 'scripts'));
|
||||
fs.unlinkSync(path.join(tmp, 'scripts', 'detector', 'vendor', 'static-html-parsers.mjs'));
|
||||
writeFixture(tmp);
|
||||
|
||||
assertDegraded(runDetect(tmp, path.join('scripts', 'detect.mjs')));
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { readSourceFiles } from '../../scripts/lib/utils.js';
|
||||
@@ -16,6 +17,35 @@ describe('skill detector bundle', () => {
|
||||
expect(scriptNames.has('detector/detect-antipatterns-browser.js')).toBe(true);
|
||||
expect(scriptNames.has('detector/cli/main.mjs')).toBe(true);
|
||||
expect(scriptNames.has('detector/engines/static-html/detect-html.mjs')).toBe(true);
|
||||
expect(scriptNames.has('detector/vendor/static-html-parsers.mjs')).toBe(true);
|
||||
});
|
||||
|
||||
test('static HTML parser vendor bundle matches the source digest', () => {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[path.join(ROOT, 'scripts/build-static-html-parsers.js'), '--check'],
|
||||
{ cwd: ROOT, encoding: 'utf8' },
|
||||
);
|
||||
if (result.status !== 0) {
|
||||
throw new Error(result.stderr || result.stdout || `--check exited ${result.status}`);
|
||||
}
|
||||
expect(result.status).toBe(0);
|
||||
});
|
||||
|
||||
test('static HTML parser --check fails when the vendor bundle is stale', () => {
|
||||
const vendor = path.join(ROOT, 'cli/engine/vendor/static-html-parsers.mjs');
|
||||
const original = fs.readFileSync(vendor, 'utf8');
|
||||
try {
|
||||
fs.writeFileSync(vendor, original.replace(/Source digest: [0-9a-f]+/, 'Source digest: deadbeefdeadbeef'));
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[path.join(ROOT, 'scripts/build-static-html-parsers.js'), '--check'],
|
||||
{ cwd: ROOT, encoding: 'utf8' },
|
||||
);
|
||||
expect(result.status).toBe(1);
|
||||
} finally {
|
||||
fs.writeFileSync(vendor, original);
|
||||
}
|
||||
});
|
||||
|
||||
test('critique references the bundled detector command', () => {
|
||||
|
||||
@@ -67,15 +67,4 @@ describe('skill reference authoring contracts', () => {
|
||||
assert.match(polish, /if a newer critique landed meanwhile, its backlog stays live/);
|
||||
assert.doesNotMatch(polish, /git status|git log/);
|
||||
});
|
||||
|
||||
it('finish capture forbids installing a Playwright browser and names the installed-browser fallback', () => {
|
||||
const newWork = readFileSync(join(ROOT, 'skill/reference/new-work.md'), 'utf-8').replace(/\r\n?/g, '\n');
|
||||
const inspectAndFinish = newWork.match(/## 7\. Inspect and finish\n([\s\S]*?)(?:\n## |$)/)?.[1] ?? '';
|
||||
|
||||
assert.match(inspectAndFinish, /native or browser-canvas screenshot path/);
|
||||
assert.match(inspectAndFinish, /Never run `npx playwright install`/);
|
||||
assert.match(inspectAndFinish, /installed Chrome, Chromium, or Edge/);
|
||||
assert.match(inspectAndFinish, /fail fast if none is present/);
|
||||
assert.match(inspectAndFinish, /leftover page from another process on the same localhost port/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user