diff --git a/package.json b/package.json
index 5a6d1a3a5..c503159aa 100644
--- a/package.json
+++ b/package.json
@@ -47,10 +47,10 @@
"rebuild:release": "bun run clean && bun run build:release",
"test": "node scripts/run-tests.mjs default",
"test:core": "node scripts/run-tests.mjs core",
+ "test:oracle": "node scripts/run-tests.mjs oracle",
"test:detector": "node scripts/run-tests.mjs detector",
"test:framework": "node scripts/run-tests.mjs framework",
"test:live": "node scripts/run-tests.mjs live",
- "test:cli-e2e": "node scripts/run-tests.mjs cli-e2e",
"test:cli-remote-e2e": "node scripts/run-tests.mjs cli-remote-e2e",
"test:plugin-e2e": "node scripts/run-tests.mjs plugin-e2e",
"test:live-e2e": "node scripts/run-tests.mjs live-e2e",
diff --git a/scripts/ci-test-plan.mjs b/scripts/ci-test-plan.mjs
index 39764af77..f489917c5 100644
--- a/scripts/ci-test-plan.mjs
+++ b/scripts/ci-test-plan.mjs
@@ -15,17 +15,31 @@ const changedFiles = localNoChanges || isSchedule ? [] : getChangedFiles();
const forceDeterministic = localNoChanges || isSchedule || eventName === 'push' || eventName === 'workflow_dispatch';
const forceOptIn = eventName === 'workflow_dispatch';
-const plan = {
- core: true,
- detector: forceDeterministic || matchesSuiteTriggers('detector', changedFiles),
- live: forceDeterministic || matchesSuiteTriggers('live', changedFiles),
- framework: forceDeterministic || matchesSuiteTriggers('framework', changedFiles),
- cli_remote_e2e: forceOptIn,
- live_e2e: isSchedule || forceOptIn || matchesSuiteTriggers('live-e2e', changedFiles),
- live_e2e_accept_cleanup: forceOptIn || matchesSuiteTriggers('live-e2e-accept-cleanup', changedFiles),
- skill_behavior: forceOptIn || matchesSuiteTriggers('skill-behavior', changedFiles),
- live_svelte_adapter_deepseek: forceOptIn || matchesSuiteTriggers('live-svelte-adapter-deepseek', changedFiles),
-};
+const plan = isSchedule
+ ? {
+ core: true,
+ oracle: true,
+ detector: true,
+ live: true,
+ framework: true,
+ cli_remote_e2e: false,
+ live_e2e: true,
+ live_e2e_accept_cleanup: false,
+ skill_behavior: false,
+ live_svelte_adapter_deepseek: false,
+ }
+ : {
+ core: true,
+ oracle: forceDeterministic || matchesSuiteTriggers('oracle', changedFiles),
+ detector: forceDeterministic || matchesSuiteTriggers('detector', changedFiles),
+ live: forceDeterministic || matchesSuiteTriggers('live', changedFiles),
+ framework: forceDeterministic || matchesSuiteTriggers('framework', changedFiles),
+ cli_remote_e2e: forceOptIn,
+ live_e2e: forceOptIn || matchesSuiteTriggers('live-e2e', changedFiles),
+ live_e2e_accept_cleanup: forceOptIn || matchesSuiteTriggers('live-e2e-accept-cleanup', changedFiles),
+ skill_behavior: forceOptIn || matchesSuiteTriggers('skill-behavior', changedFiles),
+ live_svelte_adapter_deepseek: forceOptIn || matchesSuiteTriggers('live-svelte-adapter-deepseek', changedFiles),
+ };
writeGithubOutputs(plan);
printSummary(plan, changedFiles);
diff --git a/scripts/lib/cli-args.mjs b/scripts/lib/cli-args.mjs
deleted file mode 100644
index 8e0a58efd..000000000
--- a/scripts/lib/cli-args.mjs
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- * One argv parser for the Live benchmark / judging scripts.
- *
- * These scripts had four subtly different hand-rolled parsers, and the gaps
- * failed silently rather than loudly: a parser without the `argv[i + 1]`
- * lookahead turned `--iterations 20` into `iterations: true` and benchmarked
- * the default 5 runs; a parser without kebab→camel mapping turned
- * `--median-target=0.4` into a key nothing read, so the comparison ran against
- * the default threshold. Both produce a clean-looking report of the wrong thing.
- *
- * Supported forms, per flag:
- * --flag → true
- * --flag=value → 'value'
- * --flag value → 'value' (unless `value` itself starts with `--`)
- *
- * Keys are camel-cased, so `--simulated-tail-ms` and `--simulatedTailMs` both
- * land on `simulatedTailMs`.
- */
-export function parseArgs(argv) {
- const out = {};
- for (let index = 0; index < argv.length; index += 1) {
- const arg = argv[index];
- if (!arg.startsWith('--')) continue;
- const body = arg.slice(2);
- if (!body) continue;
- const equals = body.indexOf('=');
- if (equals !== -1) {
- out[toCamel(body.slice(0, equals))] = body.slice(equals + 1);
- continue;
- }
- const next = argv[index + 1];
- if (next !== undefined && !next.startsWith('--')) {
- out[toCamel(body)] = next;
- index += 1;
- } else {
- out[toCamel(body)] = true;
- }
- }
- return out;
-}
-
-export function toCamel(value) {
- return String(value).replace(/-([a-z0-9])/gi, (_, char) => char.toUpperCase());
-}
-
-/**
- * Read a boolean flag. `--headed` and `--headed=true` must mean the same thing;
- * comparing the raw value against `true` silently ignores the second form.
- */
-export function boolFlag(value, fallback = false) {
- if (value === undefined) return fallback;
- if (typeof value === 'boolean') return value;
- const normalized = String(value).trim().toLowerCase();
- if (['', 'true', '1', 'yes', 'on'].includes(normalized)) return true;
- if (['false', '0', 'no', 'off'].includes(normalized)) return false;
- return fallback;
-}
-
-/**
- * Parse a positive integer flag, falling back when absent. Throws on a value
- * that was clearly meant as a number but isn't one, so `--iterations abc`
- * fails instead of quietly benchmarking the default.
- */
-export function positiveIntFlag(value, fallback) {
- if (value === undefined || value === true) return fallback;
- const parsed = Number.parseInt(String(value), 10);
- if (!Number.isFinite(parsed) || parsed <= 0 || String(parsed) !== String(value).trim()) {
- throw new Error(`expected a positive integer, got: ${value}`);
- }
- return parsed;
-}
-
-/**
- * Resolve a flag that must be one of a fixed set.
- *
- * A silent `x === 'known' ? 'known' : fallback` is the trap this replaces: the
- * private evals Live runner passes `--agent=codex`, which fell through to the
- * canned fake agent and produced a clean-looking report of a deterministic stub
- * labelled as a real harness run. An unrecognized value is a mistake, not a
- * request for the default.
- */
-export function resolveEnum(value, allowed, fallback, flagName) {
- if (value === undefined || value === true) return fallback;
- const normalized = String(value).trim().toLowerCase();
- if (allowed.includes(normalized)) return normalized;
- throw new Error(`${flagName} must be one of ${allowed.join(', ')}; got: ${value}`);
-}
diff --git a/scripts/test-suites.mjs b/scripts/test-suites.mjs
index 9a241e769..b0fed8df3 100644
--- a/scripts/test-suites.mjs
+++ b/scripts/test-suites.mjs
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
-export const DEFAULT_SUITES = ['core', 'detector', 'live', 'framework', 'plugin-e2e'];
+export const DEFAULT_SUITES = ['core', 'oracle', 'detector', 'live', 'framework', 'plugin-e2e'];
export const OPT_IN_SUITES = [
'cli-remote-e2e',
'live-e2e',
@@ -22,11 +22,12 @@ const COMMON_INFRA_PATTERNS = [
export const SUITES = {
core: {
- description: 'Build, provider transforms, CLI helpers, context, and storage unit tests.',
+ description: 'Build, provider transforms, hook manifests, plugin validators, and prose gates.',
triggers: [
...COMMON_INFRA_PATTERNS,
- /^scripts\/(?!benchmark-detector|build-browser-detector|build-extension)/,
- /^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))/,
+ /^scripts\/(?!build-browser-detector|build-extension)/,
+ /^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/)/,
+ /^ENGINE_VERSION$/,
/^README(\.npm)?\.md$/,
/^cli\/bin\//,
],
@@ -35,15 +36,11 @@ export const SUITES = {
runner: 'bun',
files: [
'tests/build.test.js',
- 'tests/cli-ignores.test.js',
- 'tests/windows-path-fix.test.js',
'tests/lib/provider-blocks.test.js',
'tests/lib/transformers/provider-blocks.test.js',
'tests/lib/utils.test.js',
- 'tests/lib/impeccable-config.test.js',
'tests/lib/transformers/factory.test.js',
'tests/lib/transformers/providers.test.js',
- 'tests/skills-cli.test.js',
'tests/validate-plugin-versions.test.js',
'tests/validate-plugin-manifest.test.js',
'tests/plugin-paths.test.js',
@@ -52,147 +49,89 @@ export const SUITES = {
{
runner: 'node',
files: [
- 'tests/ci-test-plan.test.mjs',
- 'tests/cli-args.test.mjs',
- 'tests/concept-seed.test.mjs',
- 'tests/generate-image-embed.test.mjs',
- 'tests/comp-diff.test.mjs',
'tests/build-phase.test.mjs',
+ 'tests/ci-test-plan.test.mjs',
+ 'tests/comp-diff.test.mjs',
'tests/font-match.test.mjs',
- 'tests/hero-checks.test.mjs',
- 'tests/serve-question.test.mjs',
- 'tests/context.test.mjs',
- 'tests/context-signals.test.mjs',
- 'tests/critique-storage.test.mjs',
- 'tests/design-parser.test.mjs',
'tests/github-sheriff.test.mjs',
+ 'tests/hero-checks.test.mjs',
'tests/hook-build.test.mjs',
- 'tests/hook.test.mjs',
- 'tests/impeccable-paths.test.mjs',
'tests/openai-plugin.test.mjs',
- 'tests/pin.test.mjs',
'tests/release.test.mjs',
- 'tests/doctor.test.mjs',
- 'tests/staleness.test.mjs',
'tests/skill-reference.test.mjs',
'tests/readme-gitignore.test.mjs',
- 'tests/target-args.test.mjs',
- 'tests/surface-brief.test.mjs',
- 'tests/template-extensions.test.mjs',
'tests/test-suites.test.mjs',
- 'tests/zip.test.mjs',
],
},
],
},
+ // The verbs live in the engine binary; this repo pins its behavior with the
+ // oracle goldens (tests/oracle) and drives its live-mode verbs over the
+ // framework fixtures. Both skip when no binary is present (bun run
+ // fetch:engine, or IMPECCABLE_BIN).
+ oracle: {
+ description: 'Oracle corpus replay against the engine binary; skips without a binary.',
+ triggers: [
+ ...COMMON_INFRA_PATTERNS,
+ /^ENGINE_VERSION$/,
+ /^tests\/oracle\//,
+ /^tests\/fixtures\//,
+ /^tests\/lib\/engine-bin\.mjs$/,
+ /^skill\/(reference\/|scripts\/)/,
+ ],
+ commands: [
+ {
+ runner: 'node',
+ timeoutMs: 900000,
+ files: ['tests/oracle.test.mjs'],
+ },
+ ],
+ },
detector: {
- description: 'Anti-pattern detector tests across text, jsdom fixtures, and Puppeteer browser paths.',
- needsPuppeteer: true,
+ description: 'Extension packaging checks (the detector engine itself is tested in the engine repo and by the oracle).',
triggers: [
...COMMON_INFRA_PATTERNS,
- /^cli\/engine\//,
/^extension\/(background|content|detector|devtools|popup|manifest\.json)/,
- /^scripts\/(benchmark-detector|build-browser-detector|build-extension)\.js$/,
- /^site\/(pages\/detector|public\/antipattern|data\/anti-patterns-catalog\.js)/,
- /^tests\/fixtures\/antipatterns/,
+ /^scripts\/(build-browser-detector|build-extension)\.js$/,
],
commands: [
- {
- runner: 'bun',
- files: [
- 'tests/detect-antipatterns.test.js',
- 'tests/detect-url-launch.test.mjs',
- 'tests/inline-ignores.test.mjs',
- 'tests/lib/detector-bundle.test.js',
- ],
- },
{
runner: 'node',
- files: [
- 'tests/extension-build.test.mjs',
- 'tests/design-system.test.mjs',
- 'tests/detect-antipatterns-fixtures.test.mjs',
- 'tests/detect-antipatterns-browser.test.mjs',
- 'tests/detect-cli-design-contamination.test.mjs',
- 'tests/detect-cli-design-monorepo.test.mjs',
- 'tests/detect-cli-stdin-dispatch.test.mjs',
- ],
+ files: ['tests/extension-build.test.mjs'],
},
],
},
live: {
- description: 'Fast live-mode unit and local-server integration tests, excluding full browser fixture sweeps.',
+ description: 'Live-mode reference contract checks plus the live-e2e helper units (agent output, CLI options, LLM agent parsing, steer loop against the binary); the live verbs themselves are covered by the oracle and framework suites.',
triggers: [
...COMMON_INFRA_PATTERNS,
- // `palette` is deliberately absent: skill/scripts/palette.mjs has no
- // test anywhere, and listing it here made edits run a suite that never
- // touches it, which reads as coverage that does not exist.
- /^skill\/(reference\/live\.md|scripts\/(detect-csp|lib\/is-generated|lib\/template-extensions|live\/|live|live-|modern-screenshot|pin))/,
- /^tests\/live-/,
+ /^skill\/(reference\/live\.md|scripts\/live-browser)/,
+ /^tests\/live-e2e\//,
+ /^tests\/lib\/engine-bin\.mjs$/,
],
commands: [
{
runner: 'node',
files: [
- 'tests/live-accept.test.mjs',
- 'tests/live-accept-css.test.mjs',
- 'tests/live-accept-scrub.test.mjs',
- 'tests/live-browser-dom.test.mjs',
+ 'tests/live-reference.test.mjs',
'tests/live-browser-ignores.test.mjs',
- 'tests/live-browser-script-parts.test.mjs',
- 'tests/live-browser-regression.test.mjs',
- 'tests/live-browser-session.test.mjs',
- 'tests/live-browser-source.test.mjs',
- 'tests/live-commit-manual-edits.test.mjs',
- 'tests/live-completion.test.mjs',
- 'tests/live-copy-edit-agent.test.mjs',
- 'tests/live-discard-manual-edits.test.mjs',
'tests/live-e2e-agent-output.test.mjs',
'tests/live-e2e-cli-options.test.mjs',
'tests/live-e2e-llm-agent.test.mjs',
'tests/live-e2e-steer-agent.test.mjs',
'tests/live-e2e/agent-insert.test.mjs',
- 'tests/live-event-validation.test.mjs',
- 'tests/live-frameworks.test.mjs',
- 'tests/live-generation-preflight.test.mjs',
- 'tests/live-inject.test.mjs',
- 'tests/live-insert.test.mjs',
- 'tests/live-insert-ui.test.mjs',
- 'tests/live-manual-edits-buffer.test.mjs',
- 'tests/live-poll.test.mjs',
- 'tests/live-project-ignores.test.mjs',
- 'tests/live-poll-lanes.test.mjs',
- 'tests/live-poll-stream.test.mjs',
- 'tests/live-recovery-commands.test.mjs',
- 'tests/live-reference.test.mjs',
- 'tests/live-roots.test.mjs',
- 'tests/live-server.test.mjs',
- 'tests/live-session-store.test.mjs',
- 'tests/live-source-lock.test.mjs',
- 'tests/live-source-search.test.mjs',
- 'tests/live-svelte-ast.test.mjs',
- 'tests/live-svelte-component-accept.test.mjs',
- 'tests/live-svelte-props-script.test.mjs',
- 'tests/live-tanstack-adapter.test.mjs',
- 'tests/live-target-context.test.mjs',
- 'tests/live-ui-surfaces.test.mjs',
- 'tests/live-wrap.test.mjs',
- 'tests/live-wrap-buffer-aware.test.mjs',
],
},
],
},
framework: {
- description: 'Framework fixture coverage for live injection, CSP, generated-file detection, and wrapping.',
+ description: 'Framework fixture coverage for live injection, CSP detection, and wrapping through the engine binary; skips without a binary.',
triggers: [
...COMMON_INFRA_PATTERNS,
+ /^ENGINE_VERSION$/,
/^tests\/framework-fixtures/,
/^tests\/framework-fixtures\.test\.mjs$/,
- /^skill\/scripts\/(detect-csp|live-inject|live-wrap)\.mjs$/,
- /^skill\/scripts\/lib\/is-generated\.mjs$/,
- /^skill\/scripts\/lib\/template-extensions\.mjs$/,
- /^skill\/scripts\/live\/(source-search|sveltekit-adapter|tanstack-adapter)\.mjs$/,
- /^skill\/scripts\/live\/frameworks\//,
+ /^tests\/lib\/engine-bin\.mjs$/,
],
commands: [
{
@@ -201,30 +140,14 @@ export const SUITES = {
},
],
},
- 'cli-e2e': {
- description: 'Deterministic CLI install/update tests against a local universal bundle.',
- commands: [
- {
- runner: 'bun',
- files: ['tests/skills-cli.test.js'],
- },
- ],
- },
+ // `impeccable install/update/check` and their remote smoke moved into the
+ // engine binary and its repo; the deterministic coverage here is the oracle
+ // corpus. The lane name stays so ci.yml and package.json keep resolving.
'cli-remote-e2e': {
- description: 'Remote CLI install/update smoke tests against impeccable.style.',
+ description: 'Remote CLI install/update smoke (moved to the engine repo; no tests here).',
optIn: true,
- triggers: [
- ...COMMON_INFRA_PATTERNS,
- /^cli\/bin\/commands\/skills\.mjs$/,
- /^tests\/skills-cli\.test\.js$/,
- ],
- commands: [
- {
- runner: 'bun',
- env: { IMPECCABLE_CLI_REMOTE_E2E: '1' },
- files: ['tests/skills-cli.test.js'],
- },
- ],
+ triggers: [...COMMON_INFRA_PATTERNS],
+ commands: [],
},
'plugin-e2e': {
description: 'Install the committed ./plugin subtree into a real (sandboxed) Claude Code and assert skills, agents, and hooks all load. Skips when the claude CLI is not on PATH.',
@@ -234,7 +157,6 @@ export const SUITES = {
/^skill\/agents\//,
/^scripts\/build\.js$/,
/^scripts\/lib\/validate-plugin-manifest\.js$/,
- /^scripts\/lib\/plugin-paths\.js$/,
/^tests\/plugin-e2e\.test\.mjs$/,
],
commands: [
@@ -252,7 +174,8 @@ export const SUITES = {
needsPlaywright: true,
triggers: [
...COMMON_INFRA_PATTERNS,
- /^skill\/scripts\/live/,
+ /^skill\/scripts\/live-browser/,
+ /^ENGINE_VERSION$/,
/^tests\/framework-fixtures/,
/^tests\/live-e2e(\.test\.mjs|\/)/,
],
@@ -271,7 +194,7 @@ export const SUITES = {
needsPlaywright: true,
triggers: [
...COMMON_INFRA_PATTERNS,
- /^skill\/scripts\/(serve-question|generate-image|concept-seed)\.mjs$/,
+ /^ENGINE_VERSION$/,
/^tests\/new-work-e2e(\.test\.mjs|\/)/,
],
commands: [
@@ -289,8 +212,7 @@ export const SUITES = {
needsPlaywright: true,
triggers: [
...COMMON_INFRA_PATTERNS,
- /^skill\/scripts\/(live-accept|live-browser|live-server|live-wrap)\.mjs$/,
- /^skill\/scripts\/live\/sveltekit-adapter\.mjs$/,
+ /^ENGINE_VERSION$/,
/^tests\/live-e2e-accept-cleanup-regression\.test\.mjs$/,
/^tests\/live-e2e\//,
],
@@ -318,7 +240,7 @@ export const SUITES = {
...COMMON_INFRA_PATTERNS,
/^skill\/SKILL\.src\.md$/,
/^skill\/reference\/(init|document|brand|product|shape|craft|audit|polish|live)\.md$/,
- /^skill\/scripts\/(context|context-signals|detect|detect-csp)\.mjs$/,
+ /^ENGINE_VERSION$/,
/^tests\/skill-behavior\//,
],
commands: [
@@ -346,8 +268,7 @@ export const SUITES = {
needsPlaywright: true,
triggers: [
...COMMON_INFRA_PATTERNS,
- /^skill\/scripts\/(live-server|live-wrap)\.mjs$/,
- /^skill\/scripts\/live\/(sveltekit-adapter|svelte-component)\.mjs$/,
+ /^ENGINE_VERSION$/,
/^tests\/framework-fixtures\/vite8-sveltekit-stateful\//,
/^tests\/live-svelte-adapter-deepseek\.test\.mjs$/,
],
diff --git a/tests/ci-test-plan.test.mjs b/tests/ci-test-plan.test.mjs
index 2a9c16124..d87c53aa1 100644
--- a/tests/ci-test-plan.test.mjs
+++ b/tests/ci-test-plan.test.mjs
@@ -23,10 +23,10 @@ describe('ci-test-plan', () => {
assert.equal(outputs.live_svelte_adapter_deepseek, 'false');
});
- it('routes detector changes to detector tests only', () => {
+ it('routes extension changes to detector tests only', () => {
const outputs = runPlan({
GITHUB_EVENT_NAME: 'pull_request',
- CI_CHANGED_FILES: 'cli/engine/detect-antipatterns.mjs',
+ CI_CHANGED_FILES: 'extension/manifest.json',
});
assert.equal(outputs.detector, 'true');
@@ -34,13 +34,13 @@ describe('ci-test-plan', () => {
assert.equal(outputs.framework, 'false');
});
- it('routes live server changes to live unit and full live E2E lanes', () => {
+ it('routes an engine version bump to every binary-driven lane', () => {
const outputs = runPlan({
GITHUB_EVENT_NAME: 'pull_request',
- CI_CHANGED_FILES: 'skill/scripts/live-server.mjs',
+ CI_CHANGED_FILES: 'ENGINE_VERSION',
});
- assert.equal(outputs.live, 'true');
+ assert.equal(outputs.framework, 'true');
assert.equal(outputs.live_e2e, 'true');
assert.equal(outputs.live_e2e_accept_cleanup, 'true');
assert.equal(outputs.live_svelte_adapter_deepseek, 'true');
diff --git a/tests/cli-args.test.mjs b/tests/cli-args.test.mjs
deleted file mode 100644
index afbf2d296..000000000
--- a/tests/cli-args.test.mjs
+++ /dev/null
@@ -1,175 +0,0 @@
-/**
- * Tests for scripts/lib/cli-args.mjs — the shared argv parser for the Live
- * benchmark / judging scripts.
- * Run with: node --test tests/cli-args.test.mjs
- */
-
-import { describe, it } from 'node:test';
-import assert from 'node:assert/strict';
-import { spawnSync } from 'node:child_process';
-import { mkdtempSync, rmSync } from 'node:fs';
-import { tmpdir } from 'node:os';
-import { join } from 'node:path';
-import { fileURLToPath } from 'node:url';
-
-import { boolFlag, parseArgs, positiveIntFlag, resolveEnum, toCamel } from '../scripts/lib/cli-args.mjs';
-
-const PROVIDER_SMOKE_SCRIPT = fileURLToPath(new URL('../scripts/smoke-provider-hooks.mjs', import.meta.url));
-
-describe('parseArgs', () => {
- it('reads space-separated values', () => {
- // The regression: without the argv[i+1] lookahead this yielded
- // {fixture: true, iterations: true}, silently benchmarking the defaults.
- assert.deepEqual(
- parseArgs(['--fixture', 'vite8-react-modal', '--iterations', '20']),
- { fixture: 'vite8-react-modal', iterations: '20' },
- );
- });
-
- it('reads --flag=value values', () => {
- assert.deepEqual(parseArgs(['--fixture=vite8-react-plain']), { fixture: 'vite8-react-plain' });
- });
-
- it('treats a flag followed by another flag as boolean', () => {
- assert.deepEqual(parseArgs(['--headed', '--quiet']), { headed: true, quiet: true });
- });
-
- it('treats a trailing flag as boolean', () => {
- assert.deepEqual(parseArgs(['--append']), { append: true });
- });
-
- it('camel-cases kebab keys so both spellings land on one key', () => {
- assert.deepEqual(parseArgs(['--simulated-tail-ms=250']), { simulatedTailMs: '250' });
- assert.deepEqual(parseArgs(['--simulatedTailMs=250']), { simulatedTailMs: '250' });
- assert.deepEqual(parseArgs(['--median-target', '0.4']), { medianTarget: '0.4' });
- });
-
- it('keeps a value that contains an equals sign intact', () => {
- assert.deepEqual(parseArgs(['--model=claude-sonnet-4-6=x']), { model: 'claude-sonnet-4-6=x' });
- });
-
- it('ignores positional args and a bare --', () => {
- assert.deepEqual(parseArgs(['positional', '--', '--real', 'v']), { real: 'v' });
- });
-
- it('lets a later occurrence win', () => {
- assert.deepEqual(parseArgs(['--agent', 'fake', '--agent', 'llm']), { agent: 'llm' });
- });
-});
-
-describe('toCamel', () => {
- it('upcases after hyphens only', () => {
- assert.equal(toCamel('simulated-tail-ms'), 'simulatedTailMs');
- assert.equal(toCamel('p95-target'), 'p95Target');
- assert.equal(toCamel('already'), 'already');
- });
-});
-
-describe('boolFlag', () => {
- it('accepts the bare-flag sentinel and the explicit spellings alike', () => {
- // --headed and --headed=true must not diverge.
- assert.equal(boolFlag(true), true);
- assert.equal(boolFlag('true'), true);
- assert.equal(boolFlag('1'), true);
- assert.equal(boolFlag('yes'), true);
- assert.equal(boolFlag(''), true);
- });
-
- it('recognizes negative spellings', () => {
- assert.equal(boolFlag('false'), false);
- assert.equal(boolFlag('0'), false);
- assert.equal(boolFlag('no'), false);
- });
-
- it('falls back when absent or unrecognized', () => {
- assert.equal(boolFlag(undefined), false);
- assert.equal(boolFlag(undefined, true), true);
- assert.equal(boolFlag('maybe', true), true);
- });
-});
-
-describe('positiveIntFlag', () => {
- it('parses positive integers', () => {
- assert.equal(positiveIntFlag('20', 5), 20);
- });
-
- it('falls back when absent or given as a bare flag', () => {
- assert.equal(positiveIntFlag(undefined, 5), 5);
- assert.equal(positiveIntFlag(true, 5), 5);
- });
-
- it('throws rather than silently using the default', () => {
- // Quietly benchmarking 5 iterations when 20 were asked for is the failure
- // this replaces.
- for (const bad of ['abc', '0', '-3', '2.5', '20x']) {
- assert.throws(() => positiveIntFlag(bad, 5), /positive integer/, `accepted ${bad}`);
- }
- });
-});
-
-describe('resolveEnum', () => {
- it('accepts an allowed value, case-insensitively', () => {
- assert.equal(resolveEnum('llm', ['fake', 'llm'], 'fake', '--agent'), 'llm');
- assert.equal(resolveEnum('LLM', ['fake', 'llm'], 'fake', '--agent'), 'llm');
- });
-
- it('falls back when absent or given as a bare flag', () => {
- assert.equal(resolveEnum(undefined, ['fake', 'llm'], 'fake', '--agent'), 'fake');
- assert.equal(resolveEnum(true, ['fake', 'llm'], 'fake', '--agent'), 'fake');
- });
-
- it('throws on an unrecognized value instead of silently using the default', () => {
- // The private evals Live runner passes --agent=codex. Falling back to the
- // canned fake agent produced a clean report of a deterministic stub labelled
- // as a real harness run.
- assert.throws(
- () => resolveEnum('codex', ['fake', 'llm'], 'fake', '--agent'),
- /--agent must be one of fake, llm; got: codex/,
- );
- assert.throws(
- () => resolveEnum('progresive', ['atomic', 'progressive'], 'atomic', '--delivery'),
- /--delivery must be one of atomic, progressive/,
- );
- });
-});
-
-describe('provider hook smoke CLI', () => {
- it('prints help without requiring a target repository', () => {
- const result = spawnSync(process.execPath, [PROVIDER_SMOKE_SCRIPT, '--help'], { encoding: 'utf8' });
-
- assert.equal(result.status, 0);
- assert.match(result.stdout, /^Usage: bun run smoke:hooks/);
- assert.match(result.stdout, /target repo must be explicit/);
- assert.equal(result.stderr, '');
- });
-
- it('fails with the same usage guidance when the target repository is omitted', () => {
- const result = spawnSync(process.execPath, [PROVIDER_SMOKE_SCRIPT], { encoding: 'utf8' });
-
- assert.equal(result.status, 1);
- assert.equal(result.stdout, '');
- assert.match(result.stderr, /^Usage: bun run smoke:hooks/);
- assert.match(result.stderr, /target repo must be explicit/);
- });
-
- it('preserves the legacy string sentinel for value-less options', () => {
- const cases = [
- { args: ['--repo'], error: /target repo does not exist: .*\/true/ },
- { args: ['--repo', '.', '--bundle'], error: /universal bundle does not exist: .*\/true/ },
- { args: ['--repo', '.', '--bundle', './missing.zip', '--providers'], error: /universal bundle does not exist: .*\/missing\.zip/ },
- ];
-
- for (const { args, error } of cases) {
- const cwd = mkdtempSync(join(tmpdir(), 'impeccable-provider-smoke-cli-'));
- try {
- const result = spawnSync(process.execPath, [PROVIDER_SMOKE_SCRIPT, ...args], { cwd, encoding: 'utf8' });
-
- assert.equal(result.status, 1);
- assert.doesNotMatch(result.stderr, /TypeError/);
- assert.match(result.stderr, error);
- } finally {
- rmSync(cwd, { recursive: true, force: true });
- }
- }
- });
-});
diff --git a/tests/cli-ignores.test.js b/tests/cli-ignores.test.js
deleted file mode 100644
index 8049106fb..000000000
--- a/tests/cli-ignores.test.js
+++ /dev/null
@@ -1,147 +0,0 @@
-import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
-import { mkdtempSync, rmSync, readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
-import { tmpdir } from 'node:os';
-import { join, resolve } from 'node:path';
-import { spawnSync } from 'node:child_process';
-
-const CLI = resolve('cli/bin/cli.js');
-
-describe('impeccable ignores CLI', () => {
- let root;
-
- beforeEach(() => {
- root = mkdtempSync(join(tmpdir(), 'imp-ignores-'));
- });
-
- afterEach(() => {
- rmSync(root, { recursive: true, force: true });
- });
-
- function run(args, options = {}) {
- const result = spawnSync(process.execPath, [CLI, 'ignores', ...args], {
- cwd: root,
- encoding: 'utf-8',
- ...options,
- });
- if (result.error) throw result.error;
- return result;
- }
-
- function detect(args, options = {}) {
- const result = spawnSync(process.execPath, [CLI, 'detect', '--json', ...args], {
- cwd: root,
- encoding: 'utf-8',
- ...options,
- });
- if (result.error) throw result.error;
- return result;
- }
-
- function readConfig(name = 'config.json') {
- return JSON.parse(readFileSync(join(root, '.impeccable', name), 'utf-8'));
- }
-
- test('adds and lists shared file and value ignores under detector', () => {
- expect(run(['add-file', 'src/legacy/**']).status).toBe(0);
- expect(run(['add-value', 'overused-font', 'Inter', '--reason', 'Brand font']).status).toBe(0);
-
- const raw = readConfig();
- expect(raw.hook).toBeUndefined();
- expect(raw.detector.ignoreFiles).toEqual(['src/legacy/**']);
- expect(raw.detector.ignoreValues.map(({ rule, value, reason }) => ({ rule, value, reason }))).toEqual([
- { rule: 'overused-font', value: 'inter', reason: 'Brand font' },
- ]);
- expect(raw.detector.designSystem).toBeUndefined();
-
- const listed = run(['list']);
- expect(listed.status).toBe(0);
- expect(listed.stdout).toContain('ignoreFiles: src/legacy/**');
- expect(listed.stdout).toContain('overused-font=inter');
- });
-
- test('supports scoped wildcard value ignores and removal', () => {
- expect(run(['add-value', 'design-system-color', '*', '--file', 'site/styles/demo.css']).status).toBe(0);
- let raw = readConfig();
- expect(raw.detector.ignoreValues).toEqual([
- expect.objectContaining({
- rule: 'design-system-color',
- value: '*',
- files: ['site/styles/demo.css'],
- }),
- ]);
-
- expect(run(['remove-value', 'design-system-color', '*', '--file', 'site/styles/demo.css']).status).toBe(0);
- raw = readConfig();
- expect(raw.detector.ignoreValues).toEqual([]);
- });
-
- test('file-scoped wildcard value ignores suppress non-value-bearing rules only in matching files', () => {
- mkdirSync(join(root, 'components'), { recursive: true });
- const triangle = [
- 'export function TopicCard() {',
- ' return (',
- '
',
- ' );',
- '}',
- '',
- ].join('\n');
- writeFileSync(join(root, 'components', 'TopicCard.jsx'), triangle);
- writeFileSync(join(root, 'components', 'Other.jsx'), triangle.replace('TopicCard', 'Other'));
-
- const before = detect(['components/TopicCard.jsx']);
- expect(before.status).toBe(2);
- expect(before.stdout).toContain('side-tab');
-
- expect(run(['add-value', 'side-tab', '*', '--file', '**/TopicCard.jsx']).status).toBe(0);
-
- const afterTarget = detect(['components/TopicCard.jsx']);
- expect(afterTarget.status).toBe(0);
- expect(afterTarget.stdout.trim()).toBe('[]');
-
- const afterOther = detect(['components/Other.jsx']);
- expect(afterOther.status).toBe(2);
- expect(afterOther.stdout).toContain('side-tab');
- });
-
- test('rejects broad wildcard value ignores', () => {
- const result = run(['add-value', 'design-system-color', '*']);
- expect(result.status).not.toBe(0);
- expect(result.stderr).toContain('Wildcard value ignores must be scoped');
- expect(existsSync(join(root, '.impeccable', 'config.json'))).toBe(false);
- });
-
- test('rejects exact values for rules that cannot extract one', () => {
- const result = run(['add-value', 'side-tab', 'Inter']);
- expect(result.status).not.toBe(0);
- expect(result.stderr).toMatch(/side-tab has no extractable ignore value.*add-value side-tab "\*" --file /);
- expect(existsSync(join(root, '.impeccable', 'config.json'))).toBe(false);
- });
-
- test('removes an existing broad wildcard value ignore', () => {
- mkdirSync(join(root, '.impeccable'), { recursive: true });
- writeFileSync(join(root, '.impeccable', 'config.json'), JSON.stringify({
- detector: {
- ignoreValues: [{ rule: 'design-system-color', value: '*' }],
- },
- }));
-
- const result = run(['remove-value', 'design-system-color', '*']);
- expect(result.status).toBe(0);
- expect(readConfig().detector.ignoreValues).toEqual([]);
- });
-
- test('writes local ignores without overriding shared design-system config', () => {
- expect(run(['add-value', 'overused-font', 'Inter', '--local']).status).toBe(0);
-
- const local = readConfig('config.local.json');
- expect(local.detector.ignoreValues.map(({ rule, value }) => ({ rule, value }))).toEqual([
- { rule: 'overused-font', value: 'inter' },
- ]);
- expect(local.detector.designSystem).toBeUndefined();
- });
-});
diff --git a/tests/concept-seed.test.mjs b/tests/concept-seed.test.mjs
deleted file mode 100644
index 1e04ab613..000000000
--- a/tests/concept-seed.test.mjs
+++ /dev/null
@@ -1,811 +0,0 @@
-import { describe, it } from 'node:test';
-import assert from 'node:assert/strict';
-import { spawn, spawnSync } from 'node:child_process';
-import { mkdtempSync, writeFileSync } from 'node:fs';
-import { createServer } from 'node:http';
-import { tmpdir } from 'node:os';
-import path from 'node:path';
-import { fileURLToPath, pathToFileURL } from 'node:url';
-import {
- readConceptCatalog,
- validateConceptCatalog,
- validateConceptEntry,
-} from '../skill/scripts/lib/concept-catalog.mjs';
-import { readCompositionCatalog } from '../skill/scripts/lib/composition-catalog.mjs';
-import { dealCompositions, pingChosen, renderChallenger, selectApprovedChallengers, selectApprovedComposition, selectApprovedCompositions } from '../skill/scripts/concept-seed.mjs';
-
-const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
-const SCRIPT = path.join(ROOT, 'skill', 'scripts', 'concept-seed.mjs');
-// The live catalog is service-side (impeccable-site); the public repo tests
-// the seed mechanics against this fixture catalog, which passes the same
-// validators the real one does.
-const FIXTURE_DIR = path.join(ROOT, 'tests', 'fixtures', 'concept-catalog');
-
-const fixtureState = readConceptCatalog(
- path.join(FIXTURE_DIR, 'concept-ingredients.json'),
- path.join(FIXTURE_DIR, 'concept-reviews.json')
-);
-const fixtureConcepts = fixtureState.concepts;
-const fixtureCompositions = readCompositionCatalog(
- path.join(FIXTURE_DIR, 'composition-ingredients.json'),
- path.join(FIXTURE_DIR, 'composition-reviews.json')
-).compositions;
-
-function run(scope, extraArgs = [], env = {}) {
- return spawnSync(process.execPath, [SCRIPT, '--scope', scope, '--from', 'stable-test', ...extraArgs], {
- cwd: ROOT,
- encoding: 'utf-8',
- env: { ...process.env, IMPECCABLE_CATALOG_DIR: FIXTURE_DIR, ...env },
- });
-}
-
-describe('concept seed scopes', () => {
- it('keeps complete-direction and established-world surface rolls reproducible but independent', () => {
- const directionA = run('direction');
- const directionB = run('direction');
- const surface = run('surface');
- assert.equal(directionA.status, 0);
- assert.equal(directionA.stdout, directionB.stdout);
- assert.notEqual(directionA.stdout, surface.stdout);
- assert.match(directionA.stdout, /DIRECTION CONCEPT SEED/);
- assert.match(directionA.stdout, /source: local/);
- assert.match(directionA.stdout, /selected\s+independently/);
- assert.match(directionA.stdout, /substantially different future surface/);
- assert.match(directionA.stdout, /Never expose assignment metadata/);
- assert.match(directionA.stdout, /SYSTEM GRAMMAR:/);
- assert.match(directionA.stdout, /CREATIVE SPARK:/);
- assert.match(directionA.stdout, /WEB LEVERAGE:/);
- assert.match(directionA.stdout, /credible\s+interface language/);
- assert.match(directionA.stdout, /commit to it across navigation/);
- assert.doesNotMatch(directionA.stdout, /undefined/);
- assert.match(surface.stdout, /SURFACE CONCEPT SEED/);
- assert.match(surface.stdout, /committed visual identity/);
- });
-
- it('rejects unknown scopes', () => {
- const result = run('unknown');
- assert.notEqual(result.status, 0);
- assert.match(result.stderr, /direction or surface/);
- });
-
- it('never promotes a rank outside the grounded candidate ledger', () => {
- for (const count of [5, 6, 7]) {
- for (let index = 0; index < 30; index += 1) {
- const result = spawnSync(process.execPath, [SCRIPT, '--scope', 'direction', '--from', `count-${count}-${index}`, '--candidate-count', String(count)], {
- cwd: ROOT,
- encoding: 'utf-8',
- env: { ...process.env, IMPECCABLE_CATALOG_DIR: FIXTURE_DIR },
- });
- assert.equal(result.status, 0);
- const promoted = Number(result.stdout.match(/ASSIGNED INDEX: (\d+)/)?.[1]);
- assert.equal(promoted >= 3 && promoted <= count, true, `rank ${promoted} must fit ${count} candidates`);
- }
- }
- const invalid = run('direction', ['--candidate-count', '4']);
- assert.notEqual(invalid.status, 0);
- assert.match(invalid.stderr, /integer from 5 to 7/);
- });
-
- it('degrades to a promotion-only seed when catalog and API are both unreachable', () => {
- const degraded = run('direction', ['--mode', 'persuade'], {
- IMPECCABLE_CATALOG_DIR: '/nonexistent-catalog-dir',
- IMPECCABLE_API_URL: 'http://127.0.0.1:9/api',
- IMPECCABLE_API_TIMEOUT: '400',
- });
- assert.equal(degraded.status, 0);
- assert.match(degraded.stdout, /source: degraded/);
- assert.match(degraded.stdout, /ASSIGNED INDEX: [3-7]/);
- assert.match(degraded.stdout, /No challengers this run/);
- assert.doesNotMatch(degraded.stdout, /CHALLENGERS:/);
- });
-
- it('keeps composition challengers inside the requested surface mode', () => {
- const pool = [
- { id: 'persuade-stage', surface: 'persuade', status: 'approved' },
- { id: 'experience-stage', surface: 'experience', status: 'approved' },
- ];
- const experience = selectApprovedComposition({
- scope: 'direction',
- key: 'mode-match',
- mode: 'experience',
- sourceCompositions: pool,
- });
- const read = selectApprovedComposition({
- scope: 'direction',
- key: 'mode-missing',
- mode: 'read',
- sourceCompositions: pool,
- });
- assert.equal(experience?.id, 'experience-stage');
- assert.equal(read, null, 'a missing mode must not borrow an unrelated composition');
-
- const rendered = run('direction', ['--mode', 'experience']);
- assert.equal(rendered.status, 0);
- assert.match(rendered.stdout, /mode: experience/);
- assert.match(rendered.stdout, /--scope direction --mode experience --from stable-test/);
- // Compositions are pulled from the deal by default until the expanded
- // catalog ships; the draw machinery stays live behind the env gate.
- assert.doesNotMatch(rendered.stdout, /COMPOSITION/);
- const gated = run('direction', ['--mode', 'experience'], { IMPECCABLE_COMPOSITIONS: '1' });
- assert.equal(gated.status, 0);
- assert.match(gated.stdout, /FIRST-SURFACE COMPOSITION/);
- });
-
- it('draws several composition inputs from distinct families when the approved pool allows it', () => {
- const pool = [
- { id: 'a', familyId: 'first', surface: 'persuade', status: 'approved' },
- { id: 'b', familyId: 'scroll', surface: 'persuade', status: 'approved' },
- { id: 'c', familyId: 'physics', surface: 'persuade', status: 'approved' },
- { id: 'd', familyId: 'first', surface: 'persuade', status: 'approved' },
- { id: 'e', familyId: 'other', surface: 'operate', status: 'approved' },
- ];
- const picks = selectApprovedCompositions({ scope: 'direction', key: 'several', mode: 'persuade', sourceCompositions: pool });
- assert.equal(picks.length, 3);
- assert.equal(new Set(picks.map(pick => pick.familyId)).size, 3);
- assert.equal(picks.every(pick => pick.surface === 'persuade'), true);
- });
-
- it('validates the fixture catalog with the real gates', () => {
- const result = validateConceptCatalog(fixtureState.catalog, fixtureState.reviewData);
- assert.deepEqual(result.errors, []);
- assert.equal(result.stats.approved >= 24, true);
- for (const tier of ['graphic', 'interaction', 'atmosphere']) {
- const approved = fixtureConcepts.filter(concept => concept.status === 'approved' && concept.wellTier === tier);
- assert.equal(approved.length >= 6, true, `${tier} needs re-roll depth`);
- }
- });
-
- it('rejects concepts that name a motif without system grammar and web leverage', () => {
- const errors = validateConceptEntry({
- id: 'fixture-newspaper',
- form: 'a newspaper front page, with headline hierarchy and columns',
- lineage: 'editorial publishing',
- tags: ['hierarchy', 'columns', 'serial'],
- strength: 'dual',
- });
- assert.equal(errors.some(error => error.includes('system grammar')), true);
- assert.equal(errors.some(error => error.includes('web leverage')), true);
- });
-
- it('rejects literal operations archetypes even when their UI grammar is complete', () => {
- const errors = validateConceptEntry({
- id: 'fixture-mission-control',
- form: 'a mission control room, where panels, alerts, and operator stations coordinate a launch',
- lineage: 'immersive environmental experience',
- tags: ['depth', 'threshold', 'rhythm'],
- strength: 'dual',
- spark: 'Cold reflected light moves across the room as the countdown opens one route, closes another, and leaves a bright trace of every recent decision on the main board.',
- system: [
- 'Palette/material: wet basalt grays, cold green light, and one warm seam marking the active route',
- 'Type/composition: narrow engraved capitals for room names over a quiet cartographic body voice',
- 'Topology/navigation: move by chamber, threshold, and revealed route',
- 'Controls/state: inspect open, pending, closed, and remembered states',
- 'Responsive/motion: turn depth into a stepwise route with orientation kept',
- ],
- webLeverage: 'WebGL depth rendering with a complete keyboard-readable route index',
- });
- assert.equal(errors.some(error => error.includes('operations archetype')), true);
- });
-
- it('welcomes materially specific craft tools instead of confusing them with operational software', () => {
- const errors = validateConceptEntry({
- id: 'cabinetmaker-workbench',
- form: "a cabinetmaker's workbench, where holdfasts, planing stops, shavings, and old cuts turn careful joinery into visible memory",
- lineage: 'Cabinetmaking benches, workholding craft, hand-planing practice, and tool-wear conservation',
- tags: ['workholding', 'joinery', 'patina'],
- strength: 'world',
- spark: 'Morning light crosses a blackened bench top as pale curls gather behind a plane, a holdfast rings into place, and fresh dovetails rise from a century of cuts.',
- system: [
- 'Palette/material: blackened beech, pale shaving curls, and brass holdfast glints over a workshop-gray ground',
- 'Type/composition: stamped maker marks and penciled layout lines ranked against a strong horizontal bench datum',
- 'Topology/navigation: follow stock from reference face through marked joints and fitted assemblies',
- 'Controls/state: clamp, mark, plane, pare, dry-fit, revise, and preserve grain direction',
- 'Responsive/motion: unfold the bench into a project sequence and follow each hand gesture',
- ],
- webLeverage: 'Grain-aware direct manipulation, reversible project history, and a keyboard-readable construction diagram',
- });
- assert.deepEqual(errors, []);
- });
-
- it('selects six approved challengers, two from every translation tier', () => {
- for (let index = 0; index < 100; index += 1) {
- const { picks } = selectApprovedChallengers({ scope: 'surface', key: `coverage-${index}`, sourceConcepts: fixtureConcepts });
- assert.equal(picks.length, 6);
- for (const tier of ['graphic', 'interaction', 'atmosphere']) {
- assert.equal(picks.filter(pick => pick.wellTier === tier).length, 2);
- }
- assert.equal(new Set(picks.map(pick => pick.id)).size, 6);
- assert.equal(picks.every(pick => pick.status === 'approved'), true);
- }
- });
-
- it('re-rolls draw disjoint challengers and stay reproducible from the base key', () => {
- const rounds = [0, 1, 2].map(reroll =>
- selectApprovedChallengers({ scope: 'direction', key: 'reroll-chain', reroll, sourceConcepts: fixtureConcepts })
- );
- const again = selectApprovedChallengers({ scope: 'direction', key: 'reroll-chain', reroll: 2, sourceConcepts: fixtureConcepts });
- assert.deepEqual(again.picks.map(pick => pick.id), rounds[2].picks.map(pick => pick.id));
- const seen = new Set();
- for (const round of rounds) {
- assert.equal(round.picks.length, 6);
- assert.equal(round.picks.every(pick => !seen.has(pick.id)), true);
- for (const pick of round.picks) seen.add(pick.id);
- for (const tier of ['graphic', 'interaction', 'atmosphere']) {
- assert.equal(round.picks.filter(pick => pick.wellTier === tier).length, 2);
- }
- }
- });
-
- it('renders re-roll rounds with elimination framing and a chained reproduction key', () => {
- const round0 = run('direction');
- const round1A = run('direction', ['--reroll', '1']);
- const round1B = run('direction', ['--reroll', '1']);
- assert.equal(round1A.status, 0);
- assert.equal(round1A.stdout, round1B.stdout);
- assert.notEqual(round1A.stdout, round0.stdout);
- assert.match(round1A.stdout, /RE-ROLL ROUND 1/);
- assert.match(round1A.stdout, /may not return reworded/);
- assert.match(round1A.stdout, /--from stable-test --reroll 1/);
- assert.doesNotMatch(round0.stdout, /RE-ROLL ROUND/);
- const invalid = run('direction', ['--reroll', 'nope']);
- assert.notEqual(invalid.status, 0);
- assert.match(invalid.stderr, /non-negative integer/);
- });
-
- it('registers steer the round presentation without changing the deal', () => {
- const plain = run('direction', ['--reroll', '1']);
- const bolder = run('direction', ['--reroll', '1', '--register', 'bolder']);
- const safer = run('direction', ['--reroll', '1', '--register', 'safer']);
- assert.equal(bolder.status, 0);
- assert.equal(safer.status, 0);
- // A register is presentation-only: the same key and reroll count deal the
- // same challengers, so the exclusion chain never forks on register.
- const dealtIds = (out) => [...out.matchAll(/SOURCE ID: ([a-z0-9-]+)/g)].map((m) => m[1]).sort();
- assert.deepEqual(dealtIds(bolder.stdout), dealtIds(plain.stdout), 'bolder presents the same deal the plain round drew');
- assert.match(bolder.stdout, /BOLDER REGISTER/);
- assert.match(bolder.stdout, /FIRST dealt challenger leads/);
- assert.match(bolder.stdout, /--register bolder/);
- assert.doesNotMatch(bolder.stdout, /ASSIGNED INDEX:/);
- // The generic weighing instruction measures against the assigned
- // direction, which a bolder round suspended; bolder weighs against the
- // leader instead, and the contradiction must not ship.
- assert.match(bolder.stdout, /against the fused LEADER/);
- assert.doesNotMatch(bolder.stdout, /against the assigned direction/);
- assert.match(safer.stdout, /SAFER REGISTER/);
- assert.match(safer.stdout, /sanctioned lineup/);
- assert.doesNotMatch(safer.stdout, /^CHALLENGERS:/m, 'the safer round spends its hand unseen');
- // Degraded safer must not contradict itself: "the user picks" and a
- // mandatory numbered build order cannot share one output.
- const degradedSafer = run('direction', ['--reroll', '1', '--register', 'safer'], {
- IMPECCABLE_CATALOG_DIR: '/nonexistent-catalog-dir',
- IMPECCABLE_API_URL: 'http://127.0.0.1:1',
- });
- assert.equal(degradedSafer.status, 0);
- assert.match(degradedSafer.stdout, /source: degraded/);
- assert.match(degradedSafer.stdout, /SAFER REGISTER/);
- assert.doesNotMatch(degradedSafer.stdout, /ASSIGNED INDEX/, 'degraded safer suppresses the assignment machinery');
- assert.doesNotMatch(degradedSafer.stdout, /Build candidate/, 'degraded safer mandates no numbered candidate');
- const degradedBolder = run('direction', ['--reroll', '1', '--register', 'bolder'], {
- IMPECCABLE_CATALOG_DIR: '/nonexistent-catalog-dir',
- IMPECCABLE_API_URL: 'http://127.0.0.1:1',
- });
- assert.equal(degradedBolder.status, 0);
- assert.match(degradedBolder.stdout, /BOLDER REGISTER UNAVAILABLE/);
- assert.match(degradedBolder.stdout, /ASSIGNED INDEX: /, 'degraded bolder falls back to the plain grounded assignment');
- const invalidRegister = run('direction', ['--reroll', '1', '--register', 'wilder']);
- assert.notEqual(invalidRegister.status, 0);
- assert.match(invalidRegister.stderr, /must be safer or bolder/);
- const noReroll = run('direction', ['--register', 'bolder']);
- assert.notEqual(noReroll.status, 0);
- assert.match(noReroll.stderr, /re-roll round/);
- });
-
- it('filters challengers by strength per scope and falls back when a tier has no match', () => {
- const make = (id, tier, strength) => ({
- id,
- familyId: `${id}-family`,
- wellId: `${id}-well`,
- wellTier: tier,
- strength,
- status: 'approved',
- form: `${id} form`,
- spark: `${id} spark`,
- system: [],
- webLeverage: `${id} web`,
- });
- const pool = [
- make('poster', 'graphic', 'world'),
- make('flipbook', 'graphic', 'composition'),
- make('radar', 'interaction', 'dual'),
- make('cavern', 'atmosphere', 'world'),
- ];
- for (let index = 0; index < 40; index += 1) {
- const direction = selectApprovedChallengers({ scope: 'direction', key: `d-${index}`, sourceConcepts: pool });
- assert.equal(direction.picks.every(pick => pick.strength !== 'composition'), true);
- const surface = selectApprovedChallengers({ scope: 'surface', key: `s-${index}`, sourceConcepts: pool });
- const surfaceGraphic = surface.picks.find(pick => pick.wellTier === 'graphic');
- assert.equal(surfaceGraphic.id, 'flipbook');
- // atmosphere has no composition|dual entries, so surface falls back to its full pool
- const surfaceAtmosphere = surface.picks.find(pick => pick.wellTier === 'atmosphere');
- assert.equal(surfaceAtmosphere.id, 'cavern');
- }
- });
-
- it('weights challenger draws by approval rating without shrinking the pool', () => {
- const make = (id, rating) => ({
- id,
- familyId: `${id}-family`,
- wellId: `${id}-well`,
- wellTier: 'graphic',
- strength: 'world',
- status: 'approved',
- form: `${id} form`,
- spark: `${id} spark`,
- system: [],
- webLeverage: `${id} web`,
- review: rating ? { status: 'approved', rating } : { status: 'approved' },
- });
- const filler = (tier, id) => ({
- ...make(id, undefined),
- wellTier: tier,
- });
- const pool = [
- make('flagship', 3),
- make('solid-a', 2),
- make('solid-b', undefined),
- make('marginal', 1),
- filler('interaction', 'radar'),
- filler('atmosphere', 'cavern'),
- ];
- const counts = { flagship: 0, 'solid-a': 0, 'solid-b': 0, marginal: 0 };
- for (let index = 0; index < 300; index += 1) {
- const { picks } = selectApprovedChallengers({ scope: 'direction', key: `weight-${index}`, sourceConcepts: pool });
- const graphicFirst = picks.find(pick => pick.wellTier === 'graphic');
- counts[graphicFirst.id] += 1;
- }
- // A 1-star draws at half weight rather than not at all. Excluding it made a
- // rating do the job breadth already does, and a marginal keep records
- // "narrow or unexceptional" rather than "wrong".
- assert.equal(counts.marginal > 0, true, `marginal ${counts.marginal} should draw`);
- assert.equal(counts.marginal < counts['solid-b'], true,
- `marginal ${counts.marginal} should draw below solid-b ${counts['solid-b']}`);
-
- // A 3-star no longer outdraws a 2-star. The multiplier concentrated the
- // draw hard on a thin pool: measured on the live catalog, 3-star worlds took
- // 75% of the interaction draw from 15 of 25 eligible worlds.
- const spread = Math.abs(counts.flagship - counts['solid-b']) / Math.max(counts.flagship, counts['solid-b']);
- assert.equal(spread < 0.4, true,
- `flagship ${counts.flagship} and solid-b ${counts['solid-b']} should draw comparably`);
-
- // A tier holding only 1-star approvals still yields challengers.
- const onlyMarginal = [
- make('lone-marginal', 1),
- filler('interaction', 'radar2'),
- filler('atmosphere', 'cavern2'),
- ];
- const { picks } = selectApprovedChallengers({ scope: 'direction', key: 'lone', sourceConcepts: onlyMarginal });
- assert.equal(picks.some(pick => pick.id === 'lone-marginal'), true);
- });
-
- it('holds niche worlds out of the challenger pool however strong their rating', () => {
- const make = (id, rating, breadth) => ({
- id,
- familyId: `${id}-family`,
- wellId: `${id}-well`,
- wellTier: 'graphic',
- strength: 'world',
- status: 'approved',
- form: `${id} form`,
- spark: `${id} spark`,
- system: [],
- webLeverage: `${id} web`,
- review: { status: 'approved', ...(rating ? { rating } : {}), ...(breadth ? { breadth } : {}) },
- });
- const filler = (tier, id) => ({ ...make(id), wellTier: tier });
- const pool = [
- make('broad-flagship', 3),
- make('narrow-flagship', 3, 'niche'),
- make('broad-solid', 2),
- filler('interaction', 'radar3'),
- filler('atmosphere', 'cavern3'),
- ];
- // Breadth excludes independently of rating: over many keys a niche 3-star
- // never challenges while its broad peers keep rotating.
- for (let index = 0; index < 200; index += 1) {
- const { picks } = selectApprovedChallengers({ scope: 'direction', key: `breadth-${index}`, sourceConcepts: pool });
- assert.equal(picks.some(pick => pick.id === 'narrow-flagship'), false, `niche world drawn at key breadth-${index}`);
- }
- // A tier holding only niche approvals falls back rather than starving,
- // exactly like the marginal-only tier above.
- const onlyNiche = [
- make('lone-niche', 3, 'niche'),
- filler('interaction', 'radar4'),
- filler('atmosphere', 'cavern4'),
- ];
- const { picks } = selectApprovedChallengers({ scope: 'direction', key: 'lone-niche', sourceConcepts: onlyNiche });
- assert.equal(picks.some(pick => pick.id === 'lone-niche'), true);
- });
-
- it('weights composition draws by rating without letting the ticket dedupe erase the weight', () => {
- const pool = [
- { id: 'flagship-stage', surface: 'persuade', status: 'approved', review: { status: 'approved', rating: 3 } },
- { id: 'plain-stage', surface: 'persuade', status: 'approved', review: { status: 'approved' } },
- { id: 'marginal-stage', surface: 'persuade', status: 'approved', review: { status: 'approved', rating: 1 } },
- ];
- const counts = { 'flagship-stage': 0, 'plain-stage': 0, 'marginal-stage': 0 };
- for (let index = 0; index < 300; index += 1) {
- const picks = selectApprovedCompositions({ scope: 'direction', key: `stage-weight-${index}`, mode: 'persuade', sourceCompositions: pool, count: 1 });
- counts[picks[0].id] += 1;
- }
- // Same weighting as challengers: a 1-star draws at half rather than not at
- // all, and a 3-star no longer outdraws a 2-star.
- assert.equal(counts['marginal-stage'] > 0, true, 'a 1-star composition still draws, at half weight');
- assert.equal(counts['marginal-stage'] < counts['plain-stage'], true,
- `marginal ${counts['marginal-stage']} should draw below plain ${counts['plain-stage']}`);
- const stageSpread = Math.abs(counts['flagship-stage'] - counts['plain-stage'])
- / Math.max(counts['flagship-stage'], counts['plain-stage']);
- assert.equal(stageSpread < 0.4, true,
- `flagship ${counts['flagship-stage']} and plain ${counts['plain-stage']} should draw comparably`);
-
- // A pool of nothing but 1-star keeps still yields compositions.
- const onlyMarginal = [
- { id: 'lone-marginal-stage', surface: 'persuade', status: 'approved', review: { status: 'approved', rating: 1 } },
- ];
- const fallback = selectApprovedCompositions({ scope: 'direction', key: 'stage-lone', mode: 'persuade', sourceCompositions: onlyMarginal });
- assert.equal(fallback.some(pick => pick.id === 'lone-marginal-stage'), true);
- });
-
- it('gates compositions by breadth and falls back when every composition is niche', () => {
- const pool = [
- { id: 'broad-stage', surface: 'persuade', status: 'approved' },
- { id: 'niche-stage', surface: 'persuade', status: 'approved', review: { breadth: 'niche' } },
- ];
- for (let index = 0; index < 60; index += 1) {
- const picks = selectApprovedCompositions({ scope: 'direction', key: `stage-breadth-${index}`, mode: 'persuade', sourceCompositions: pool });
- assert.equal(picks.some(pick => pick.id === 'niche-stage'), false, `niche composition dealt at key stage-breadth-${index}`);
- }
- const allNiche = [
- { id: 'only-niche-stage', surface: 'persuade', status: 'approved', review: { breadth: 'niche' } },
- ];
- const fallback = selectApprovedCompositions({ scope: 'direction', key: 'all-niche', mode: 'persuade', sourceCompositions: allNiche });
- assert.equal(fallback.some(pick => pick.id === 'only-niche-stage'), true, 'an all-niche pool must fall back instead of dealing nothing');
- });
-
- it('mode-filters the fixture composition pool per surface register', () => {
- const operate = selectApprovedComposition({ scope: 'surface', key: 'fix-mode', mode: 'operate', sourceCompositions: fixtureCompositions });
- assert.equal(operate.surface, 'operate');
- const experience = selectApprovedComposition({ scope: 'surface', key: 'fix-mode', mode: 'experience', sourceCompositions: fixtureCompositions });
- assert.equal(experience.surface, 'experience');
- });
-
- it('renders the vivid spark before system and browser leverage', () => {
- const output = renderChallenger({
- form: 'a spiral galaxy, where gravity, orbit, density, and darkness organize attention across radical scales',
- spark: 'A brilliant core holds the central promise while related ideas travel through spiral arms and distant fragments wait at the edge of perception.',
- system: [
- 'Palette/material: deep space black, star-white points, and one warm core glow reserved for the focus',
- 'Type/composition: hairline astronomical labels orbiting a monumental numeral voice',
- 'Topology/navigation: orbit a stable core and travel by arm or scale',
- 'Controls/state: focus, compare, capture, and release orbiting material',
- 'Responsive/motion: collapse depth into a radial sequence with orientation kept',
- ],
- webLeverage: 'WebGL semantic zoom with a complete keyboard-readable DOM index',
- }, 0);
- assert.match(output, /spiral galaxy/);
- assert.match(output, /CREATIVE SPARK: A brilliant core/);
- assert.equal(output.indexOf('CREATIVE SPARK:') < output.indexOf('SYSTEM GRAMMAR:'), true);
- });
-});
-
-describe('init gate', () => {
- const gateRun = (cwd) => spawnSync(process.execPath, [SCRIPT, '--scope', 'direction', '--from', 'gate-test'], {
- cwd,
- encoding: 'utf-8',
- env: { ...process.env, IMPECCABLE_CATALOG_DIR: FIXTURE_DIR, IMPECCABLE_CONTEXT_DIR: '' },
- });
-
- it('refuses to deal when no PRODUCT.md exists and routes to init', () => {
- const dir = mkdtempSync(path.join(tmpdir(), 'concept-seed-noproduct-'));
- const result = gateRun(dir);
- assert.equal(result.status, 1);
- assert.match(result.stdout, /NO_PRODUCT_MD/);
- assert.match(result.stdout, /init/);
- assert.doesNotMatch(result.stdout, /ASSIGNED INDEX/);
- });
-
- it('deals normally once PRODUCT.md exists', () => {
- const dir = mkdtempSync(path.join(tmpdir(), 'concept-seed-product-'));
- writeFileSync(path.join(dir, 'PRODUCT.md'), '# Test Product\n\n## Register\n\nbrand\n');
- const result = gateRun(dir);
- assert.equal(result.status, 0);
- assert.doesNotMatch(result.stdout, /NO_PRODUCT_MD/);
- });
-
- it('never gates the choice ping', () => {
- const dir = mkdtempSync(path.join(tmpdir(), 'concept-seed-ping-'));
- const result = spawnSync(process.execPath, [SCRIPT, '--chosen', 'assigned', '--from', 'gate-test'], {
- cwd: dir,
- encoding: 'utf-8',
- env: { ...process.env, IMPECCABLE_CATALOG_DIR: FIXTURE_DIR, IMPECCABLE_NO_TELEMETRY: '1' },
- });
- assert.equal(result.status, 0);
- assert.doesNotMatch(result.stdout, /NO_PRODUCT_MD/);
- // --kind alone is a valid ping invocation (assigned/pick/canon outcomes
- // have no catalog id) and is equally ungated.
- const kindOnly = spawnSync(process.execPath, [SCRIPT, '--kind', 'assigned', '--from', 'gate-test'], {
- cwd: dir,
- encoding: 'utf-8',
- env: { ...process.env, IMPECCABLE_CATALOG_DIR: FIXTURE_DIR, IMPECCABLE_NO_TELEMETRY: '1' },
- });
- assert.equal(kindOnly.status, 0);
- assert.doesNotMatch(kindOnly.stdout, /NO_PRODUCT_MD/);
- assert.match(kindOnly.stdout, /choice ping skipped/, 'telemetry-disabled kind ping reports skipped, not an error');
- });
-
- it('pingChosen validates kinds, requires ids only for challenger wins, and honors opt-out', async () => {
- const calls = [];
- const realFetch = globalThis.fetch;
- globalThis.fetch = async (url, opts) => { calls.push(JSON.parse(opts.body)); return { ok: true }; };
- // telemetryDisabled() honors DO_NOT_TRACK too, so a developer shell with
- // it set must not fail the success-path assertions below.
- const savedDnt = process.env.DO_NOT_TRACK;
- const savedNoTelemetry = process.env.IMPECCABLE_NO_TELEMETRY;
- try {
- delete process.env.DO_NOT_TRACK;
- process.env.IMPECCABLE_NO_TELEMETRY = '1';
- assert.equal(await pingChosen({ kind: 'assigned', key: 'k' }), false, 'opt-out wins over everything');
- delete process.env.IMPECCABLE_NO_TELEMETRY;
- assert.equal(await pingChosen({ kind: 'assigned', key: 'k', scope: 'direction' }), true, 'kind-only ping for a non-challenger outcome');
- assert.equal(await pingChosen({ kind: 'challenger', key: 'k' }), false, 'a challenger win without an id is not a ping');
- assert.equal(await pingChosen({ kind: 'weird', chosenId: 'x', key: 'k' }), false, 'unknown kinds are dropped');
- assert.equal(await pingChosen({ kind: 'assigned', register: 'wilder', key: 'k' }), false, 'unknown registers are dropped');
- assert.equal(await pingChosen({ chosenId: 'legacy-id', key: 'k' }), true, 'legacy id-only shape stays valid');
- assert.equal(await pingChosen({ kind: 'canon', register: 'safer', key: 'k' }), true, 'register rides along on a steered round');
- const bodies = calls;
- assert.equal(bodies[0].kind, 'assigned');
- assert.equal(bodies[0].chosenId, undefined, 'no id field on kind-only pings');
- assert.equal(bodies[1].chosenId, 'legacy-id');
- assert.equal(bodies[1].kind, undefined, 'legacy pings carry no kind');
- assert.equal(bodies[2].register, 'safer');
- } finally {
- globalThis.fetch = realFetch;
- if (savedDnt === undefined) delete process.env.DO_NOT_TRACK;
- else process.env.DO_NOT_TRACK = savedDnt;
- if (savedNoTelemetry === undefined) delete process.env.IMPECCABLE_NO_TELEMETRY;
- else process.env.IMPECCABLE_NO_TELEMETRY = savedNoTelemetry;
- }
- });
-
- // Mode eligibility on worlds. Before this, selectApprovedChallengers never
- // received the mode at all, so a build asking for an app UI could draw six
- // worlds that only make sense on a landing page.
- it('keeps worlds out of modes their reviewer excluded, and treats absent as all modes', () => {
- const make = (id, tier, allowedModes) => ({
- id,
- familyId: `${id}-family`,
- wellTier: tier,
- strength: 'world',
- status: 'approved',
- form: `${id} form`,
- spark: `${id} spark`,
- system: [],
- webLeverage: `${id} web`,
- review: { status: 'approved', ...(allowedModes ? { allowedModes } : {}) },
- });
- const pool = [
- make('persuade-only', 'graphic', ['persuade']),
- make('anywhere', 'graphic'),
- make('operate-capable', 'graphic', ['operate', 'read']),
- make('radar', 'interaction'),
- make('cavern', 'atmosphere'),
- ];
-
- for (let index = 0; index < 25; index += 1) {
- const operate = selectApprovedChallengers({
- scope: 'direction', key: `mode-gate-${index}`, mode: 'operate', sourceConcepts: pool,
- }).picks.map(pick => pick.id);
- assert.equal(operate.includes('persuade-only'), false, `persuade-only dealt for operate at ${index}`);
- }
-
- // Absent allowedModes stays eligible in every mode.
- const seen = new Set();
- for (let index = 0; index < 25; index += 1) {
- for (const mode of ['persuade', 'operate', 'read', 'experience']) {
- for (const pick of selectApprovedChallengers({
- scope: 'direction', key: `mode-any-${index}`, mode, sourceConcepts: pool,
- }).picks) seen.add(pick.id);
- }
- }
- assert.equal(seen.has('anywhere'), true, 'a world with no allowedModes must stay eligible');
- });
-
- it('falls back rather than starving a tier whose every world excludes the mode', () => {
- const make = (id, tier, allowedModes) => ({
- id,
- familyId: `${id}-family`,
- wellTier: tier,
- strength: 'world',
- status: 'approved',
- form: `${id} form`,
- spark: `${id} spark`,
- system: [],
- webLeverage: `${id} web`,
- review: { status: 'approved', ...(allowedModes ? { allowedModes } : {}) },
- });
- // The whole interaction tier is persuade-only. Selection must degrade to it
- // rather than throw or deal fewer than six.
- const pool = [
- make('graphic-any', 'graphic'),
- make('graphic-two', 'graphic'),
- make('radar-persuade', 'interaction', ['persuade']),
- make('cavern-any', 'atmosphere'),
- ];
- const picks = selectApprovedChallengers({
- scope: 'direction', key: 'starve', mode: 'operate', sourceConcepts: pool,
- }).picks;
- assert.equal(picks.some(pick => pick.id === 'radar-persuade'), true, 'an emptied tier must fall back to its full pool');
- });
-
- // Grain: how much of the product a composition composes. Framed by what the
- // skill can be asked for (a docs site, an onboarding flow, a landing page, a
- // data table) rather than by what the catalog happens to hold.
- it('prefers the requested grain and tops up from the register', () => {
- const make = (id, grain) => ({
- id, familyId: `${id}-family`, surface: 'operate', status: 'approved',
- ...(grain ? { grain } : {}), review: { status: 'approved' },
- });
- const pool = [
- make('flow-one', 'flow'),
- make('flow-two', 'flow'),
- make('view-one', 'view'),
- make('view-two', 'view'),
- make('untagged', null),
- ];
- const dealt = dealCompositions({ scope: 'surface', key: 'grain-pref', mode: 'operate', grain: 'flow', sourceCompositions: pool });
- assert.equal(dealt.picks.length, 3, 'a thin grain tops up rather than dealing fewer');
- const ids = dealt.picks.map(pick => pick.id);
- assert.equal(ids.includes('flow-one') && ids.includes('flow-two'), true, 'both grain matches deal first');
- assert.equal(dealt.match.atGrain, 2);
- assert.equal(dealt.match.grainAvailable, 2);
- });
-
- // The report is the point. Three plausible view-grain compositions dealt
- // against a flow request, with no signal that none matched, is the same silent
- // plausibility this axis exists to remove.
- it('reports a grain miss instead of passing off borrowed structure', () => {
- const make = id => ({ id, familyId: `${id}-family`, surface: 'operate', status: 'approved', grain: 'view', review: { status: 'approved' } });
- const pool = [make('a'), make('b'), make('c')];
- const dealt = dealCompositions({ scope: 'surface', key: 'grain-miss', mode: 'operate', grain: 'flow', sourceCompositions: pool });
- assert.equal(dealt.picks.length, 3, 'still deals three');
- assert.equal(dealt.match.atGrain, 0, 'and says none matched');
- assert.equal(dealt.match.grainAvailable, 0);
- });
-
- // Platform is a hard filter, not a preference: a composition that needs hover
- // does not degrade on a phone, it stops working.
- it('excludes compositions the platform cannot carry, with no fallback', () => {
- const make = (id, platforms) => ({
- id, familyId: `${id}-family`, surface: 'operate', status: 'approved',
- ...(platforms ? { platforms } : {}), review: { status: 'approved' },
- });
- const pool = [make('web-only', ['web']), make('anywhere', null), make('native', ['ios', 'android'])];
- const onIos = dealCompositions({ scope: 'surface', key: 'plat', mode: 'operate', platform: 'ios', sourceCompositions: pool });
- const ids = onIos.picks.map(pick => pick.id);
- assert.equal(ids.includes('web-only'), false, 'a web-only composition must not reach an iOS build');
- assert.equal(onIos.match.platformExcluded, 1);
-
- const allWebOnly = [make('x', ['web']), make('y', ['web'])];
- const starved = dealCompositions({ scope: 'surface', key: 'plat2', mode: 'operate', platform: 'android', sourceCompositions: allWebOnly });
- assert.deepEqual(starved.picks, [], 'an empty deal beats dealing something that cannot work');
- });
-
- it('reproduces a grain-scoped deal from the same key', () => {
- const make = (id, grain) => ({
- id, familyId: `${id}-family`, surface: 'read', status: 'approved',
- ...(grain ? { grain } : {}), review: { status: 'approved' },
- });
- const pool = [make('p1', 'product'), make('v1', 'view'), make('v2', 'view'), make('r1', 'region')];
- const args = { scope: 'surface', key: 'grain-stable', mode: 'read', grain: 'product', sourceCompositions: pool };
- assert.deepEqual(
- selectApprovedCompositions(args).map(p => p.id),
- selectApprovedCompositions(args).map(p => p.id)
- );
- });
-});
-
-// The Windows abort in issue #504 (nodejs/node#56645) needs three things at
-// once: a successful roll over Node's undici-backed fetch, the keep-alive
-// socket that success leaves pooled, and the explicit process.exit at the end
-// of the CLI. The suite's other API test exercises only the unreachable-API
-// fallback, which leaves no pooled socket and so never walked the crashing
-// path. This one serves a real roll from a local server and asserts the CLI
-// destroys fetch's global dispatcher before exiting, so the teardown cannot
-// silently regress. The teardown is Node fetch internals, so the CLI is
-// spawned with node even when the suite itself runs under bun.
-describe('API roll path', () => {
- const NODE = process.versions.bun ? 'node' : process.execPath;
-
- const ROLL_PAYLOAD = {
- poolRevision: 'api-test-rev',
- approvedCount: 6,
- catalogCount: 9,
- challengers: [{
- id: 'api-test-world',
- form: 'a letterpress print shop, where type, ink, and impression organize the page',
- spark: 'Deep impressions hold the central promise while loose sorts wait in the case.',
- system: ['Palette/material: dense ink black bitten into soft cotton paper'],
- webLeverage: 'Variable-font impression depth with a keyboard-readable page structure',
- }],
- compositions: [],
- };
-
- // Wraps the global dispatcher's destroy so the parent test can observe the
- // CLI's exit teardown. The warmup fetch makes fetch install the dispatcher
- // before the wrap, and parks a keep-alive socket in its pool, which is the
- // exact state the Windows crash needs at exit.
- const PRELOAD = [
- "const KEY = Symbol.for('undici.globalDispatcher.1');",
- 'await fetch(`${process.env.IMPECCABLE_API_URL}/warmup`).then(r => r.arrayBuffer()).catch(() => {});',
- 'const dispatcher = globalThis[KEY];',
- "if (dispatcher && typeof dispatcher.destroy === 'function') {",
- ' const destroy = dispatcher.destroy.bind(dispatcher);',
- ' dispatcher.destroy = (...args) => {',
- " process.stderr.write('DISPATCHER_DESTROY_CALLED\\n');",
- ' return destroy(...args);',
- ' };',
- '}',
- '',
- ].join('\n');
-
- it('resolves a successful roll and destroys the fetch dispatcher before the explicit exit', async () => {
- const requests = [];
- const server = createServer((req, res) => {
- requests.push(req.url);
- if (req.url.startsWith('/api/roll?')) {
- res.setHeader('Content-Type', 'application/json');
- res.end(JSON.stringify(ROLL_PAYLOAD));
- return;
- }
- res.statusCode = 404;
- res.end('not found');
- });
- await new Promise(resolveListen => server.listen(0, '127.0.0.1', resolveListen));
- try {
- const dir = mkdtempSync(path.join(tmpdir(), 'concept-seed-api-'));
- writeFileSync(path.join(dir, 'PRODUCT.md'), '# Test Product\n\n## Platform\n\nweb\n');
- const preloadPath = path.join(dir, 'wrap-dispatcher.mjs');
- writeFileSync(preloadPath, PRELOAD);
- const result = await new Promise((resolveRun, rejectRun) => {
- const child = spawn(NODE, [
- '--import', pathToFileURL(preloadPath).href,
- SCRIPT, '--scope', 'direction', '--mode', 'persuade', '--from', 'api-test',
- ], {
- cwd: dir,
- env: {
- ...process.env,
- IMPECCABLE_CATALOG_DIR: '/nonexistent-catalog-dir',
- IMPECCABLE_API_URL: `http://127.0.0.1:${server.address().port}/api`,
- },
- });
- let stdout = '';
- let stderr = '';
- child.stdout.on('data', chunk => { stdout += chunk; });
- child.stderr.on('data', chunk => { stderr += chunk; });
- child.on('error', rejectRun);
- child.on('close', status => resolveRun({ status, stdout, stderr }));
- });
- assert.equal(result.status, 0, `stderr: ${result.stderr}`);
- assert.equal(requests.some(url => url.startsWith('/api/roll?')), true, 'the CLI must hit the roll endpoint');
- assert.match(result.stdout, /source: api/);
- assert.match(result.stdout, /letterpress print shop/);
- // The choice-recording command rides on build-phase start now (the
- // separate TELEMETRY ping was the step every comp-round-skipping run
- // suppressed); an API roll names it with the --chosen slot.
- assert.match(result.stdout, /AFTER THE CHOICE, run exactly one command/);
- assert.match(result.stdout, /build-phase\.mjs start --direction [\w-]+ --kind \[--chosen \]/);
- assert.match(result.stderr, /DISPATCHER_DESTROY_CALLED/, 'the dispatcher must be destroyed before process.exit');
- } finally {
- server.close();
- }
- });
-});
diff --git a/tests/context-signals.test.mjs b/tests/context-signals.test.mjs
deleted file mode 100644
index 8766abe82..000000000
--- a/tests/context-signals.test.mjs
+++ /dev/null
@@ -1,682 +0,0 @@
-/**
- * Tests for context-signals.mjs — the signal gatherer behind the
- * context-aware bare `/impeccable` (no-argument) path.
- *
- * The script collects deterministic project signals and emits JSON; it does
- * not score or rank (the agent reasons over the raw signals). These tests
- * cover signal collection and the never-throw / always-valid-JSON contract.
- *
- * Each test runs in its own scratch dir under os.tmpdir().
- */
-import { describe, it, beforeEach, afterEach } from 'node:test';
-import assert from 'node:assert/strict';
-import fs from 'node:fs';
-import path from 'node:path';
-import os from 'node:os';
-import { fileURLToPath } from 'node:url';
-
-import { gatherSignals } from '../skill/scripts/context-signals.mjs';
-
-const SCRIPT_PATH = path.join(
- path.dirname(fileURLToPath(import.meta.url)),
- '..', 'skill', 'scripts', 'context-signals.mjs',
-);
-
-let scratch;
-beforeEach(() => {
- scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-signals-'));
-});
-afterEach(() => {
- fs.rmSync(scratch, { recursive: true, force: true });
-});
-
-function write(rel, body) {
- const abs = path.join(scratch, rel);
- fs.mkdirSync(path.dirname(abs), { recursive: true });
- fs.writeFileSync(abs, body);
-}
-
-describe('gatherSignals', () => {
- it('reports no setup context in an empty dir', async () => {
- const s = await gatherSignals(scratch);
- assert.equal(s.setup.hasProduct, false);
- assert.equal(s.setup.hasDesign, false);
- assert.equal(Object.hasOwn(s.setup, 'register'), false);
- assert.equal(s.setup.hasCode, false);
- assert.equal(s.critique.latest, null);
- });
-
- it('detects PRODUCT.md, platform, and code presence', async () => {
- write('PRODUCT.md', '# Product\n\n## Platform\n\nweb\n');
- write('package.json', '{"name":"x"}');
- const s = await gatherSignals(scratch);
- assert.equal(s.setup.hasProduct, true);
- assert.equal(s.setup.platform, 'web');
- assert.equal(s.setup.hasCode, true);
- });
-
- it('flags missing DESIGN.md when code exists', async () => {
- write('PRODUCT.md', '# Product\n\n## Platform\n\nweb\n');
- write('src/App.tsx', 'export default 1;');
- const s = await gatherSignals(scratch);
- assert.equal(s.setup.hasProduct, true);
- assert.equal(s.setup.hasDesign, false);
- assert.equal(s.setup.hasCode, true);
- assert.equal(s.setup.platform, 'web');
- });
-
- it('reads the newest critique snapshot score', async () => {
- write('.impeccable/critique/2026-05-01T10-00-00Z__home.md',
- '---\nslug: home\nscore: 6\np0: 1\np1: 3\ntimestamp: 2026-05-01T10-00-00Z\n---\nbody\n');
- write('.impeccable/critique/2026-05-02T10-00-00Z__home.md',
- '---\nslug: home\nscore: 8\np0: 0\np1: 1\ntimestamp: 2026-05-02T10-00-00Z\n---\nbody\n');
- const s = await gatherSignals(scratch);
- assert.equal(s.critique.latest.score, 8); // newest by timestamp prefix
- assert.equal(s.critique.latest.p0, 0);
- assert.equal(s.critique.latest.slug, 'home');
- });
-
- it('reads the documented critique snapshot metadata keys', async () => {
- write('.impeccable/critique/2026-05-02T10-00-00Z__pricing.md',
- '---\nslug: pricing\ntotal_score: 24\np0_count: 2\np1_count: 5\ntimestamp: 2026-05-02T10-00-00Z\n---\nbody\n');
- const s = await gatherSignals(scratch);
- assert.equal(s.critique.latest.score, 24);
- assert.equal(s.critique.latest.p0, 2);
- assert.equal(s.critique.latest.p1, 5);
- });
-
- it('reports missing critique metrics as null', async () => {
- write('.impeccable/critique/2026-05-02T10-00-00Z__pricing.md',
- '---\nslug: pricing\ntimestamp: 2026-05-02T10-00-00Z\n---\nbody\n');
- const s = await gatherSignals(scratch);
- assert.equal(s.critique.latest.score, null);
- assert.equal(s.critique.latest.p0, null);
- assert.equal(s.critique.latest.p1, null);
- });
-
- it('reports empty and invalid critique metrics as null', async () => {
- write('.impeccable/critique/2026-05-02T10-00-00Z__pricing.md',
- '---\nslug: pricing\ntotal_score: \np0_count: \np1_count: nope\ntimestamp: 2026-05-02T10-00-00Z\n---\nbody\n');
- const s = await gatherSignals(scratch);
- assert.equal(s.critique.latest.score, null);
- assert.equal(s.critique.latest.p0, null);
- assert.equal(s.critique.latest.p1, null);
- });
-
- it('reads the newest critique snapshot across target slugs', async () => {
- write('.impeccable/critique/2026-05-01T10-00-00Z__home.md',
- '---\nslug: home\nscore: 6\np0: 1\np1: 3\ntimestamp: 2026-05-01T10-00-00Z\n---\nbody\n');
- write('.impeccable/critique/2026-05-02T10-00-00Z__pricing.md',
- '---\nslug: pricing\nscore: 9\np0: 0\np1: 1\ntimestamp: 2026-05-02T10-00-00Z\n---\nbody\n');
- write('.impeccable/critique/ignore.md', '# Critique ignores\n');
- write('.impeccable/critique/9999-not-a-snapshot.md', '# Draft\n');
- const s = await gatherSignals(scratch);
- assert.equal(s.critique.latest.slug, 'pricing');
- assert.equal(s.critique.latest.score, 9);
- assert.equal(
- s.critique.latest.file,
- '.impeccable/critique/2026-05-02T10-00-00Z__pricing.md',
- );
- });
-
- it('handles a non-git dir without throwing', async () => {
- const s = await gatherSignals(scratch);
- assert.equal(s.git.isRepo, false);
- assert.deepEqual(s.git.changedFiles, []);
- assert.equal(s.git.changedCount, 0);
- });
-
- it('reports working-tree changes with full, untruncated paths', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('site/styles/home.css', 'a{}\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- // Modify it so it shows as ` M ...` (leading-space porcelain line) — the
- // exact shape that a naive global trim would truncate to "ite/...".
- write('site/styles/home.css', 'a{color:red}\n');
- const s = await gatherSignals(scratch);
- assert.equal(s.git.isRepo, true);
- assert.ok(
- s.git.changedFiles.includes('site/styles/home.css'),
- `expected full path, got: ${JSON.stringify(s.git.changedFiles)}`,
- );
- });
-
- it('always includes a well-formed devServer probe', async () => {
- const s = await gatherSignals(scratch);
- assert.equal(typeof s.devServer.running, 'boolean');
- assert.ok(Array.isArray(s.devServer.ports));
- });
-
- it('targets a local source dir (never a URL), even with a dev server up', async () => {
- write('src/App.tsx', 'export default 1;');
- const s = await gatherSignals(scratch);
- assert.equal(s.scan.via, 'source-dir');
- assert.deepEqual(s.scan.targets, ['src']);
- // No target is ever an http(s) URL.
- assert.ok(s.scan.targets.every((t) => !/^https?:/.test(t)));
- });
-
- it('prefers the dirty tree: scans changed markup/style files', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/Hero.tsx', 'export const Hero = () => null;\n');
- write('README.md', 'x\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- write('src/Hero.tsx', 'export const Hero = () => 2;\n'); // dirty
- write('README.md', 'y\n'); // dirty but not scannable
- const s = await gatherSignals(scratch);
- assert.equal(s.scan.via, 'git-changes');
- assert.deepEqual(s.scan.targets, ['src/Hero.tsx']); // README.md filtered out
- });
-
- it('filters harness-dir files out of git-changes scan targets (#303)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/Hero.tsx', 'export const Hero = () => null;\n');
- write('.claude/skills/impeccable/scripts/detector.js', 'export const x = 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- write('src/Hero.tsx', 'export const Hero = () => 2;\n'); // dirty app code
- write('.claude/skills/impeccable/scripts/detector.js', 'export const x = 2;\n'); // dirty vendored skill
- const s = await gatherSignals(scratch);
- assert.equal(s.scan.via, 'git-changes');
- assert.deepEqual(s.scan.targets, ['src/Hero.tsx']); // harness path filtered out
- });
-
- it('keeps hidden-source-dir files (VitePress/Storybook) in scan targets', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('.vitepress/theme/Layout.vue', '\n');
- write('.claude/skills/impeccable/scripts/detector.js', 'export const x = 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- write('.vitepress/theme/Layout.vue', '\n'); // real UI source
- write('.claude/skills/impeccable/scripts/detector.js', 'export const x = 2;\n'); // vendored
- const s = await gatherSignals(scratch);
- assert.equal(s.scan.via, 'git-changes');
- assert.deepEqual(s.scan.targets, ['.vitepress/theme/Layout.vue']);
- });
-
- it('falls through to source dirs when only harness files changed (#303)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/Hero.tsx', 'export const Hero = () => null;\n');
- write('.cursor/skills/impeccable/example.css', 'a{}\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- write('.cursor/skills/impeccable/example.css', 'a{color:red}\n'); // only harness dirty
- const s = await gatherSignals(scratch);
- assert.equal(s.scan.via, 'source-dir');
- assert.deepEqual(s.scan.targets, ['src']);
- });
-
- it('diffs a feature branch against a develop integration branch (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'develop');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- git('checkout', '-q', '-b', 'feature/x');
- write('src/Hero.tsx', 'export const Hero = () => null;\n');
- git('add', '.');
- git('commit', '-qm', 'feature work');
- const s = await gatherSignals(scratch);
- // The hardcoded main/master candidate list found no base here, so the
- // committed feature work was invisible to the scan targets.
- assert.equal(s.git.base, 'develop');
- assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
- assert.deepEqual(s.scan.targets, ['src/Hero.tsx']);
- });
-
- it('prefers the remote default branch (origin/HEAD) as the diff base (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'trunk');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- // Fabricate the remote's default-branch symref without a network remote.
- git('update-ref', 'refs/remotes/origin/trunk', 'HEAD');
- git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/trunk');
- git('checkout', '-q', '-b', 'feature/y');
- write('src/Hero.tsx', 'export const Hero = () => null;\n');
- git('add', '.');
- git('commit', '-qm', 'feature work');
- const s = await gatherSignals(scratch);
- assert.equal(s.git.base, 'trunk');
- assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
- });
-
- it('a branch tracking the integration branch diffs against its upstream (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'release');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- // A self-pointing remote gives git the fetch refspec it needs to map
- // refs/heads/release -> refs/remotes/origin/release; no network involved.
- git('remote', 'add', 'origin', '.');
- git('update-ref', 'refs/remotes/origin/release', 'HEAD');
- git('checkout', '-q', '-b', 'feature/z');
- git('branch', '-q', '--set-upstream-to=origin/release');
- write('src/Hero.tsx', 'export const Hero = () => null;\n');
- git('add', '.');
- git('commit', '-qm', 'feature work');
- const s = await gatherSignals(scratch);
- assert.equal(s.git.base, 'release');
- assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
- });
-
- it('sitting on the integration branch itself falls back to the working tree', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'develop');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- write('src/App.tsx', 'export default 2;\n'); // dirty, uncommitted
- const s = await gatherSignals(scratch);
- // No self-diff: base must be null and the dirty working tree is the scope.
- assert.equal(s.git.base, null);
- assert.deepEqual(s.git.changedFiles, ['src/App.tsx']);
- });
-
- it('uses the remote-tracking ref when the base has no local branch (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'develop');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- git('update-ref', 'refs/remotes/origin/develop', 'HEAD');
- git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/develop');
- git('checkout', '-q', '-b', 'feature/w');
- git('branch', '-q', '-D', 'develop'); // remote default exists, local doesn't
- write('src/Hero.tsx', 'export const Hero = () => null;\n');
- git('add', '.');
- git('commit', '-qm', 'feature work');
- const s = await gatherSignals(scratch);
- assert.equal(s.git.base, 'develop');
- assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
- });
-
- it('honors an upstream on a non-origin remote (fork workflow) (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'release');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- git('remote', 'add', 'upstream', '.');
- git('update-ref', 'refs/remotes/upstream/release', 'HEAD');
- git('checkout', '-q', '-b', 'feature/v');
- git('branch', '-q', '--set-upstream-to=upstream/release');
- git('branch', '-q', '-D', 'release'); // the tracked base lives only on the fork parent
- write('src/Hero.tsx', 'export const Hero = () => null;\n');
- git('add', '.');
- git('commit', '-qm', 'feature work');
- const s = await gatherSignals(scratch);
- assert.equal(s.git.base, 'release');
- assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
- });
-
- it('an existing develop outranks a main-pointing origin/HEAD (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'main');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- git('branch', '-q', 'develop');
- // Classic git-flow with the platform default never flipped off main.
- git('update-ref', 'refs/remotes/origin/main', 'main');
- git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/main');
- git('checkout', '-q', 'develop');
- git('checkout', '-q', '-b', 'feature/g');
- write('src/Hero.tsx', 'export const Hero = () => null;\n');
- git('add', '.');
- git('commit', '-qm', 'feature work');
- const s = await gatherSignals(scratch);
- // Features merge to develop here; picking origin/HEAD's main would drag
- // the develop-vs-main divergence into scan targets.
- assert.equal(s.git.base, 'develop');
- assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
- });
-
- it('remote signals cannot bypass the integration-branch guard (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'main');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- git('branch', '-q', 'develop');
- git('checkout', '-q', 'develop');
- // The remote default is main; sitting on develop must still not produce
- // a develop-vs-main integration diff via the origin/HEAD signal.
- git('update-ref', 'refs/remotes/origin/main', 'main');
- git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/main');
- write('src/App.tsx', 'export default 2;\n'); // dirty on develop
- const s = await gatherSignals(scratch);
- assert.equal(s.git.base, null);
- assert.deepEqual(s.git.changedFiles, ['src/App.tsx']);
- });
-
- it('honors a local (slashless) upstream branch (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'canary');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- git('checkout', '-q', '-b', 'feature/u');
- git('branch', '-q', '--set-upstream-to=canary'); // local upstream, no remote
- write('src/Hero.tsx', 'export const Hero = () => null;\n');
- git('add', '.');
- git('commit', '-qm', 'feature work');
- const s = await gatherSignals(scratch);
- // canary is neither conventional nor remote, but the configured
- // upstream names it as the merge target.
- assert.equal(s.git.base, 'canary');
- assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
- });
-
- it('sitting on a non-standard default branch keeps the working-tree scope (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'trunk');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- git('branch', '-q', 'develop'); // a conventional name also exists
- git('update-ref', 'refs/remotes/origin/trunk', 'HEAD');
- git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/trunk');
- write('src/App.tsx', 'export default 2;\n'); // dirty on trunk
- const s = await gatherSignals(scratch);
- // trunk IS the integration branch (origin/HEAD says so); develop must
- // not win the candidate scan and produce a trunk-vs-develop diff.
- assert.equal(s.git.base, null);
- assert.deepEqual(s.git.changedFiles, ['src/App.tsx']);
- });
-
- it('a detached HEAD keeps the working-tree scope (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'main');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- git('branch', '-q', 'develop');
- git('checkout', '-q', '--detach');
- write('src/App.tsx', 'export default 2;\n'); // dirty on a detached tip
- const s = await gatherSignals(scratch);
- // A detached checkout has no branch identity to diff for; picking
- // develop here would refill changedFiles with integration divergence.
- assert.equal(s.git.base, null);
- assert.deepEqual(s.git.changedFiles, ['src/App.tsx']);
- });
-
- it('the integration guard sees non-origin remote defaults (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'trunk');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- git('branch', '-q', 'develop');
- // The only remote is upstream (fork-parent layout, no origin at all);
- // its default branch is trunk, which is exactly where we're sitting.
- git('remote', 'add', 'upstream', '.');
- git('update-ref', 'refs/remotes/upstream/trunk', 'HEAD');
- git('symbolic-ref', 'refs/remotes/upstream/HEAD', 'refs/remotes/upstream/trunk');
- write('src/App.tsx', 'export default 2;\n'); // dirty on trunk
- const s = await gatherSignals(scratch);
- assert.equal(s.git.base, null);
- assert.deepEqual(s.git.changedFiles, ['src/App.tsx']);
- });
-
- it('finds a develop that exists only on a non-origin remote (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'feature/f');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- // Fork-parent layout: develop lives only as upstream/develop, no local
- // copy, no origin remote, and the feature branch has no upstream.
- git('remote', 'add', 'upstream', '.');
- git('update-ref', 'refs/remotes/upstream/develop', 'HEAD');
- write('src/Hero.tsx', 'export const Hero = () => null;\n');
- git('add', '.');
- git('commit', '-qm', 'feature work');
- const s = await gatherSignals(scratch);
- assert.equal(s.git.base, 'develop');
- assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
- });
-
- it('a same-name default on a second remote still resolves (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'feature/h');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- git('remote', 'add', 'origin', '.');
- git('remote', 'add', 'upstream', '.');
- // origin advertises main but its tracking ref is gone (pruned); the
- // real main lives only as upstream/main. Name-level dedup must not
- // discard the upstream rev.
- git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/main');
- git('update-ref', 'refs/remotes/upstream/main', 'HEAD');
- git('symbolic-ref', 'refs/remotes/upstream/HEAD', 'refs/remotes/upstream/main');
- write('src/Hero.tsx', 'export const Hero = () => null;\n');
- git('add', '.');
- git('commit', '-qm', 'feature work');
- const s = await gatherSignals(scratch);
- assert.equal(s.git.base, 'main');
- assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
- });
-
- it('a local upstream with a slash in its name is not misparsed (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'release/2.0');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- git('checkout', '-q', '-b', 'hotfix/x');
- git('branch', '-q', '--set-upstream-to=release/2.0');
- write('src/Hero.tsx', 'export const Hero = () => null;\n');
- git('add', '.');
- git('commit', '-qm', 'hotfix work');
- const s = await gatherSignals(scratch);
- // "release" is not a remote here; the whole ref is the local base name.
- assert.equal(s.git.base, 'release/2.0');
- assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
- });
-
- it('a local upstream sharing the branch leaf name is not self-skipped (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'feature/foo');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- git('checkout', '-q', '-b', 'foo');
- git('branch', '-q', '--set-upstream-to=feature/foo');
- // Adversarial twist: a remote literally named "feature" exists, so any
- // prefix-based guess would still misread the LOCAL feature/foo upstream
- // as remote-tracking. Only the full symbolic ref disambiguates.
- git('remote', 'add', 'feature', '.');
- write('src/Hero.tsx', 'export const Hero = () => null;\n');
- git('add', '.');
- git('commit', '-qm', 'work');
- const s = await gatherSignals(scratch);
- // Truncating feature/foo to "foo" made it look like the current branch
- // and the valid upstream was discarded.
- assert.equal(s.git.base, 'feature/foo');
- assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
- });
-
- it('a pruned upstream tracking ref falls back to other remotes (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'feature/p');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- git('remote', 'add', 'origin', '.');
- git('remote', 'add', 'upstream', '.');
- // The branch tracks origin/main, but that tracking ref was pruned; the
- // live main exists only on the upstream remote.
- git('config', 'branch.feature/p.remote', 'origin');
- git('config', 'branch.feature/p.merge', 'refs/heads/main');
- git('update-ref', 'refs/remotes/upstream/main', 'HEAD');
- write('src/Hero.tsx', 'export const Hero = () => null;\n');
- git('add', '.');
- git('commit', '-qm', 'feature work');
- const s = await gatherSignals(scratch);
- assert.equal(s.git.base, 'main');
- assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
- });
-
- it('a remote-advertised default outranks a stale local checkout (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'main');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/base.css', 'a{}\n');
- git('add', '.');
- git('commit', '-qm', 'A');
- write('src/extra.css', 'b{}\n');
- git('add', '.');
- git('commit', '-qm', 'A2');
- git('update-ref', 'refs/remotes/origin/main', 'HEAD');
- git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/main');
- git('checkout', '-q', '-b', 'feature/s');
- write('src/Hero.tsx', 'export const Hero = () => null;\n');
- git('add', '.');
- git('commit', '-qm', 'feature work');
- // The local main checkout is stale (still at A); the remote default is
- // at A2. Diffing against the stale local would drag src/extra.css in.
- git('branch', '-f', 'main', 'HEAD~2');
- const s = await gatherSignals(scratch);
- assert.equal(s.git.base, 'main');
- assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
- });
-
- it('a develop-pointing remote default outranks a stale local develop (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'develop');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/base.css', 'a{}\n');
- git('add', '.');
- git('commit', '-qm', 'A');
- write('src/extra.css', 'b{}\n');
- git('add', '.');
- git('commit', '-qm', 'A2');
- git('update-ref', 'refs/remotes/origin/develop', 'HEAD');
- git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/develop');
- git('checkout', '-q', '-b', 'feature/t');
- write('src/Hero.tsx', 'export const Hero = () => null;\n');
- git('add', '.');
- git('commit', '-qm', 'feature work');
- git('branch', '-f', 'develop', 'HEAD~2'); // local develop is stale at A
- const s = await gatherSignals(scratch);
- assert.equal(s.git.base, 'develop');
- assert.deepEqual(s.git.changedFiles, ['src/Hero.tsx']);
- });
-
- it('never diffs one integration branch against another (#302)', async () => {
- const { execFileSync } = await import('node:child_process');
- const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
- git('init', '-q', '-b', 'main');
- git('config', 'user.email', 't@example.com');
- git('config', 'user.name', 'Test');
- write('src/App.tsx', 'export default 1;\n');
- git('add', '.');
- git('commit', '-qm', 'init');
- git('branch', '-q', 'develop'); // both integration branches exist
- write('src/App.tsx', 'export default 2;\n'); // dirty on main
- const s = await gatherSignals(scratch);
- // Sitting on main must not pick develop as a base; the dirty working
- // tree is the scope, exactly as before this change.
- assert.equal(s.git.base, null);
- assert.deepEqual(s.git.changedFiles, ['src/App.tsx']);
- });
-
- it('has empty scan.targets only when there is no code at all', async () => {
- const s = await gatherSignals(scratch);
- assert.deepEqual(s.scan.targets, []);
- assert.equal(s.scan.via, null);
- });
-});
-
-describe('context-signals CLI', () => {
- it('emits valid JSON with all top-level signal groups', async () => {
- const { spawnSync } = await import('node:child_process');
- const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8' });
- assert.equal(res.status, 0);
- const parsed = JSON.parse(res.stdout);
- for (const k of ['setup', 'critique', 'git', 'devServer']) {
- assert.ok(k in parsed, `expected "${k}" in signals output`);
- }
- });
-});
diff --git a/tests/context.test.mjs b/tests/context.test.mjs
deleted file mode 100644
index 76db0648d..000000000
--- a/tests/context.test.mjs
+++ /dev/null
@@ -1,1647 +0,0 @@
-/**
- * Tests for the shared context loader (PRODUCT.md / DESIGN.md resolver).
- * Run with: node --test tests/load-context.test.mjs
- *
- * Covers the resolution order:
- * 1. cwd, when canonical files are at the root
- * 2. Auto-fallback to .agents/context/ then docs/
- * 3. IMPECCABLE_CONTEXT_DIR env var as a power-user escape hatch (only
- * consulted when the default paths come up empty)
- * 4. Default to cwd when nothing is found
- *
- * Each test runs in its own scratch dir under os.tmpdir() so the suite stays
- * independent of the project root and parallel-safe.
- */
-
-import { describe, it, beforeEach, afterEach } from 'node:test';
-import { spawnSync, spawn } from 'node:child_process';
-import http from 'node:http';
-import assert from 'node:assert/strict';
-import fs from 'node:fs';
-import path from 'node:path';
-import os from 'node:os';
-
-import { loadContext, resolveContextDir, resolveProjectRoot, extractPlatform, hasVisualImplementation } from '../skill/scripts/context.mjs';
-
-import { fileURLToPath } from 'node:url';
-const SCRIPT_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'skill', 'scripts', 'context.mjs');
-
-let scratch;
-let savedEnv;
-
-beforeEach(() => {
- scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-loadctx-'));
- savedEnv = process.env.IMPECCABLE_CONTEXT_DIR;
- delete process.env.IMPECCABLE_CONTEXT_DIR;
-});
-
-afterEach(() => {
- if (savedEnv === undefined) delete process.env.IMPECCABLE_CONTEXT_DIR;
- else process.env.IMPECCABLE_CONTEXT_DIR = savedEnv;
- fs.rmSync(scratch, { recursive: true, force: true });
-});
-
-function write(rel, body = '# placeholder\n') {
- const abs = path.join(scratch, rel);
- fs.mkdirSync(path.dirname(abs), { recursive: true });
- fs.writeFileSync(abs, body);
- return abs;
-}
-
-// Stage a runnable copy of context.mjs plus its whole lib/ directory.
-// The bundle tests used to enumerate the helpers they needed, which turned
-// every new import in context.mjs into a mysterious non-zero exit here.
-// Copying the directory keeps them honest about what the real script loads.
-function stageContextBundle(scriptsDir, { providerId } = {}) {
- const libSrc = path.join(path.dirname(SCRIPT_PATH), 'lib');
- const libDest = path.join(scriptsDir, 'lib');
- fs.mkdirSync(scriptsDir, { recursive: true });
- fs.copyFileSync(SCRIPT_PATH, path.join(scriptsDir, 'context.mjs'));
- fs.cpSync(libSrc, libDest, { recursive: true });
- if (providerId) {
- const providerPath = path.join(libDest, 'provider.mjs');
- fs.writeFileSync(
- providerPath,
- fs.readFileSync(providerPath, 'utf8')
- .replace("IMPECCABLE_PROVIDER_ID = 'source'", `IMPECCABLE_PROVIDER_ID = '${providerId}'`),
- );
- }
- return path.join(scriptsDir, 'context.mjs');
-}
-
-function parseTargetSelection(stdout) {
- const tail = stdout.split('TARGET_SELECTION_REQUIRED:\n')[1];
- assert.ok(tail, `missing TARGET_SELECTION_REQUIRED block in:\n${stdout}`);
- return JSON.parse(tail.split('\n\n')[0].trim());
-}
-
-describe('resolveContextDir', () => {
- it('returns cwd when PRODUCT.md is at the root', () => {
- write('PRODUCT.md');
- assert.equal(resolveContextDir(scratch), scratch);
- });
-
- it('returns cwd when DESIGN.md is at the root', () => {
- write('DESIGN.md');
- assert.equal(resolveContextDir(scratch), scratch);
- });
-
- it('falls back to .agents/context/ when root is clean', () => {
- write('.agents/context/PRODUCT.md');
- assert.equal(resolveContextDir(scratch), path.join(scratch, '.agents', 'context'));
- });
-
- it('falls back to docs/ when root is clean and .agents/context/ is empty', () => {
- write('docs/PRODUCT.md');
- assert.equal(resolveContextDir(scratch), path.join(scratch, 'docs'));
- });
-
- it('prefers .agents/context/ over docs/ when both exist', () => {
- write('.agents/context/PRODUCT.md');
- write('docs/PRODUCT.md');
- assert.equal(resolveContextDir(scratch), path.join(scratch, '.agents', 'context'));
- });
-
- it('prefers cwd over fallback dirs when canonical files are at the root', () => {
- write('PRODUCT.md');
- write('.agents/context/PRODUCT.md');
- assert.equal(resolveContextDir(scratch), scratch);
- });
-
- it('uses IMPECCABLE_CONTEXT_DIR as a fallback when defaults are empty (relative path)', () => {
- write('design/PRODUCT.md');
- process.env.IMPECCABLE_CONTEXT_DIR = 'design';
- assert.equal(resolveContextDir(scratch), path.join(scratch, 'design'));
- });
-
- it('uses IMPECCABLE_CONTEXT_DIR as a fallback when defaults are empty (absolute path)', () => {
- const elsewhere = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-elsewhere-'));
- try {
- process.env.IMPECCABLE_CONTEXT_DIR = elsewhere;
- assert.equal(resolveContextDir(scratch), elsewhere);
- } finally {
- fs.rmSync(elsewhere, { recursive: true, force: true });
- }
- });
-
- it('default paths win over IMPECCABLE_CONTEXT_DIR (lazy escape hatch)', () => {
- write('PRODUCT.md', 'root');
- write('design/PRODUCT.md', 'overridden');
- process.env.IMPECCABLE_CONTEXT_DIR = 'design';
- assert.equal(resolveContextDir(scratch), scratch);
- });
-
- it('ignores empty IMPECCABLE_CONTEXT_DIR', () => {
- write('PRODUCT.md');
- process.env.IMPECCABLE_CONTEXT_DIR = ' ';
- assert.equal(resolveContextDir(scratch), scratch);
- });
-
- it('returns cwd when nothing is found anywhere', () => {
- assert.equal(resolveContextDir(scratch), scratch);
- });
-});
-
-describe('loadContext', () => {
- it('reads PRODUCT.md and DESIGN.md from the root', () => {
- write('PRODUCT.md', '# product content\n');
- write('DESIGN.md', '# design content\n');
- const ctx = loadContext(scratch);
- assert.equal(ctx.hasProduct, true);
- assert.equal(ctx.hasDesign, true);
- assert.match(ctx.product, /product content/);
- assert.match(ctx.design, /design content/);
- assert.equal(ctx.productPath, 'PRODUCT.md');
- assert.equal(ctx.designPath, 'DESIGN.md');
- assert.equal(ctx.contextDir, scratch);
- });
-
- it('reads from .agents/context/ when the root is clean', () => {
- write('.agents/context/PRODUCT.md', '# product in agents\n');
- write('.agents/context/DESIGN.md', '# design in agents\n');
- const ctx = loadContext(scratch);
- assert.equal(ctx.hasProduct, true);
- assert.equal(ctx.hasDesign, true);
- assert.match(ctx.product, /product in agents/);
- assert.equal(ctx.contextDir, path.join(scratch, '.agents', 'context'));
- // productPath/designPath are relative to cwd, not contextDir
- assert.equal(ctx.productPath, path.join('.agents', 'context', 'PRODUCT.md'));
- assert.equal(ctx.designPath, path.join('.agents', 'context', 'DESIGN.md'));
- });
-
- it('reads from docs/ when .agents/context/ is empty', () => {
- write('docs/PRODUCT.md', '# product in docs\n');
- const ctx = loadContext(scratch);
- assert.equal(ctx.hasProduct, true);
- assert.equal(ctx.contextDir, path.join(scratch, 'docs'));
- assert.equal(ctx.productPath, path.join('docs', 'PRODUCT.md'));
- });
-});
-
-describe('loadContext (monorepo project context)', () => {
- function writeMonorepo() {
- write('package.json', JSON.stringify({
- private: true,
- workspaces: ['apps/*', 'packages/*'],
- }, null, 2));
- write('turbo.json', JSON.stringify({ tasks: {} }));
- write('PRODUCT.md', '# Root product\n');
- write('DESIGN.md', '# Root design\n');
- for (const app of ['marketing', 'dashboard', 'admin']) {
- write(`apps/${app}/src/App.jsx`, `export default function App() { return ${JSON.stringify(app)}; }\n`);
- }
- }
-
- it('inherits root PRODUCT.md and DESIGN.md for child apps without project context', () => {
- writeMonorepo();
-
- for (const app of ['marketing', 'dashboard', 'admin']) {
- const ctx = loadContext(scratch, { targetPath: `apps/${app}/src/App.jsx` });
- assert.equal(ctx.hasProduct, true);
- assert.equal(ctx.hasDesign, true);
- assert.match(ctx.product, /Root product/);
- assert.match(ctx.design, /Root design/);
- assert.equal(ctx.productPath, 'PRODUCT.md');
- assert.equal(ctx.designPath, 'DESIGN.md');
- assert.equal(ctx.projectRoot, path.join(scratch, 'apps', app));
- assert.equal(ctx.repoRoot, scratch);
- assert.equal(ctx.isMonorepo, true);
- }
- });
-
- it('lets child app context override root files independently', () => {
- writeMonorepo();
- write('apps/marketing/PRODUCT.md', '# Marketing product\n');
- write('apps/marketing/DESIGN.md', '# Marketing design\n');
- write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
-
- const marketing = loadContext(scratch, { targetPath: 'apps/marketing/src/App.jsx' });
- assert.match(marketing.product, /Marketing product/);
- assert.match(marketing.design, /Marketing design/);
- assert.equal(marketing.productPath, path.join('apps', 'marketing', 'PRODUCT.md'));
- assert.equal(marketing.designPath, path.join('apps', 'marketing', 'DESIGN.md'));
-
- const dashboard = loadContext(scratch, { targetPath: 'apps/dashboard/src/App.jsx' });
- assert.match(dashboard.product, /Dashboard product/);
- assert.match(dashboard.design, /Root design/);
- assert.equal(dashboard.productPath, path.join('apps', 'dashboard', 'PRODUCT.md'));
- assert.equal(dashboard.designPath, 'DESIGN.md');
-
- const admin = loadContext(scratch, { targetPath: 'apps/admin/src/App.jsx' });
- assert.match(admin.product, /Root product/);
- assert.match(admin.design, /Root design/);
- assert.equal(admin.productPath, 'PRODUCT.md');
- assert.equal(admin.designPath, 'DESIGN.md');
- });
-
- it('resolves child project roots from cwd inside a workspace', () => {
- writeMonorepo();
- const appDir = path.join(scratch, 'apps', 'dashboard');
- const ctx = loadContext(appDir);
- assert.match(ctx.product, /Root product/);
- assert.match(ctx.design, /Root design/);
- assert.equal(ctx.productPath, path.join('..', '..', 'PRODUCT.md'));
- assert.equal(ctx.designPath, path.join('..', '..', 'DESIGN.md'));
- assert.equal(resolveProjectRoot(appDir), appDir);
- });
-
- it('supports pnpm workspace patterns when resolving the active project', () => {
- write('pnpm-workspace.yaml', 'packages:\n - "services/*"\n');
- write('PRODUCT.md', '# Root product\n');
- write('DESIGN.md', '# Root design\n');
- write('services/checkout/src/App.jsx');
-
- const ctx = loadContext(scratch, { targetPath: 'services/checkout/src/App.jsx' });
- assert.equal(ctx.projectRoot, path.join(scratch, 'services', 'checkout'));
- assert.match(ctx.product, /Root product/);
- assert.match(ctx.design, /Root design/);
- });
-
- it('supports pnpm workspace patterns with inline comments and flow arrays', () => {
- write('pnpm-workspace.yaml', 'packages: ["services/*", "tools/*"] # workspace packages\n');
- write('PRODUCT.md', '# Root product\n');
- write('DESIGN.md', '# Root design\n');
- write('tools/inspector/PRODUCT.md', '# Inspector product\n');
- write('tools/inspector/src/App.jsx');
-
- const ctx = loadContext(scratch, { targetPath: 'tools/inspector/src/App.jsx' });
- assert.equal(ctx.projectRoot, path.join(scratch, 'tools', 'inspector'));
- assert.match(ctx.product, /Inspector product/);
- assert.match(ctx.design, /Root design/);
- });
-
- it('honors negated pnpm workspace patterns', () => {
- write('pnpm-workspace.yaml', 'packages:\n - "packages/**"\n - "!packages/private/**"\n');
- write('PRODUCT.md', '# Root product\n');
- write('DESIGN.md', '# Root design\n');
- write('packages/private/app/src/index.ts', 'export const hidden = true;\n');
-
- const ctx = loadContext(scratch, { targetPath: 'packages/private/app/src/index.ts' });
- assert.equal(ctx.projectRoot, scratch);
- assert.match(ctx.product, /Root product/);
- assert.match(ctx.design, /Root design/);
- });
-
- it('keeps unmatched child projects from being hijacked by an ancestor workspace', () => {
- write('package.json', JSON.stringify({
- private: true,
- workspaces: ['apps/*'],
- }, null, 2));
- write('PRODUCT.md', '# Ancestor product\n');
- write('side-project/PRODUCT.md', '# Side project product\n');
- write('side-project/src/App.jsx', 'export default null;\n');
-
- const ctx = loadContext(path.join(scratch, 'side-project'), { targetPath: 'src/App.jsx' });
- assert.equal(ctx.projectRoot, path.join(scratch, 'side-project'));
- assert.match(ctx.product, /Side project product/);
- assert.equal(ctx.productPath, 'PRODUCT.md');
- });
-
- it('does not reuse stale project resolution after workspace markers change', () => {
- write('PRODUCT.md', '# Root product\n');
- write('apps/dashboard/package.json', JSON.stringify({ name: 'dashboard' }, null, 2));
- write('apps/dashboard/src/App.jsx', 'export default null;\n');
-
- const before = loadContext(scratch, { targetPath: 'apps/dashboard/src/App.jsx' });
- assert.equal(before.projectRoot, scratch);
- assert.equal(before.isMonorepo, false);
- assert.match(before.product, /Root product/);
-
- write('package.json', JSON.stringify({
- private: true,
- workspaces: ['apps/*'],
- }, null, 2));
-
- const after = loadContext(scratch, { targetPath: 'apps/dashboard/src/App.jsx' });
- assert.equal(after.projectRoot, path.join(scratch, 'apps', 'dashboard'));
- assert.equal(after.isMonorepo, true);
- assert.match(after.product, /Root product/);
- });
-
- it('resolves an explicit target onto a nested product context in a non-monorepo repo', () => {
- write('nested/product/PRODUCT.md', '# Nested product\n');
- write('nested/product/DESIGN.md', '# Nested design\n');
- write('nested/product/file.ts', 'export const x = 1;\n');
-
- const ctx = loadContext(scratch, { targetPath: 'nested/product/file.ts' });
- assert.equal(ctx.isMonorepo, false);
- assert.equal(ctx.projectRoot, path.join(scratch, 'nested', 'product'));
- assert.equal(ctx.repoRoot, scratch);
- assert.match(ctx.product, /Nested product/);
- assert.match(ctx.design, /Nested design/);
- assert.equal(ctx.productPath, path.join('nested', 'product', 'PRODUCT.md'));
- assert.equal(ctx.designPath, path.join('nested', 'product', 'DESIGN.md'));
- });
-
- it('resolves a nested target whose context lives in .agents/context/', () => {
- write('nested/product/.agents/context/PRODUCT.md', '# Nested product\n');
- write('nested/product/file.ts', 'export const x = 1;\n');
-
- const ctx = loadContext(scratch, { targetPath: 'nested/product/file.ts' });
- assert.equal(ctx.isMonorepo, false);
- assert.equal(ctx.projectRoot, path.join(scratch, 'nested', 'product'));
- assert.match(ctx.product, /Nested product/);
- assert.equal(ctx.productPath, path.join('nested', 'product', '.agents', 'context', 'PRODUCT.md'));
- });
-
- it('resolves a nested target whose context lives in docs/', () => {
- write('nested/product/docs/DESIGN.md', '# Nested design\n');
- write('nested/product/file.ts', 'export const x = 1;\n');
-
- const ctx = loadContext(scratch, { targetPath: 'nested/product/file.ts' });
- assert.equal(ctx.isMonorepo, false);
- assert.equal(ctx.projectRoot, path.join(scratch, 'nested', 'product'));
- assert.match(ctx.design, /Nested design/);
- assert.equal(ctx.designPath, path.join('nested', 'product', 'docs', 'DESIGN.md'));
- });
-
- it('does not treat the root fallback context dirs as a nested product', () => {
- write('.agents/context/PRODUCT.md', '# Root product\n');
- write('.agents/context/notes/file.md', '# Notes\n');
-
- const ctx = loadContext(scratch, { targetPath: '.agents/context/notes/file.md' });
- assert.equal(ctx.projectRoot, scratch);
- assert.match(ctx.product, /Root product/);
- });
-
- it('inherits missing root context per-file for a nested target in a non-monorepo repo', () => {
- write('DESIGN.md', '# Root design\n');
- write('nested/product/PRODUCT.md', '# Nested product\n');
- write('nested/product/file.ts', 'export const x = 1;\n');
-
- const ctx = loadContext(scratch, { targetPath: 'nested/product/file.ts' });
- assert.equal(ctx.projectRoot, path.join(scratch, 'nested', 'product'));
- assert.match(ctx.product, /Nested product/);
- assert.match(ctx.design, /Root design/);
- assert.equal(ctx.designPath, 'DESIGN.md');
- });
-
- it('keeps the repo root project for targets without nearby context files', () => {
- write('PRODUCT.md', '# Root product\n');
- write('src/App.jsx', 'export default null;\n');
-
- const ctx = loadContext(scratch, { targetPath: 'src/App.jsx' });
- assert.equal(ctx.isMonorepo, false);
- assert.equal(ctx.projectRoot, scratch);
- assert.match(ctx.product, /Root product/);
- assert.equal(ctx.productPath, 'PRODUCT.md');
- });
-
- it('does not escape a nested git repo to an ancestor workspace', () => {
- write('package.json', JSON.stringify({
- private: true,
- workspaces: ['repos/*'],
- }, null, 2));
- write('PRODUCT.md', '# Outer product\n');
- write('DESIGN.md', '# Outer design\n');
- write('repos/standalone/.git/HEAD', 'ref: refs/heads/main\n');
- write('repos/standalone/PRODUCT.md', '# Standalone product\n');
- write('repos/standalone/src/App.jsx', 'export default null;\n');
-
- const project = path.join(scratch, 'repos', 'standalone');
- const ctx = loadContext(project, { targetPath: 'src/App.jsx' });
- assert.equal(ctx.isMonorepo, false);
- assert.equal(ctx.projectRoot, project);
- assert.equal(ctx.repoRoot, project);
- assert.match(ctx.product, /Standalone product/);
- assert.equal(ctx.productPath, 'PRODUCT.md');
- assert.equal(ctx.designPath, null);
- });
-
- it('resolves an explicit root target into a nested-git workspace child', () => {
- write('package.json', JSON.stringify({
- private: true,
- workspaces: ['repos/*'],
- }, null, 2));
- write('PRODUCT.md', '# Outer product\n');
- write('DESIGN.md', '# Outer design\n');
- write('repos/standalone/.git/HEAD', 'ref: refs/heads/main\n');
- write('repos/standalone/PRODUCT.md', '# Standalone product\n');
- write('repos/standalone/src/App.jsx', 'export default null;\n');
-
- const project = path.join(scratch, 'repos', 'standalone');
- const ctx = loadContext(scratch, { targetPath: 'repos/standalone/src/App.jsx' });
- assert.equal(ctx.isMonorepo, true);
- assert.equal(ctx.projectRoot, project);
- assert.equal(ctx.repoRoot, scratch);
- assert.match(ctx.product, /Standalone product/);
- assert.match(ctx.design, /Outer design/);
- assert.equal(ctx.productPath, path.join('repos', 'standalone', 'PRODUCT.md'));
- assert.equal(ctx.designPath, 'DESIGN.md');
- });
-
- it('supports double-star workspace patterns by resolving the shallow child project', () => {
- write('package.json', JSON.stringify({
- private: true,
- workspaces: ['libs/**'],
- }, null, 2));
- write('PRODUCT.md', '# Root product\n');
- write('DESIGN.md', '# Root design\n');
- write('libs/ui/PRODUCT.md', '# UI product\n');
- write('libs/ui/src/index.ts', 'export const ui = true;\n');
-
- const ctx = loadContext(scratch, { targetPath: 'libs/ui/src/index.ts' });
- assert.equal(ctx.projectRoot, path.join(scratch, 'libs', 'ui'));
- assert.match(ctx.product, /UI product/);
- assert.match(ctx.design, /Root design/);
- assert.equal(ctx.productPath, path.join('libs', 'ui', 'PRODUCT.md'));
- assert.equal(ctx.designPath, 'DESIGN.md');
- });
-
- it('supports packages/**/* workspace patterns without promoting src folders to projects', () => {
- write('package.json', JSON.stringify({
- private: true,
- workspaces: ['packages/**/*'],
- }, null, 2));
- write('PRODUCT.md', '# Root product\n');
- write('DESIGN.md', '# Root design\n');
- write('packages/dashboard/PRODUCT.md', '# Dashboard package product\n');
- write('packages/dashboard/src/index.ts', 'export const dashboard = true;\n');
-
- const ctx = loadContext(scratch, { targetPath: 'packages/dashboard/src/index.ts' });
- assert.equal(ctx.projectRoot, path.join(scratch, 'packages', 'dashboard'));
- assert.match(ctx.product, /Dashboard package product/);
- assert.match(ctx.design, /Root design/);
- assert.equal(ctx.productPath, path.join('packages', 'dashboard', 'PRODUCT.md'));
- assert.equal(ctx.designPath, 'DESIGN.md');
- });
-
- it('supports packages/** workspace patterns for nested package roots', () => {
- write('package.json', JSON.stringify({
- private: true,
- workspaces: ['packages/**'],
- }, null, 2));
- write('PRODUCT.md', '# Root product\n');
- write('DESIGN.md', '# Root design\n');
- write('packages/group/app/package.json', JSON.stringify({ name: '@acme/app' }, null, 2));
- write('packages/group/app/PRODUCT.md', '# Group app product\n');
- write('packages/group/app/src/index.ts', 'export const app = true;\n');
-
- const ctx = loadContext(scratch, { targetPath: 'packages/group/app/src/index.ts' });
- assert.equal(ctx.projectRoot, path.join(scratch, 'packages', 'group', 'app'));
- assert.match(ctx.product, /Group app product/);
- assert.match(ctx.design, /Root design/);
- });
-
- it('does not discover dependency or generated directories as workspace candidates', () => {
- write('package.json', JSON.stringify({
- private: true,
- workspaces: ['packages/**'],
- }, null, 2));
- write('PRODUCT.md', '# Root product\n');
- write('DESIGN.md', '# Root design\n');
- write('packages/ui/package.json', JSON.stringify({ name: '@acme/ui' }, null, 2));
- write('packages/ui/src/index.ts', 'export const ui = true;\n');
- write('packages/ui/node_modules/dep/package.json', JSON.stringify({ name: 'dep' }, null, 2));
- write('packages/ui/node_modules/dep/src/index.ts', 'export const dep = true;\n');
- write('packages/ui/dist/package.json', JSON.stringify({ name: '@acme/ui-dist' }, null, 2));
- write('packages/ui/dist/src/index.ts', 'export const dist = true;\n');
-
- const res = spawnSync(process.execPath, [SCRIPT_PATH], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0);
- const selection = parseTargetSelection(res.stdout);
-
- assert.deepEqual(selection.targetCandidates.map((candidate) => candidate.path), ['packages/ui']);
- });
-
- it('uses apps and packages folders as a fallback when a monorepo marker exists', () => {
- write('nx.json', '{}\n');
- write('PRODUCT.md', '# Root product\n');
- write('DESIGN.md', '# Root design\n');
- write('packages/ui/src/index.ts');
-
- const ctx = loadContext(scratch, { targetPath: 'packages/ui/src/index.ts' });
- assert.equal(ctx.projectRoot, path.join(scratch, 'packages', 'ui'));
- assert.match(ctx.product, /Root product/);
- assert.match(ctx.design, /Root design/);
- });
-
- it('does not treat turbo.json alone as a monorepo marker', () => {
- write('turbo.json', JSON.stringify({ tasks: {} }));
- write('PRODUCT.md', '# Root product\n');
- write('src/App.jsx', 'export default null;\n');
-
- const ctx = loadContext(scratch);
- assert.equal(ctx.isMonorepo, false);
-
- const res = spawnSync(process.execPath, [SCRIPT_PATH], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0);
- assert.doesNotMatch(res.stdout, /MONOREPO_TARGET_REQUIRED/);
- });
-
- it('supports --target in the CLI', async () => {
- writeMonorepo();
- write('apps/dashboard/PRODUCT.md', '# Dashboard product\n\n## Platform\n\nweb\n');
- const { spawnSync } = await import('node:child_process');
- const res = spawnSync(process.execPath, [SCRIPT_PATH, '--target', 'apps/dashboard/src/App.jsx'], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /# Dashboard product/);
- assert.match(res.stdout, /# DESIGN\.md\n\n# Root design/);
- assert.match(res.stdout, /RESOLVED_CONTEXT:/);
- assert.match(res.stdout, /"targetPath": "apps\/dashboard\/src\/App\.jsx"/);
- assert.match(res.stdout, /"productPath": "apps\/dashboard\/PRODUCT\.md"/);
- assert.match(res.stdout, /"designPath": "DESIGN\.md"/);
- assert.doesNotMatch(res.stdout, /REGISTER:/);
- });
-
- it('asks for an app when the CLI runs from a monorepo root without selection', () => {
- writeMonorepo();
- const res = spawnSync(process.execPath, [SCRIPT_PATH], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /TARGET_SELECTION_REQUIRED:/);
- assert.match(res.stdout, /"targetPath": null/);
- assert.match(res.stdout, /"path": "apps\/dashboard"/);
- assert.match(res.stdout, /"targetExample": "apps\/dashboard\/src\/App\.jsx"/);
- assert.match(res.stdout, /"productStatus": "inherited"/);
- assert.match(res.stdout, /"productPath": "PRODUCT\.md"/);
- assert.match(res.stdout, /"designStatus": "inherited"/);
- assert.match(res.stdout, /"designPath": "DESIGN\.md"/);
- assert.doesNotMatch(res.stdout, /# PRODUCT\.md/);
- assert.doesNotMatch(res.stdout, /# DESIGN\.md/);
- assert.doesNotMatch(res.stdout, /MONOREPO_TARGET_REQUIRED/);
- });
-
- it('describes child, inherited, and mixed context sources in app selection candidates', () => {
- writeMonorepo();
- write('apps/admin/PRODUCT.md', '# Admin product\n');
- write('apps/admin/DESIGN.md', '# Admin design\n');
- write('apps/marketing/PRODUCT.md', '# Marketing product\n');
-
- const res = spawnSync(process.execPath, [SCRIPT_PATH], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0);
- const selection = parseTargetSelection(res.stdout);
- const byPath = Object.fromEntries(selection.targetCandidates.map((candidate) => [candidate.path, candidate]));
-
- assert.deepEqual(
- {
- productStatus: byPath['apps/admin'].productStatus,
- productPath: byPath['apps/admin'].productPath,
- designStatus: byPath['apps/admin'].designStatus,
- designPath: byPath['apps/admin'].designPath,
- },
- {
- productStatus: 'child',
- productPath: 'apps/admin/PRODUCT.md',
- designStatus: 'child',
- designPath: 'apps/admin/DESIGN.md',
- },
- );
- assert.deepEqual(
- {
- productStatus: byPath['apps/dashboard'].productStatus,
- productPath: byPath['apps/dashboard'].productPath,
- designStatus: byPath['apps/dashboard'].designStatus,
- designPath: byPath['apps/dashboard'].designPath,
- },
- {
- productStatus: 'inherited',
- productPath: 'PRODUCT.md',
- designStatus: 'inherited',
- designPath: 'DESIGN.md',
- },
- );
- assert.deepEqual(
- {
- productStatus: byPath['apps/marketing'].productStatus,
- productPath: byPath['apps/marketing'].productPath,
- designStatus: byPath['apps/marketing'].designStatus,
- designPath: byPath['apps/marketing'].designPath,
- },
- {
- productStatus: 'child',
- productPath: 'apps/marketing/PRODUCT.md',
- designStatus: 'inherited',
- designPath: 'DESIGN.md',
- },
- );
- });
-
- it('marks missing context files in app selection candidates', () => {
- write('package.json', JSON.stringify({
- private: true,
- workspaces: ['apps/*'],
- }, null, 2));
- write('PRODUCT.md', '# Root product\n');
- write('apps/dashboard/src/App.jsx', 'export default null;\n');
-
- const res = spawnSync(process.execPath, [SCRIPT_PATH], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0);
- const selection = parseTargetSelection(res.stdout);
- const dashboard = selection.targetCandidates.find((candidate) => candidate.path === 'apps/dashboard');
- assert.equal(dashboard.productStatus, 'inherited');
- assert.equal(dashboard.productPath, 'PRODUCT.md');
- assert.equal(dashboard.designStatus, 'missing');
- assert.equal(dashboard.designPath, null);
- });
-
- it('asks for app selection before init when root context is missing but child context exists', () => {
- write('package.json', JSON.stringify({
- private: true,
- workspaces: ['apps/*'],
- }, null, 2));
- write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
- write('apps/dashboard/src/App.jsx', 'export default null;\n');
-
- const res = spawnSync(process.execPath, [SCRIPT_PATH], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /TARGET_SELECTION_REQUIRED:/);
- assert.match(res.stdout, /"path": "apps\/dashboard"/);
- assert.doesNotMatch(res.stdout, /^NO_PRODUCT_MD:/);
- });
-
- it('excludes negated workspace packages from the selection candidates', () => {
- write('pnpm-workspace.yaml', 'packages:\n - "packages/*"\n - "!packages/internal"\n');
- write('PRODUCT.md', '# Root product\n');
- write('packages/web/src/App.jsx', 'export default null;\n');
- write('packages/internal/src/App.jsx', 'export default null;\n');
-
- const res = spawnSync(process.execPath, [SCRIPT_PATH], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0);
- const selection = parseTargetSelection(res.stdout);
- const paths = selection.targetCandidates.map((candidate) => candidate.path);
- assert.ok(paths.includes('packages/web'), `expected packages/web in ${JSON.stringify(paths)}`);
- assert.ok(!paths.includes('packages/internal'), `packages/internal should be excluded: ${JSON.stringify(paths)}`);
- });
-
- it('does not block on target selection when the monorepo has no child apps', () => {
- write('package.json', JSON.stringify({ private: true, workspaces: ['.'] }, null, 2));
- write('PRODUCT.md', '# Root product\n');
- write('DESIGN.md', '# Root design\n');
-
- const res = spawnSync(process.execPath, [SCRIPT_PATH], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0);
- assert.doesNotMatch(res.stdout, /TARGET_SELECTION_REQUIRED/);
- assert.match(res.stdout, /# Root product/);
- });
-
- it('lets --target . explicitly select the monorepo root', () => {
- writeMonorepo();
- const res = spawnSync(process.execPath, [SCRIPT_PATH, '--target', '.'], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /# PRODUCT\.md\n\n# Root product/);
- assert.match(res.stdout, /# DESIGN\.md\n\n# Root design/);
- assert.match(res.stdout, /"targetPath": "\."/);
- assert.doesNotMatch(res.stdout, /TARGET_SELECTION_REQUIRED/);
- });
-
- it('does not parse --help as a --target value', () => {
- writeMonorepo();
- const res = spawnSync(process.execPath, [SCRIPT_PATH, '--target', '--help'], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 1);
- assert.match(res.stderr, /--target requires a path value/);
- assert.equal(res.stdout, '');
- });
-
- it('uses the last --target value when duplicate target flags are provided', () => {
- writeMonorepo();
- write('apps/marketing/PRODUCT.md', '# Marketing product\n');
- write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
-
- const res = spawnSync(process.execPath, [
- SCRIPT_PATH,
- '--target', 'apps/marketing/src/App.jsx',
- '--target', 'apps/dashboard/src/App.jsx',
- ], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0, res.stderr);
- assert.match(res.stdout, /# Dashboard product/);
- assert.match(res.stdout, /"targetPath": "apps\/dashboard\/src\/App\.jsx"/);
- assert.doesNotMatch(res.stdout, /# Marketing product/);
- });
-
- it('warns when --target names a missing path in a monorepo', () => {
- writeMonorepo();
- const res = spawnSync(process.execPath, [SCRIPT_PATH, '--target', 'apps/dashboard/routes/pricing'], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
-
- assert.equal(res.status, 0, res.stderr);
- assert.match(res.stdout, /RESOLVED_CONTEXT:/);
- assert.match(res.stdout, /"targetExists": false/);
- assert.match(res.stdout, /MONOREPO_TARGET_REQUIRED/);
- });
-
- it('asks for app selection even when root PRODUCT.md is absent', () => {
- write('package.json', JSON.stringify({
- private: true,
- workspaces: ['apps/*'],
- }, null, 2));
- write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
- write('apps/dashboard/src/App.jsx', 'export default null;\n');
-
- const res = spawnSync(process.execPath, [SCRIPT_PATH], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /TARGET_SELECTION_REQUIRED:/);
- assert.match(res.stdout, /"path": "apps\/dashboard"/);
- assert.doesNotMatch(res.stdout, /^NO_PRODUCT_MD:/);
- });
-});
-
-describe('loadContext (impeccable projectRoots config)', () => {
- function writeSkinsConfig(extra = {}) {
- write('.impeccable/config.json', JSON.stringify({ projectRoots: ['docs/design/skins/*'], ...extra }, null, 2));
- write('PRODUCT.md', '# Root product\n');
- write('DESIGN.md', '# Root design\n');
- }
-
- it('treats a config-declared context root as a monorepo with no package-manager files', () => {
- writeSkinsConfig();
- write('docs/design/skins/neon-seoul/DESIGN.md', '# Neon Seoul design\n');
-
- const ctx = loadContext(scratch, { targetPath: 'docs/design/skins/neon-seoul' });
- assert.equal(ctx.isMonorepo, true);
- assert.equal(ctx.projectRoot, path.join(scratch, 'docs', 'design', 'skins', 'neon-seoul'));
- assert.equal(ctx.repoRoot, scratch);
- // The skin uses its own DESIGN.md and inherits the root PRODUCT.md per file.
- assert.match(ctx.design, /Neon Seoul design/);
- assert.match(ctx.product, /Root product/);
- assert.equal(ctx.designPath, path.join('docs', 'design', 'skins', 'neon-seoul', 'DESIGN.md'));
- assert.equal(ctx.productPath, 'PRODUCT.md');
- });
-
- it('resolves a config-declared child from cwd inside the folder', () => {
- writeSkinsConfig();
- write('docs/design/skins/marble/DESIGN.md', '# Marble design\n');
-
- const skinDir = path.join(scratch, 'docs', 'design', 'skins', 'marble');
- const ctx = loadContext(skinDir);
- assert.equal(ctx.isMonorepo, true);
- assert.equal(ctx.projectRoot, skinDir);
- assert.match(ctx.design, /Marble design/);
- assert.match(ctx.product, /Root product/);
- });
-
- it('extends shared projectRoots with config.local.json', () => {
- write('.impeccable/config.json', JSON.stringify({ projectRoots: ['docs/design/skins/*'] }, null, 2));
- write('.impeccable/config.local.json', JSON.stringify({ projectRoots: ['experiments/*'] }, null, 2));
- write('PRODUCT.md', '# Root product\n');
- write('DESIGN.md', '# Root design\n');
- write('experiments/wip/DESIGN.md', '# WIP design\n');
-
- const ctx = loadContext(scratch, { targetPath: 'experiments/wip' });
- assert.equal(ctx.projectRoot, path.join(scratch, 'experiments', 'wip'));
- assert.match(ctx.design, /WIP design/);
- assert.match(ctx.product, /Root product/);
- });
-
- it('does not treat an .impeccable config without projectRoots as a monorepo', () => {
- write('.impeccable/config.json', JSON.stringify({ hook: { consent: 'accepted' } }, null, 2));
- write('PRODUCT.md', '# Root product\n');
- write('docs/design/skins/marble/DESIGN.md', '# Marble design\n');
-
- const ctx = loadContext(scratch, { targetPath: 'docs/design/skins/marble' });
- assert.equal(ctx.isMonorepo, false);
- });
-
- it('asks for app selection from a config-declared monorepo root', () => {
- write('.impeccable/config.json', JSON.stringify({ projectRoots: ['docs/design/skins/*'] }, null, 2));
- write('PRODUCT.md', '# Root product\n');
- write('DESIGN.md', '# Root design\n');
- write('docs/design/skins/neon-seoul/DESIGN.md', '# Neon Seoul\n');
- write('docs/design/skins/marble/DESIGN.md', '# Marble\n');
-
- const res = spawnSync(process.execPath, [SCRIPT_PATH], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0);
- const selection = parseTargetSelection(res.stdout);
- const paths = selection.targetCandidates.map((candidate) => candidate.path).sort();
- assert.deepEqual(paths, ['docs/design/skins/marble', 'docs/design/skins/neon-seoul']);
- });
-
- // Composition with package-manager workspaces: a path matched by any
- // projectRoots pattern (positive or negated) is governed by the impeccable
- // config alone; package-manager patterns fill in the paths it does not match.
- describe('composition with package-manager workspaces', () => {
- function selectionPaths() {
- const res = spawnSync(process.execPath, [SCRIPT_PATH], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0, res.stderr);
- const selection = parseTargetSelection(res.stdout);
- return selection.targetCandidates.map((candidate) => candidate.path).sort();
- }
-
- it('lets an impeccable negation exclude a package-manager workspace', () => {
- write('package.json', JSON.stringify({ private: true, workspaces: ['apps/*'] }, null, 2));
- write('.impeccable/config.json', JSON.stringify({ projectRoots: ['!apps/internal'] }, null, 2));
- write('PRODUCT.md', '# Root product\n');
- write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
- write('apps/internal/PRODUCT.md', '# Internal product\n');
-
- const ctx = loadContext(scratch, { targetPath: 'apps/internal' });
- assert.equal(ctx.isMonorepo, true);
- assert.equal(ctx.projectRoot, scratch);
- assert.match(ctx.product, /Root product/);
- assert.deepEqual(selectionPaths(), ['apps/dashboard']);
- });
-
- it('keeps a package-manager negation scoped to its own source', () => {
- write('package.json', JSON.stringify({
- private: true,
- workspaces: ['apps/*', '!docs/design/skins/marble'],
- }, null, 2));
- write('.impeccable/config.json', JSON.stringify({ projectRoots: ['docs/design/skins/*'] }, null, 2));
- write('PRODUCT.md', '# Root product\n');
- write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
- write('docs/design/skins/marble/DESIGN.md', '# Marble design\n');
-
- const ctx = loadContext(scratch, { targetPath: 'docs/design/skins/marble' });
- assert.equal(ctx.projectRoot, path.join(scratch, 'docs', 'design', 'skins', 'marble'));
- assert.match(ctx.design, /Marble design/);
- assert.deepEqual(selectionPaths(), ['apps/dashboard', 'docs/design/skins/marble']);
- });
-
- it('gives a broad impeccable pattern the boundary over a deeper package workspace', () => {
- write('package.json', JSON.stringify({ private: true, workspaces: ['apps/web/packages/ui'] }, null, 2));
- write('.impeccable/config.json', JSON.stringify({ projectRoots: ['apps/*'] }, null, 2));
- write('PRODUCT.md', '# Root product\n');
- write('apps/web/PRODUCT.md', '# Web product\n');
- write('apps/web/packages/ui/PRODUCT.md', '# UI product\n');
- write('apps/web/packages/ui/src/Button.jsx', 'export default null;\n');
-
- const ctx = loadContext(scratch, { targetPath: 'apps/web/packages/ui/src/Button.jsx' });
- assert.equal(ctx.projectRoot, path.join(scratch, 'apps', 'web'));
- assert.match(ctx.product, /Web product/);
- // The subsumed package workspace must not appear as its own pick:
- // choosing it would silently resolve to apps/web.
- assert.deepEqual(selectionPaths(), ['apps/web']);
- });
-
- it('falls through to package workspaces for paths impeccable does not match', () => {
- write('package.json', JSON.stringify({ private: true, workspaces: ['apps/*'] }, null, 2));
- write('.impeccable/config.json', JSON.stringify({ projectRoots: ['docs/design/skins/*'] }, null, 2));
- write('PRODUCT.md', '# Root product\n');
- write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
- write('docs/design/skins/marble/DESIGN.md', '# Marble design\n');
-
- const ctx = loadContext(scratch, { targetPath: 'apps/dashboard' });
- assert.equal(ctx.projectRoot, path.join(scratch, 'apps', 'dashboard'));
- assert.match(ctx.product, /Dashboard product/);
- });
-
- it('resolves other workspaces normally when impeccable config only negates', () => {
- write('package.json', JSON.stringify({ private: true, workspaces: ['apps/*'] }, null, 2));
- write('.impeccable/config.json', JSON.stringify({ projectRoots: ['!apps/internal'] }, null, 2));
- write('PRODUCT.md', '# Root product\n');
- write('apps/dashboard/PRODUCT.md', '# Dashboard product\n');
- write('apps/internal/PRODUCT.md', '# Internal product\n');
-
- const ctx = loadContext(scratch, { targetPath: 'apps/dashboard' });
- assert.equal(ctx.isMonorepo, true);
- assert.equal(ctx.projectRoot, path.join(scratch, 'apps', 'dashboard'));
- assert.match(ctx.product, /Dashboard product/);
- });
- });
-});
-
-describe('loadContext (IMPECCABLE_CONTEXT_DIR escape hatch)', () => {
- it('reads from the override path when defaults are empty', () => {
- write('design/PRODUCT.md', '# overridden product\n');
- write('design/DESIGN.md', '# overridden design\n');
- process.env.IMPECCABLE_CONTEXT_DIR = 'design';
- const ctx = loadContext(scratch);
- assert.equal(ctx.hasProduct, true);
- assert.equal(ctx.hasDesign, true);
- assert.match(ctx.product, /overridden product/);
- assert.equal(ctx.contextDir, path.join(scratch, 'design'));
- });
-
- it('does not override defaults when both exist (lazy escape hatch)', () => {
- write('PRODUCT.md', '# root product\n');
- write('design/PRODUCT.md', '# overridden product\n');
- process.env.IMPECCABLE_CONTEXT_DIR = 'design';
- const ctx = loadContext(scratch);
- assert.match(ctx.product, /root product/);
- assert.equal(ctx.contextDir, scratch);
- });
-
- it('reports a missing override directory as no-context, not as a crash', () => {
- process.env.IMPECCABLE_CONTEXT_DIR = 'no/such/dir';
- const ctx = loadContext(scratch);
- assert.equal(ctx.hasProduct, false);
- assert.equal(ctx.hasDesign, false);
- assert.equal(ctx.product, null);
- assert.equal(ctx.design, null);
- assert.equal(ctx.contextDir, path.resolve(scratch, 'no/such/dir'));
- });
-});
-
-describe('extractPlatform', () => {
- it('returns null when the product is empty or platform-less', () => {
- assert.equal(extractPlatform(null), null);
- assert.equal(extractPlatform('# P\n\nno platform here\n'), null);
- });
-
- it('reads web / ios / android / adaptive case-insensitively', () => {
- assert.equal(extractPlatform('## Platform\n\nweb\n'), 'web');
- assert.equal(extractPlatform('## Platform\n\nios\n'), 'ios');
- assert.equal(extractPlatform('## platform\n\nANDROID\n'), 'android');
- assert.equal(extractPlatform('## Platform\n\nAdaptive\n'), 'adaptive');
- });
-
- it('reads a line naming both native targets as adaptive', () => {
- assert.equal(extractPlatform('## Platform\n\nios, android\n'), 'adaptive');
- assert.equal(extractPlatform('## Platform\n\nandroid and ios\n'), 'adaptive');
- assert.equal(extractPlatform('## Platform\n\nios/android\n'), 'adaptive');
- });
-
- it('does not read prose mentioning both targets as adaptive', () => {
- // Negations and explanations must fall through to the unrecognized-value
- // warning, never silently classify as cross-platform native.
- assert.equal(extractPlatform('## Platform\n\nweb only, not ios or android\n'), null);
- assert.equal(extractPlatform('## Platform\n\nios first, android later this year\n'), null);
- });
-
- it('returns null for an unrecognized value', () => {
- assert.equal(extractPlatform('## Platform\n\ndesktop\n'), null);
- assert.equal(extractPlatform('## Platform\n\nflutter\n'), null);
- });
-
- it('ignores a near-miss heading and reads the real one', () => {
- // `## Platform notes` must not be mistaken for the `## Platform` field.
- const product = '## Platform notes\n\nsome prose here\n\n## Platform\n\nios\n';
- assert.equal(extractPlatform(product), 'ios');
- });
-
- it('reads the first non-empty line after the heading', () => {
- assert.equal(extractPlatform('## Platform\n\n\nios\n'), 'ios');
- });
-
- it('treats an empty section followed by another heading as absent', () => {
- // An empty `## Platform` must not swallow the next heading as its value
- // (which would surface a nonsense "value `## Product Purpose` is not
- // recognized" warning from the CLI).
- assert.equal(extractPlatform('## Platform\n\n## Product Purpose\n\nAn app.\n'), null);
- });
-});
-
-describe('context.mjs CLI', () => {
- it('emits NO_PRODUCT_MD directive when no PRODUCT.md is found', async () => {
- const { spawnSync } = await import('node:child_process');
- const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /^NO_PRODUCT_MD:/);
- assert.match(res.stdout, /reference\/init\.md/);
- assert.match(res.stdout, /structured simulated-user interview/);
- assert.match(res.stdout, /PRODUCT_INIT_REQUIRED:/);
- });
-
- it('prints a PRODUCT.md markdown block when only PRODUCT.md exists', async () => {
- write('PRODUCT.md', '# Acme\n\nbody\n');
- const { spawnSync } = await import('node:child_process');
- const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /^# PRODUCT\.md/);
- assert.match(res.stdout, /# Acme/);
- assert.equal(res.stdout.includes('# DESIGN.md'), false);
- // Directives are appended after `---`; missing visual authority now
- // routes to new-work rather than back through product init.
- assert.match(res.stdout, /\n---\n\n/);
- assert.match(res.stdout, /WORLD_DISCOVERY_REQUIRED: PRODUCT\.md exists but no DESIGN\.md/);
- assert.match(res.stdout, /MANUAL_DETECTOR_REQUIRED:/);
- assert.match(res.stdout, /detect\.mjs --json /);
- });
-
- it('drains stdout before exit when the parent pipe is paused', async () => {
- const MARKER = 'END_MARKER_573';
- write('PRODUCT.md', `# Acme\n\n${'x'.repeat(256 * 1024)}\n\n${MARKER}\n`);
- const child = spawn(process.execPath, [SCRIPT_PATH], {
- cwd: scratch,
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- let stdout = '';
- child.stdout.on('data', (chunk) => { stdout += chunk; });
- child.stdout.pause();
- const resume = setTimeout(() => child.stdout.resume(), 100);
- const status = await new Promise((resolve, reject) => {
- child.on('error', reject);
- child.on('close', resolve);
- });
- clearTimeout(resume);
- assert.equal(status, 0);
- assert.match(stdout, /END_MARKER_573/);
- assert.match(stdout, /RESOLVED_CONTEXT:/);
- });
-
- // The build-path preference rides the unified config beside hook and
- // detector settings. The local file wins because whether a machine can
- // generate images is a property of that machine, not of the committed
- // default the rest of the team shares.
- describe('BUILD_PATH_DEFAULT', () => {
- const run = () => spawnSync(process.execPath, [SCRIPT_PATH], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
-
- it('reports a recorded preference from the shared config', () => {
- write('PRODUCT.md', '# Acme\n');
- write('.impeccable/config.json', JSON.stringify({ buildPath: 'code' }));
- assert.match(run().stdout, /BUILD_PATH_DEFAULT: code \(from \.impeccable\/config\.json\)/);
- });
-
- it('lets the gitignored local config win over the committed one', () => {
- write('PRODUCT.md', '# Acme\n');
- write('.impeccable/config.json', JSON.stringify({ buildPath: 'comp' }));
- write('.impeccable/config.local.json', JSON.stringify({ buildPath: 'code' }));
- assert.match(run().stdout, /BUILD_PATH_DEFAULT: code \(from \.impeccable\/config\.local\.json\)/);
- });
-
- it('stays silent when nothing is recorded, leaving new-work its own default', () => {
- write('PRODUCT.md', '# Acme\n');
- assert.equal(run().stdout.includes('BUILD_PATH_DEFAULT'), false);
- });
-
- it('stays silent on a value nothing reads rather than guessing at it', () => {
- write('PRODUCT.md', '# Acme\n');
- write('.impeccable/config.json', JSON.stringify({ buildPath: 'code-first' }));
- assert.equal(run().stdout.includes('BUILD_PATH_DEFAULT'), false);
- });
-
- // A monorepo commits the preference once at the repo root for every app in
- // it. Reading only projectRoot left the staleness finding (which does read
- // both) silent while the directive never named the value.
- describe('in a monorepo', () => {
- const writeWorkspace = () => {
- write('package.json', JSON.stringify({ private: true, workspaces: ['apps/*'] }));
- write('turbo.json', JSON.stringify({ tasks: {} }));
- write('PRODUCT.md', '# Root product\n');
- write('apps/dashboard/src/App.jsx', 'export default function App() { return "d"; }\n');
- };
- const runFromWorkspace = () => spawnSync(process.execPath, [SCRIPT_PATH, '--target', 'apps/dashboard/src/App.jsx'], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
-
- it('falls back to the repo-root value for a workspace that sets none', () => {
- writeWorkspace();
- write('.impeccable/config.json', JSON.stringify({ buildPath: 'code' }));
- assert.match(runFromWorkspace().stdout, /BUILD_PATH_DEFAULT: code/);
- });
-
- it('lets the workspace override the repo root, nearest root first', () => {
- writeWorkspace();
- write('.impeccable/config.json', JSON.stringify({ buildPath: 'code' }));
- write('apps/dashboard/.impeccable/config.json', JSON.stringify({ buildPath: 'comp' }));
- assert.match(runFromWorkspace().stdout, /BUILD_PATH_DEFAULT: comp/);
- });
-
- // Running from one workspace while targeting another must not hand the
- // target the caller's preference. The invoking directory is not evidence
- // about the project being worked on.
- it('does not let the invoking workspace lend its value to the target', () => {
- writeWorkspace();
- write('apps/marketing/src/App.jsx', 'export default function App() { return "m"; }\n');
- write('.impeccable/config.json', JSON.stringify({ buildPath: 'code' }));
- write('apps/marketing/.impeccable/config.json', JSON.stringify({ buildPath: 'comp' }));
- // Absolute, so the target actually resolves onto dashboard. A relative
- // path would be read against the caller's cwd, which resolves the
- // project back to marketing and tests nothing.
- const res = spawnSync(process.execPath, [SCRIPT_PATH, '--target', path.join(scratch, 'apps', 'dashboard', 'src', 'App.jsx')], {
- cwd: path.join(scratch, 'apps', 'marketing'),
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- // The repo-root default, not marketing's comp.
- assert.match(res.stdout, /BUILD_PATH_DEFAULT: code/);
- });
- });
- });
-
- it('keeps the manual-detector directive out of early context when the current provider hook is active', () => {
- const scripts = path.join(scratch, 'bundle', 'skills', 'impeccable', 'scripts');
- stageContextBundle(scripts, { providerId: 'codex' });
-
- const project = path.join(scratch, 'project');
- fs.mkdirSync(path.join(project, '.codex'), { recursive: true });
- fs.writeFileSync(path.join(project, 'PRODUCT.md'), '# Acme\n');
- fs.writeFileSync(path.join(project, '.codex', 'hooks.json'), JSON.stringify({
- hooks: { Stop: [{ hooks: [{ command: 'node .agents/skills/impeccable/scripts/hook.mjs' }] }] },
- }));
-
- const res = spawnSync(process.execPath, [path.join(scripts, 'context.mjs')], {
- cwd: project,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0, res.stderr);
- assert.doesNotMatch(res.stdout, /MANUAL_DETECTOR_REQUIRED:/);
-
- fs.mkdirSync(path.join(project, '.impeccable'), { recursive: true });
- fs.writeFileSync(path.join(project, '.impeccable', 'config.json'), JSON.stringify({ hook: { enabled: false } }));
- const disabled = spawnSync(process.execPath, [path.join(scripts, 'context.mjs')], {
- cwd: project,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(disabled.status, 0, disabled.stderr);
- assert.match(disabled.stdout, /MANUAL_DETECTOR_REQUIRED:/);
- assert.match(disabled.stdout, /detect\.mjs --json /);
- });
-
- it('adds no detector directive when a per-edit-only hook is active', () => {
- const scripts = path.join(scratch, 'bundle', 'skills', 'impeccable', 'scripts');
- stageContextBundle(scripts, { providerId: 'cursor' });
-
- const project = path.join(scratch, 'project');
- fs.mkdirSync(path.join(project, '.cursor'), { recursive: true });
- fs.writeFileSync(path.join(project, 'PRODUCT.md'), '# Acme\n');
- fs.writeFileSync(path.join(project, '.cursor', 'hooks.json'), JSON.stringify({
- hooks: { preToolUse: [{ command: 'node .cursor/skills/impeccable/scripts/hook-before-edit.mjs' }] },
- }));
-
- const res = spawnSync(process.execPath, [path.join(scripts, 'context.mjs')], {
- cwd: project,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0, res.stderr);
- assert.doesNotMatch(res.stdout, /MANUAL_DETECTOR_REQUIRED:/);
- assert.doesNotMatch(res.stdout, /detect\.mjs --json /);
- });
-
- it('treats tokenized code as incumbent design authority when DESIGN.md is missing', () => {
- write('PRODUCT.md', '# Acme\n');
- write('src/app.css', ':root { --color-brand: red; --color-surface: white; --color-text: black; }\nbody { font-family: system-ui; background: var(--color-surface); color: var(--color-text); }\n');
- assert.equal(hasVisualImplementation(scratch), true);
- const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /INCUMBENT_WORLD_UNDOCUMENTED:/);
- assert.match(res.stdout, /For shape or a new-surface\/redesign request, load reference\/new-work\.md/);
- assert.doesNotMatch(res.stdout, /WORLD_DISCOVERY_REQUIRED:/);
- assert.match(res.stdout, /"hasVisualImplementation": true/);
- });
-
- it('does not mistake the empty Astro eval scaffold for an incumbent identity', () => {
- write('src/styles/global.css', '@import "tailwindcss";\n');
- write('src/pages/index.astro', `---\nimport "../styles/global.css";\n---\nEval Workspace\n`);
- assert.equal(hasVisualImplementation(scratch), false);
- });
-
- it('recognizes one substantive authored Astro surface', () => {
- write('src/pages/index.astro', `Field notes for the night shift
Specific authored content.
\n\n`);
- assert.equal(hasVisualImplementation(scratch), true);
- });
-
- it('does not let irrelevant or vendored files exhaust or satisfy the visual scan', () => {
- for (let i = 0; i < 300; i++) write(`src/data/item-${String(i).padStart(3, '0')}.txt`, 'not visual\n');
- write('public/vendor/framework.min.css', ':root { --a: 1; --b: 2; --c: 3; } body { color: red; background: blue; border-color: green; font-family: sans-serif; }\n');
- write('styles/z-theme.css', ':root { --brand: #124; --surface: #fff; --text: #111; } main { color: var(--text); background-color: var(--surface); border-color: var(--brand); }\n');
- assert.equal(hasVisualImplementation(scratch), true);
- });
-
- it('routes new surfaces through init but keeps narrow refinements non-blocking when visual code exists without PRODUCT.md', () => {
- write('styles/theme.css', ':root { --brand: #124; --surface: #fff; --text: #111; }\nmain { color: var(--text); background-color: var(--surface); border-color: var(--brand); }\n');
- const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /^NO_PRODUCT_MD:/);
- assert.match(res.stdout, /EXISTING_VISUAL_SYSTEM:/);
- assert.match(res.stdout, /BUILD_INIT_REQUIRED:/);
- assert.match(res.stdout, /SCOPED_EXISTING_ALLOWED:/);
- assert.match(res.stdout, /proceed without blocking/);
- assert.match(res.stdout, /For `init`, `teach`, `shape`, or any request to create a new surface/);
- assert.match(res.stdout, /For a redesign\/rebrand.*old look only as evidence and anti-reference/s);
- assert.doesNotMatch(res.stdout, /WORLD_DISCOVERY_REQUIRED:/);
- });
-
- it('prints DESIGN.md even when PRODUCT.md is missing', () => {
- // The skill resumes after init writes PRODUCT.md without rerunning this
- // script, so a DESIGN.md withheld from the no-PRODUCT.md branch is never
- // seen at all. Emit incumbent visual authority on both branches.
- write('DESIGN.md', '# Acme design\n\nUNIQUE_DESIGN_MARKER\n');
- const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /^NO_PRODUCT_MD:/);
- assert.match(res.stdout, /# DESIGN\.md\n\n# Acme design/);
- assert.match(res.stdout, /UNIQUE_DESIGN_MARKER/);
- });
-
- it('concatenates PRODUCT.md and DESIGN.md with a --- separator', async () => {
- write('PRODUCT.md', '# Acme product\n');
- write('DESIGN.md', '# Acme design\n');
- const { spawnSync } = await import('node:child_process');
- const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /^# PRODUCT\.md/);
- assert.match(res.stdout, /\n---\n/);
- assert.match(res.stdout, /# DESIGN\.md\n\n# Acme design/);
- assert.equal(res.stdout.includes('WORLD_DISCOVERY_REQUIRED:'), false);
- });
-
- it('loads the only persisted surface brief as current task context', () => {
- write('PRODUCT.md', '# Acme product\n');
- write('DESIGN.md', '# Acme design\n');
- write('.impeccable/surfaces/src-pages-pricing-astro.md', `---
-version: 1
-slug: "src-pages-pricing-astro"
-primary_target: "src/pages/pricing.astro"
-related_targets: []
----
-
-# Surface brief: Pricing
-
-## Product strategy
-Make plan tradeoffs legible before asking for a trial.
-`);
- const res = spawnSync(process.execPath, [SCRIPT_PATH], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /# SURFACE BRIEF \(\.impeccable\/surfaces\/src-pages-pricing-astro\.md\)/);
- assert.match(res.stdout, /Make plan tradeoffs legible/);
- assert.match(res.stdout, /"surfaceBriefReason": "only-brief"/);
- });
-
- it('selects a surface brief by an exact primary or related target', () => {
- write('PRODUCT.md', '# Acme product\n');
- write('DESIGN.md', '# Acme design\n');
- write('src/pages/pricing.astro', 'Pricing\n');
- write('.impeccable/surfaces/src-pages-pricing-astro.md', `---
-version: 1
-slug: "src-pages-pricing-astro"
-primary_target: "src/pages/pricing.astro"
-related_targets: ["src/components/PricingTable.astro"]
----
-
-# Surface brief: Pricing
-
-PRICING_STRATEGY_SENTINEL
-`);
- write('.impeccable/surfaces/src-pages-home-astro.md', `---
-version: 1
-slug: "src-pages-home-astro"
-primary_target: "src/pages/home.astro"
-related_targets: []
----
-
-# Surface brief: Home
-
-HOME_STRATEGY_SENTINEL
-`);
- const res = spawnSync(process.execPath, [SCRIPT_PATH, '--target', 'src/pages/pricing.astro'], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /PRICING_STRATEGY_SENTINEL/);
- assert.doesNotMatch(res.stdout, /HOME_STRATEGY_SENTINEL/);
- });
-
- it('lists candidates instead of guessing when several surface briefs exist', () => {
- write('PRODUCT.md', '# Acme product\n');
- write('DESIGN.md', '# Acme design\n');
- for (const [slug, target] of [
- ['src-pages-pricing-astro', 'src/pages/pricing.astro'],
- ['src-pages-home-astro', 'src/pages/home.astro'],
- ]) {
- write(`.impeccable/surfaces/${slug}.md`, `---
-version: 1
-slug: "${slug}"
-primary_target: "${target}"
-related_targets: []
----
-
-# Surface brief: ${slug}
-`);
- }
- const res = spawnSync(process.execPath, [SCRIPT_PATH], {
- cwd: scratch,
- encoding: 'utf8',
- env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1', IMPECCABLE_NO_STALENESS_CHECK: '1' },
- });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /SURFACE_CONTEXT_AVAILABLE:/);
- assert.match(res.stdout, /src\/pages\/pricing\.astro/);
- assert.match(res.stdout, /src\/pages\/home\.astro/);
- assert.doesNotMatch(res.stdout, /# SURFACE BRIEF \(/);
- });
-
- it('reads from a fallback dir when cwd is clean', async () => {
- write('.agents/context/PRODUCT.md', '# fallback product\n');
- const { spawnSync } = await import('node:child_process');
- const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /^# PRODUCT\.md/);
- assert.match(res.stdout, /# fallback product/);
- });
-
- it('ignores a legacy Register field because visitor mode is task-scoped', async () => {
- write('PRODUCT.md', '# Acme\n\n## Register\n\nbrand\n');
- const { spawnSync } = await import('node:child_process');
- const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
- assert.equal(res.status, 0);
- assert.doesNotMatch(res.stdout, /REGISTER:/);
- assert.match(res.stdout, /WORLD_DISCOVERY_REQUIRED: PRODUCT\.md exists but no DESIGN\.md/);
- });
-
- it('loads the native platform reference for an ios project', async () => {
- write('PRODUCT.md', '# Acme\n\n## Platform\n\nios\n');
- const { spawnSync } = await import('node:child_process');
- const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /# NATIVE PLATFORM REFERENCE: IOS \(reference\/ios\.md\)/);
- assert.match(res.stdout, /Apple Human Interface Guidelines|iOS/i);
- assert.doesNotMatch(res.stdout, /NEXT STEP:.*reference\/ios\.md/);
- });
-
- it('loads both native platform references for an adaptive project', async () => {
- write('PRODUCT.md', '# Acme\n\n## Platform\n\nadaptive\n');
- const { spawnSync } = await import('node:child_process');
- const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /# NATIVE PLATFORM REFERENCE: IOS \(reference\/ios\.md\)/);
- assert.match(res.stdout, /# NATIVE PLATFORM REFERENCE: ANDROID \(reference\/android\.md\)/);
- });
-
- it('appends no native platform directive for a web project', async () => {
- write('PRODUCT.md', '# Acme\n\n## Platform\n\nweb\n');
- const { spawnSync } = await import('node:child_process');
- const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
- assert.equal(res.status, 0);
- assert.equal(res.stdout.includes('This project targets'), false);
- assert.equal(res.stdout.includes('reference/ios.md'), false);
- });
-
- it('loads the native platform reference for an android project', async () => {
- write('PRODUCT.md', '# Acme\n\n## Platform\n\nandroid\n');
- const { spawnSync } = await import('node:child_process');
- const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /# NATIVE PLATFORM REFERENCE: ANDROID \(reference\/android\.md\)/);
- assert.match(res.stdout, /Material Design|Android/i);
- });
-
- it('warns on an unrecognized platform value instead of silently defaulting to web', async () => {
- // The likeliest misconfiguration is a toolchain name where the target
- // belongs. Silent fallback to web would give web guidance to the exact
- // projects that tried to declare themselves native.
- write('PRODUCT.md', '# Acme\n\n## Platform\n\nflutter\n');
- const { spawnSync } = await import('node:child_process');
- const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /WARNING: PRODUCT\.md's `## Platform` value `flutter` is not recognized/);
- assert.match(res.stdout, /treating the project as `web`/);
- assert.equal(res.stdout.includes('This project targets'), false);
- });
-
- it('emits no warning for an empty Platform section', async () => {
- write('PRODUCT.md', '# Acme\n\n## Platform\n\n## Users\n\nAnglers.\n');
- const { spawnSync } = await import('node:child_process');
- const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } });
- assert.equal(res.status, 0);
- assert.equal(res.stdout.includes('WARNING: PRODUCT.md'), false);
- assert.equal(res.stdout.includes('This project targets'), false);
- });
-});
-
-describe('context.mjs update check', () => {
- // The script reads its own version from a sibling SKILL.md (resolved via
- // import.meta.url, not cwd). The source tree has no SKILL.md, so we copy the
- // script into a scratch skill dir with a controlled version and run that.
- // Local version is pinned to 1.0.0; "newer" = 2.0.0, "older" = 0.0.1.
- const LOCAL_VERSION = '1.0.0';
-
- const cachePath = () => path.join(scratch, 'update-check.json');
-
- function setup(cacheObj, { disable = false, host } = {}) {
- const skillScript = stageContextBundle(path.join(scratch, 'skill', 'scripts'));
- fs.writeFileSync(
- path.join(scratch, 'skill', 'SKILL.md'),
- `---\nname: impeccable\nversion: ${LOCAL_VERSION}\n---\n\nbody\n`,
- );
- fs.writeFileSync(cachePath(), JSON.stringify(cacheObj));
- const project = path.join(scratch, 'project');
- fs.mkdirSync(project, { recursive: true });
- fs.writeFileSync(path.join(project, 'PRODUCT.md'), '# Acme\n');
- const env = {
- ...process.env,
- IMPECCABLE_UPDATE_CACHE: cachePath(),
- IMPECCABLE_NO_UPDATE_CHECK: disable ? '1' : '',
- // This suite asserts on the update directive alone. Staleness findings
- // are a separate directive with their own tests, and leaving the check on
- // would also write notice state into the developer's home dir.
- IMPECCABLE_NO_STALENESS_CHECK: '1',
- ...(host ? { IMPECCABLE_UPDATE_HOST: host } : {}),
- };
- return { skillScript, project, env };
- }
-
- // A fresh cache (lastCheck = now) skips the network poll, so cache-driven
- // tests stay synchronous and hermetic.
- function run(cacheObj, opts) {
- const { skillScript, project, env } = setup(cacheObj, opts);
- return spawnSync(process.execPath, [skillScript], { cwd: project, encoding: 'utf8', env });
- }
-
- // Async variant for the live-fetch tests: the stub server runs in THIS
- // process, so the runner must not block the event loop (spawnSync would
- // deadlock the loopback connection). spawn keeps the loop serving.
- function runAsync(cacheObj, opts) {
- const { skillScript, project, env } = setup(cacheObj, opts);
- return new Promise((resolve) => {
- const proc = spawn(process.execPath, [skillScript], { cwd: project, env });
- let stdout = '';
- proc.stdout.on('data', (d) => (stdout += d.toString()));
- proc.on('exit', (status) => resolve({ status, stdout }));
- });
- }
-
- function readCache() {
- return JSON.parse(fs.readFileSync(cachePath(), 'utf8'));
- }
-
- it('appends UPDATE_AVAILABLE when the cached latest version is newer', () => {
- const res = run({ lastCheck: Date.now(), latestVersion: '2.0.0' });
- assert.equal(res.status, 0);
- assert.match(res.stdout, /UPDATE_AVAILABLE: A newer Impeccable skill is available/);
- assert.match(res.stdout, /installed v1\.0\.0, latest v2\.0\.0/);
- assert.match(res.stdout, /npx impeccable update/);
- // It must come after the real context, never replace it.
- assert.match(res.stdout, /^# PRODUCT\.md/);
- });
-
- // The directive used to say "ask once" and "if they agree, run it" while also
- // saying to continue without waiting. Nothing gated the run on an answer that
- // could not arrive, so the command read as the next step. It now forbids
- // running in this turn outright, whatever the answer.
- it('forbids running the update in the same turn, on any answer', () => {
- const { stdout } = run({ lastCheck: Date.now(), latestVersion: '2.0.0' });
- assert.match(stdout, /Do not run `npx impeccable update` in this turn, whatever the user answers/);
- assert.match(stdout, /only after the user has asked for it in their own words/);
- // The conditional that made the command look reachable must be gone.
- assert.equal(/If they agree, run/.test(stdout), false);
- });
-
- it('stays silent when the cached latest version is not newer', () => {
- const res = run({ lastCheck: Date.now(), latestVersion: '0.0.1' });
- assert.equal(res.status, 0);
- assert.equal(res.stdout.includes('UPDATE_AVAILABLE'), false);
- });
-
- it('does not re-surface a version notified within the last week', () => {
- const res = run({
- lastCheck: Date.now(),
- latestVersion: '2.0.0',
- notifiedVersion: '2.0.0',
- notifiedAt: Date.now(),
- });
- assert.equal(res.status, 0);
- assert.equal(res.stdout.includes('UPDATE_AVAILABLE'), false);
- });
-
- it('respects IMPECCABLE_NO_UPDATE_CHECK', () => {
- const res = run({ lastCheck: Date.now(), latestVersion: '2.0.0' }, { disable: true });
- assert.equal(res.status, 0);
- assert.equal(res.stdout.includes('UPDATE_AVAILABLE'), false);
- });
-
- // ─── live fetch path (against a localhost stub, never the real site) ──────
- function startStub(body, { status = 200 } = {}) {
- return new Promise((resolve) => {
- const srv = http.createServer((req, res) => {
- res.statusCode = status;
- res.setHeader('content-type', 'application/json');
- res.end(typeof body === 'string' ? body : JSON.stringify(body));
- });
- srv.listen(0, '127.0.0.1', () => resolve({ srv, host: `http://127.0.0.1:${srv.address().port}` }));
- });
- }
-
- it('polls /api/version over the network and caches a newer version', async () => {
- const { srv, host } = await startStub({ skills: '2.0.0' });
- try {
- const res = await runAsync({}, { host }); // empty cache forces the poll
- assert.equal(res.status, 0);
- assert.match(res.stdout, /UPDATE_AVAILABLE/);
- assert.match(res.stdout, /installed v1\.0\.0, latest v2\.0\.0/);
- const cache = readCache();
- assert.equal(cache.latestVersion, '2.0.0');
- assert.equal(typeof cache.lastCheck, 'number');
- } finally {
- srv.close();
- }
- });
-
- it('stays silent when the network reports a same-or-older version', async () => {
- const { srv, host } = await startStub({ skills: '1.0.0' });
- try {
- const res = await runAsync({}, { host });
- assert.equal(res.status, 0);
- assert.equal(res.stdout.includes('UPDATE_AVAILABLE'), false);
- // The poll still happened, so lastCheck is stamped to throttle the next.
- assert.equal(typeof readCache().lastCheck, 'number');
- } finally {
- srv.close();
- }
- });
-
- it('fails silent and stamps lastCheck when the endpoint is unreachable', async () => {
- // Bind then immediately close to obtain a port nothing is listening on.
- const { srv, host } = await startStub({ skills: '2.0.0' });
- await new Promise((r) => srv.close(r));
- const res = run({}, { host });
- assert.equal(res.status, 0);
- assert.equal(res.stdout.includes('UPDATE_AVAILABLE'), false);
- assert.match(res.stdout, /^# PRODUCT\.md/); // core output is unaffected
- const cache = readCache();
- assert.equal(typeof cache.lastCheck, 'number'); // stamped so we don't re-poll every boot
- assert.equal(cache.latestVersion, undefined); // nothing learned
- });
-
- // Targeted live-fetch boot: the Windows abort in issue #573 fired after
- // stdout was already complete, so the contract is exit 0 with the full
- // context still on stdout.
- it('exits 0 after a targeted live-fetch boot writes full context', async () => {
- const { srv, host } = await startStub({ skills: '2.0.0' });
- try {
- const { skillScript, project, env } = setup({}, { host });
- fs.writeFileSync(
- path.join(project, 'package.json'),
- JSON.stringify({ private: true, workspaces: ['packages/*'] }),
- );
- const jervPi = path.join(project, 'packages', 'jerv-pi');
- fs.mkdirSync(jervPi, { recursive: true });
- fs.writeFileSync(path.join(jervPi, 'PRODUCT.md'), '# Jerv Pi product\n');
-
- const result = await new Promise((resolveRun, rejectRun) => {
- const child = spawn(process.execPath, [skillScript, '--target', 'packages/jerv-pi'], {
- cwd: project,
- env,
- });
- let stdout = '';
- child.stdout.on('data', (chunk) => { stdout += chunk; });
- child.on('error', rejectRun);
- child.on('close', (status) => resolveRun({ status, stdout }));
- });
-
- assert.equal(result.status, 0);
- assert.match(result.stdout, /RESOLVED_CONTEXT:/);
- assert.match(result.stdout, /# Jerv Pi product/);
- assert.match(result.stdout, /UPDATE_AVAILABLE/);
- } finally {
- srv.close();
- }
- });
-});
diff --git a/tests/critique-storage.test.mjs b/tests/critique-storage.test.mjs
deleted file mode 100644
index 38c49b6f4..000000000
--- a/tests/critique-storage.test.mjs
+++ /dev/null
@@ -1,881 +0,0 @@
-/**
- * Tests for critique snapshot persistence.
- * Run with: node --test tests/critique-storage.test.mjs
- */
-
-import { describe, it, beforeEach, afterEach } from 'node:test';
-import assert from 'node:assert/strict';
-import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
-import { basename, join } from 'node:path';
-import { tmpdir } from 'node:os';
-import { spawnSync } from 'node:child_process';
-import { fileURLToPath } from 'node:url';
-
-const SCRIPT = fileURLToPath(new URL('../skill/scripts/critique-storage.mjs', import.meta.url));
-
-import {
- fingerprintTarget,
- slugFromTarget,
- writeSnapshot,
- readLatestSnapshot,
- readLatestSnapshotAcrossTargets,
- readTrend,
- closeSnapshot,
- nowFilenameStamp,
-} from '../skill/scripts/critique-storage.mjs';
-
-let cwd;
-beforeEach(() => { cwd = mkdtempSync(join(tmpdir(), 'imp-critique-')); });
-afterEach(() => { rmSync(cwd, { recursive: true, force: true }); });
-
-describe('slugFromTarget', () => {
- it('kebabs a relative file path', () => {
- assert.equal(slugFromTarget('site/pages/index.astro', { cwd }), 'site-pages-index-astro');
- });
-
- it('kebabs an absolute path inside cwd by relativizing', () => {
- const abs = join(cwd, 'site/pages/index.astro');
- assert.equal(slugFromTarget(abs, { cwd }), 'site-pages-index-astro');
- });
-
- it('uses basename for absolute paths outside cwd', () => {
- // Sibling path, not under cwd
- const abs = join(tmpdir(), 'somewhere', 'else', 'page.html');
- assert.equal(slugFromTarget(abs, { cwd }), 'page-html');
- });
-
- it('drops port from URL', () => {
- assert.equal(slugFromTarget('http://localhost:3000/pricing', { cwd }), 'localhost-pricing');
- });
-
- it('normalizes URL casing and trailing slash', () => {
- assert.equal(
- slugFromTarget('https://Impeccable.Style/docs/audit/', { cwd }),
- 'impeccable-style-docs-audit',
- );
- });
-
- it('strips query strings', () => {
- assert.equal(
- slugFromTarget('https://example.com/x?utm=1&foo=bar', { cwd }),
- 'example-com-x',
- );
- });
-
- it('returns null for empty / project-root inputs', () => {
- assert.equal(slugFromTarget('', { cwd }), null);
- assert.equal(slugFromTarget('.', { cwd }), null);
- assert.equal(slugFromTarget(null, { cwd }), null);
- });
-
- it('caps overly long slugs from the tail', () => {
- const longPath = 'a/'.repeat(60) + 'file.tsx'; // way over 50
- const slug = slugFromTarget(longPath, { cwd });
- assert.ok(slug.length <= 50);
- assert.ok(slug.endsWith('file-tsx'));
- });
-
- it('is stable: same input → same slug', () => {
- const a = slugFromTarget('site/pages/index.astro', { cwd });
- const b = slugFromTarget('site/pages/index.astro', { cwd });
- assert.equal(a, b);
- });
-});
-
-describe('nowFilenameStamp', () => {
- it('is windows-safe (no colons or dots in the time fragment)', () => {
- const stamp = nowFilenameStamp(new Date('2026-05-12T18:30:00.123Z'));
- assert.equal(stamp, '2026-05-12T18-30-00Z');
- });
-});
-
-describe('fingerprintTarget', () => {
- it('fingerprints exact local file bytes independent of Git state', () => {
- const target = join(cwd, 'index.html');
- writeFileSync(target, 'hello');
- const first = fingerprintTarget(target, { cwd });
- assert.match(first, /^sha256:[a-f0-9]{64}$/);
- assert.equal(fingerprintTarget('index.html', { cwd }), first);
-
- writeFileSync(target, 'changed');
- assert.notEqual(fingerprintTarget(target, { cwd }), first);
- });
-
- it('returns null for URLs, directories, and missing files', () => {
- assert.equal(fingerprintTarget('https://example.com/page', { cwd }), null);
- assert.equal(fingerprintTarget('.', { cwd }), null);
- assert.equal(fingerprintTarget('missing.html', { cwd }), null);
- });
-});
-
-describe('writeSnapshot + readLatestSnapshot', () => {
- it('round-trips body and frontmatter', () => {
- const out = writeSnapshot({
- slug: 'index-astro',
- meta: { target: 'the homepage', total_score: 28, p0_count: 1, p1_count: 3 },
- body: '# Critique\n\nP0: nested cards',
- cwd,
- });
- assert.ok(out.endsWith('__index-astro.md'));
- const latest = readLatestSnapshot('index-astro', { cwd });
- assert.equal(latest.meta.slug, 'index-astro');
- assert.equal(latest.meta.target, 'the homepage');
- assert.equal(latest.meta.total_score, 28);
- assert.match(latest.body, /P0: nested cards/);
- });
-
- it('returns null when no snapshot for slug', () => {
- assert.equal(readLatestSnapshot('nope', { cwd }), null);
- });
-
- it('picks the newest by filename when multiple exist', () => {
- writeSnapshot({ slug: 'index-astro', meta: { total_score: 22 }, body: 'old', cwd, now: new Date('2026-05-01T00:00:00Z') });
- writeSnapshot({ slug: 'index-astro', meta: { total_score: 30 }, body: 'new', cwd, now: new Date('2026-05-12T00:00:00Z') });
- const latest = readLatestSnapshot('index-astro', { cwd });
- assert.equal(latest.meta.total_score, 30);
- assert.match(latest.body, /new/);
- });
-
- it('preserves same-second snapshots with a sortable collision suffix', () => {
- const now = new Date('2026-05-12T18:30:00Z');
- const first = writeSnapshot({
- slug: 'index-astro',
- meta: { total_score: 20 },
- body: 'first',
- cwd,
- now,
- });
- const second = writeSnapshot({
- slug: 'index-astro',
- meta: { total_score: 30 },
- body: 'second',
- cwd,
- now,
- });
-
- assert.notEqual(second, first);
- assert.ok(first.endsWith('2026-05-12T18-30-00Z__index-astro.md'));
- assert.ok(second.endsWith('2026-05-12T18-30-00Z~0001__index-astro.md'));
- assert.match(readLatestSnapshot('index-astro', { cwd }).body, /second/);
- assert.deepEqual(
- readTrend('index-astro', { cwd }).map((entry) => entry.total_score),
- [20, 30],
- );
- });
-
- it('picks the newest snapshot across target slugs', () => {
- writeSnapshot({ slug: 'home', meta: {}, body: 'old', cwd, now: new Date('2026-05-01T00:00:00Z') });
- writeSnapshot({ slug: 'pricing', meta: {}, body: 'new', cwd, now: new Date('2026-05-12T00:00:00Z') });
- writeFileSync(join(cwd, '.impeccable', 'critique', 'ignore.md'), '# Critique ignores\n');
- writeFileSync(join(cwd, '.impeccable', 'critique', '9999-not-a-snapshot.md'), '# Draft\n');
- const latest = readLatestSnapshotAcrossTargets({ cwd });
- assert.equal(latest.meta.slug, 'pricing');
- assert.match(latest.body, /new/);
- });
-
- it('does not see snapshots for a different slug', () => {
- writeSnapshot({ slug: 'pricing-astro', meta: { total_score: 10 }, body: 'b', cwd });
- assert.equal(readLatestSnapshot('index-astro', { cwd }), null);
- });
-
- it('caller-supplied meta cannot override computed timestamp or slug', () => {
- // Defends against a corrupt IMPECCABLE_CRITIQUE_META blob (parsed from
- // an env var) silently rewriting fields that must agree with the
- // filename. Otherwise readTrend would attribute scores to the wrong
- // timestamps with no error.
- const out = writeSnapshot({
- slug: 'index-astro',
- meta: { timestamp: 'NOT_A_REAL_STAMP', slug: 'somewhere-else', total_score: 50 },
- body: 'b',
- cwd,
- now: new Date('2026-05-12T18:30:00Z'),
- });
- const latest = readLatestSnapshot('index-astro', { cwd });
- assert.equal(latest.meta.slug, 'index-astro');
- assert.equal(latest.meta.timestamp, '2026-05-12T18-30-00Z');
- // The legit meta field still lands.
- assert.equal(latest.meta.total_score, 50);
- // The filename matches the computed slug.
- assert.ok(out.endsWith('2026-05-12T18-30-00Z__index-astro.md'));
- });
-
- it('quotes values containing : or # to keep parsing simple', () => {
- writeSnapshot({
- slug: 'x',
- meta: { target: 'docs: critique # main' },
- body: '...',
- cwd,
- });
- const latest = readLatestSnapshot('x', { cwd });
- assert.equal(latest.meta.target, 'docs: critique # main');
- });
-
- it('closeSnapshot returns the path and leaves readLatestSnapshot null', () => {
- const out = writeSnapshot({ slug: 'index-astro', meta: { total_score: 20 }, body: 'open', cwd });
- const closed = closeSnapshot(out, { cwd });
- assert.equal(closed, out);
- assert.ok(closed.endsWith('__index-astro.md'));
- assert.equal(readLatestSnapshot('index-astro', { cwd }), null);
- });
-
- it('closeSnapshot closes the backlog without deleting its trend history', () => {
- writeSnapshot({
- slug: 'index-astro',
- meta: { total_score: 21, p0_count: 7 },
- body: 'old leftover',
- cwd,
- now: new Date('2026-05-01T00:00:00Z'),
- });
- const newest = writeSnapshot({
- slug: 'index-astro',
- meta: { total_score: 30 },
- body: 'newer',
- cwd,
- now: new Date('2026-05-12T00:00:00Z'),
- });
- const closed = closeSnapshot(newest, { cwd });
- assert.equal(closed, newest);
- assert.equal(readLatestSnapshot('index-astro', { cwd }), null);
- const trend = readTrend('index-astro', { cwd });
- assert.equal(trend.length, 2);
- assert.equal(trend[0].total_score, 21);
- assert.equal(trend[1].total_score, 30);
- assert.equal(trend[1].closed, true);
- });
-
- it('a new snapshot reopens a previously closed slug', () => {
- const resolved = writeSnapshot({
- slug: 'index-astro',
- meta: { total_score: 20 },
- body: 'resolved',
- cwd,
- now: new Date('2026-05-01T00:00:00Z'),
- });
- closeSnapshot(resolved, { cwd });
- const reopened = writeSnapshot({
- slug: 'index-astro',
- meta: { total_score: 15 },
- body: 'new findings',
- cwd,
- now: new Date('2026-05-12T00:00:00Z'),
- });
-
- assert.equal(readLatestSnapshot('index-astro', { cwd }).path, reopened);
- assert.equal(readTrend('index-astro', { cwd }).length, 2);
- });
-
- it('latest across targets skips a closed slug without hiding other backlogs', () => {
- const pricing = writeSnapshot({
- slug: 'pricing',
- meta: { total_score: 25 },
- body: 'pricing backlog',
- cwd,
- now: new Date('2026-05-01T00:00:00Z'),
- });
- const home = writeSnapshot({
- slug: 'home',
- meta: { total_score: 30 },
- body: 'home backlog',
- cwd,
- now: new Date('2026-05-12T00:00:00Z'),
- });
- closeSnapshot(home, { cwd });
-
- assert.equal(readLatestSnapshotAcrossTargets({ cwd }).path, pricing);
- });
-
- it('latest across targets keeps colliding target identities independent', () => {
- const original = writeSnapshot({
- slug: 'foo-bar',
- meta: {
- target_identity: `file:${join(cwd, 'foo', 'bar')}`,
- total_score: 20,
- },
- body: 'older original backlog',
- cwd,
- now: new Date('2026-05-01T00:00:00Z'),
- });
- const colliding = writeSnapshot({
- slug: 'foo-bar',
- meta: {
- target_identity: `file:${join(cwd, 'foo-bar')}`,
- total_score: 30,
- },
- body: 'newer colliding backlog',
- cwd,
- now: new Date('2026-05-12T00:00:00Z'),
- });
- closeSnapshot(colliding, { cwd });
-
- assert.equal(readLatestSnapshotAcrossTargets({ cwd }).path, original);
- });
-
- it('latest across targets does not resurrect legacy work after identity migration', () => {
- writeSnapshot({
- slug: 'index-html',
- meta: { total_score: 20 },
- body: 'legacy backlog',
- cwd,
- now: new Date('2026-05-01T00:00:00Z'),
- });
- const modern = writeSnapshot({
- slug: 'index-html',
- meta: {
- target_identity: `file:${join(cwd, 'index.html')}`,
- total_score: 30,
- },
- body: 'modern backlog',
- cwd,
- now: new Date('2026-05-12T00:00:00Z'),
- });
- closeSnapshot(modern, { cwd });
-
- assert.equal(readLatestSnapshotAcrossTargets({ cwd }), null);
- });
-});
-
-describe('CLI entry point', () => {
- // Why a subprocess test: the CLI guard at the bottom of the script
- // previously compared import.meta.url to `file://${process.argv[1]}`,
- // which silently broke on Windows (forward vs back slashes) — exit 0,
- // no output, save skipped. The exported functions kept passing because
- // tests never spawned the script as a process. See issue #155.
- it('slug subcommand prints a slug and exits 0', () => {
- const r = spawnSync(process.execPath, [SCRIPT, 'slug', 'site/pages/index.astro'], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(r.status, 0, `stderr: ${r.stderr}`);
- assert.equal(r.stdout.trim(), 'site-pages-index-astro');
- });
-
- it('slug subcommand exits 1 with a message for empty input', () => {
- const r = spawnSync(process.execPath, [SCRIPT, 'slug', ''], { cwd, encoding: 'utf-8' });
- assert.equal(r.status, 1);
- assert.match(r.stderr, /no stable slug/);
- });
-
- it('runs when invoked through a symlinked harness path', () => {
- const linkedScript = join(cwd, 'linked-critique-storage.mjs');
- symlinkSync(SCRIPT, linkedScript);
-
- const r = spawnSync(process.execPath, [linkedScript, 'slug', 'index.html'], {
- cwd,
- encoding: 'utf-8',
- });
-
- assert.equal(r.status, 0, `stderr: ${r.stderr}`);
- assert.equal(r.stdout.trim(), 'index-html');
- });
-
- it('latest subcommand exits 2 when no snapshot exists', () => {
- const r = spawnSync(process.execPath, [SCRIPT, 'latest', 'never-written'], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(r.status, 2);
- });
-
- it('inherits an unchanged untracked file snapshot and closes it after any byte change', () => {
- const target = join(cwd, 'index.html');
- const bodyFile = join(cwd, 'critique.md');
- writeFileSync(target, 'assessed worktree');
- writeFileSync(bodyFile, '# Critique\n\nP1: improve hierarchy');
-
- const write = spawnSync(process.execPath, [SCRIPT, 'write', target, bodyFile], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(write.status, 0, `stderr: ${write.stderr}`);
- const written = readLatestSnapshot('index-html', { cwd });
- assert.equal(written.meta.target_path, target);
- assert.equal(written.meta.target_identity, `file:${target}`);
- assert.match(written.meta.target_fingerprint, /^sha256:[a-f0-9]{64}$/);
-
- const unchanged = spawnSync(process.execPath, [SCRIPT, 'latest', target], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(unchanged.status, 0, `stderr: ${unchanged.stderr}`);
- assert.match(unchanged.stdout, /improve hierarchy/);
-
- // The edit can happen in the same clock second as the snapshot; exact
- // bytes, rather than timestamp precision, determine freshness.
- writeFileSync(target, 'newer worktree');
- const changed = spawnSync(process.execPath, [SCRIPT, 'latest', target], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(changed.status, 2, `stderr: ${changed.stderr}`);
- assert.equal(readLatestSnapshot('index-html', { cwd }), null);
- assert.equal(readTrend('index-html', { cwd })[0].closed, true);
- });
-
- it('fingerprints extensionless local targets instead of mistaking them for slugs', () => {
- const target = join(cwd, 'main');
- const bodyFile = join(cwd, 'critique.md');
- writeFileSync(target, 'assessed');
- writeFileSync(bodyFile, '# Critique\n\nP1: improve hierarchy');
-
- const write = spawnSync(process.execPath, [SCRIPT, 'write', target, bodyFile], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(write.status, 0, `stderr: ${write.stderr}`);
-
- writeFileSync(target, 'changed');
- const changed = spawnSync(process.execPath, [SCRIPT, 'latest', target], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(changed.status, 2, `stderr: ${changed.stderr}`);
- assert.equal(readLatestSnapshot('main', { cwd }), null);
-
- const deletedTarget = join(cwd, 'shell');
- writeFileSync(deletedTarget, '#!/bin/sh\n');
- const deletedWrite = spawnSync(process.execPath, [SCRIPT, 'write', deletedTarget, bodyFile], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(deletedWrite.status, 0, `stderr: ${deletedWrite.stderr}`);
- rmSync(deletedTarget);
- const deleted = spawnSync(process.execPath, [SCRIPT, 'latest', deletedTarget], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(deleted.status, 2, `stderr: ${deleted.stderr}`);
- assert.equal(readLatestSnapshot('shell', { cwd }), null);
- });
-
- it('rejects a concrete target that collides with another target slug', () => {
- const originalDir = join(cwd, 'foo');
- const originalTarget = join('foo', 'bar');
- const originalPath = join(cwd, originalTarget);
- const ambiguousTarget = join(cwd, 'foo-bar');
- const bodyFile = join(cwd, 'critique.md');
- mkdirSync(originalDir);
- writeFileSync(originalPath, 'assessed original');
- writeFileSync(bodyFile, '# Critique\n\nP1: preserve this backlog');
-
- const write = spawnSync(process.execPath, [SCRIPT, 'write', originalTarget, bodyFile], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(write.status, 0, `stderr: ${write.stderr}`);
-
- // This distinct extensionless file shares the original target's slug.
- // The bare value is ambiguous while that file exists, and an explicit
- // local path is a known identity mismatch. Neither may inherit or close
- // the original snapshot.
- writeFileSync(ambiguousTarget, 'different target');
- const ambiguous = spawnSync(process.execPath, [SCRIPT, 'latest', 'foo-bar'], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(ambiguous.status, 2, `stderr: ${ambiguous.stderr}`);
- assert.match(ambiguous.stderr, /ambiguous snapshot slug/);
- const explicitOther = spawnSync(process.execPath, [SCRIPT, 'latest', './foo-bar'], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(explicitOther.status, 2, `stderr: ${explicitOther.stderr}`);
- assert.notEqual(readLatestSnapshot('foo-bar', { cwd }), null);
-
- // Once the local name collision is gone, the same bare value is an
- // intentional slug lookup and can return the original backlog.
- rmSync(ambiguousTarget);
- const bySlug = spawnSync(process.execPath, [SCRIPT, 'latest', 'foo-bar'], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(bySlug.status, 0, `stderr: ${bySlug.stderr}`);
- assert.match(bySlug.stdout, /preserve this backlog/);
-
- // The recorded original path still owns freshness invalidation.
- writeFileSync(originalPath, 'changed original');
- const changedOriginal = spawnSync(process.execPath, [SCRIPT, 'latest', originalTarget], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(changedOriginal.status, 2, `stderr: ${changedOriginal.stderr}`);
- assert.equal(readLatestSnapshot('foo-bar', { cwd }), null);
- });
-
- it('finds the exact target backlog when two live snapshots share a slug', () => {
- const originalDir = join(cwd, 'foo');
- const originalTarget = join('foo', 'bar');
- const otherTarget = join(cwd, 'foo-bar');
- const bodyFile = join(cwd, 'critique.md');
- mkdirSync(originalDir);
- writeFileSync(join(cwd, originalTarget), 'original');
- writeFileSync(otherTarget, 'other');
-
- writeFileSync(bodyFile, '# Critique\n\nP1: original backlog');
- const originalWrite = spawnSync(
- process.execPath,
- [SCRIPT, 'write', originalTarget, bodyFile],
- { cwd, encoding: 'utf-8' },
- );
- assert.equal(originalWrite.status, 0, `stderr: ${originalWrite.stderr}`);
-
- writeFileSync(bodyFile, '# Critique\n\nP1: newer other backlog');
- const otherWrite = spawnSync(
- process.execPath,
- [SCRIPT, 'write', './foo-bar', bodyFile],
- { cwd, encoding: 'utf-8' },
- );
- assert.equal(otherWrite.status, 0, `stderr: ${otherWrite.stderr}`);
-
- const originalLatest = spawnSync(
- process.execPath,
- [SCRIPT, 'latest', originalTarget, '--json'],
- { cwd, encoding: 'utf-8' },
- );
- assert.equal(originalLatest.status, 0, `stderr: ${originalLatest.stderr}`);
- const originalResult = JSON.parse(originalLatest.stdout);
- assert.match(originalResult.body, /original backlog/);
- assert.doesNotMatch(originalResult.body, /newer other backlog/);
-
- const otherLatest = spawnSync(
- process.execPath,
- [SCRIPT, 'latest', './foo-bar', '--json'],
- { cwd, encoding: 'utf-8' },
- );
- assert.equal(otherLatest.status, 0, `stderr: ${otherLatest.stderr}`);
- const otherResult = JSON.parse(otherLatest.stdout);
- assert.match(otherResult.body, /newer other backlog/);
- assert.notEqual(otherResult.snapshot_file, originalResult.snapshot_file);
-
- const closeOriginal = spawnSync(process.execPath, [
- SCRIPT,
- 'close',
- originalTarget,
- originalResult.snapshot_file,
- ], { cwd, encoding: 'utf-8' });
- assert.equal(closeOriginal.status, 0, `stderr: ${closeOriginal.stderr}`);
-
- const closedOriginal = spawnSync(
- process.execPath,
- [SCRIPT, 'latest', originalTarget],
- { cwd, encoding: 'utf-8' },
- );
- assert.equal(closedOriginal.status, 2, `stderr: ${closedOriginal.stderr}`);
- const stillOpenOther = spawnSync(
- process.execPath,
- [SCRIPT, 'latest', './foo-bar'],
- { cwd, encoding: 'utf-8' },
- );
- assert.equal(stillOpenOther.status, 0, `stderr: ${stillOpenOther.stderr}`);
- assert.match(stillOpenOther.stdout, /newer other backlog/);
- });
-
- it('closes a local snapshot when its target is deleted or replaced by a directory', () => {
- const bodyFile = join(cwd, 'critique.md');
- writeFileSync(bodyFile, '# Critique\n\nP1: improve hierarchy');
-
- for (const replacement of ['missing', 'directory']) {
- const target = join(cwd, `${replacement}.html`);
- writeFileSync(target, 'assessed');
- const write = spawnSync(process.execPath, [SCRIPT, 'write', target, bodyFile], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(write.status, 0, `stderr: ${write.stderr}`);
-
- rmSync(target);
- if (replacement === 'directory') mkdirSync(target);
-
- const latest = spawnSync(process.execPath, [SCRIPT, 'latest', target], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(latest.status, 2, `stderr: ${latest.stderr}`);
- const slug = `${replacement}-html`;
- assert.equal(readLatestSnapshot(slug, { cwd }), null);
- assert.equal(readTrend(slug, { cwd })[0].closed, true);
- }
- });
-
- it('treats a legacy local-file snapshot without a fingerprint as stale', () => {
- const target = join(cwd, 'index.html');
- writeFileSync(target, 'current');
- writeSnapshot({ slug: 'index-html', meta: { total_score: 20 }, body: 'legacy', cwd });
-
- const latest = spawnSync(process.execPath, [SCRIPT, 'latest', target], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(latest.status, 2, `stderr: ${latest.stderr}`);
- assert.equal(readTrend('index-html', { cwd })[0].closed, true);
- });
-
- it('rejects an ambiguous legacy extensionless lookup until the path is explicit', () => {
- const target = join(cwd, 'main');
- writeFileSync(target, 'changed since legacy critique');
- writeSnapshot({ slug: 'main', meta: { total_score: 20 }, body: 'legacy stale', cwd });
-
- const ambiguous = spawnSync(process.execPath, [SCRIPT, 'latest', 'main'], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(ambiguous.status, 2, `stderr: ${ambiguous.stderr}`);
- assert.match(ambiguous.stderr, /ambiguous legacy snapshot target/);
- assert.notEqual(readLatestSnapshot('main', { cwd }), null);
-
- const explicit = spawnSync(process.execPath, [SCRIPT, 'latest', './main'], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(explicit.status, 2, `stderr: ${explicit.stderr}`);
- assert.equal(readLatestSnapshot('main', { cwd }), null);
- assert.equal(readTrend('main', { cwd })[0].closed, true);
- });
-
- it('keeps URL snapshots current without a local fingerprint', () => {
- const bodyFile = join(cwd, 'critique.md');
- writeFileSync(bodyFile, '# Critique\n\nP1: improve hierarchy');
- const target = 'https://example.com/page';
-
- const write = spawnSync(process.execPath, [SCRIPT, 'write', target, bodyFile], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(write.status, 0, `stderr: ${write.stderr}`);
- assert.equal(
- readLatestSnapshot('example-com-page', { cwd }).meta.target_identity,
- 'url:https://example.com/page',
- );
-
- const latest = spawnSync(process.execPath, [SCRIPT, 'latest', target], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(latest.status, 0, `stderr: ${latest.stderr}`);
- assert.match(latest.stdout, /improve hierarchy/);
-
- const bySlug = spawnSync(process.execPath, [SCRIPT, 'latest', 'example-com-page'], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(bySlug.status, 0, `stderr: ${bySlug.stderr}`);
- assert.match(bySlug.stdout, /improve hierarchy/);
- });
-
- it('keeps URL schemes and non-default ports in separate identity streams', () => {
- const bodyFile = join(cwd, 'critique.md');
- const targets = [
- ['http://example.test/review', 'http backlog'],
- ['https://example.test/review', 'https backlog'],
- ['https://example.test:8443/review', 'port backlog'],
- ];
-
- for (const [target, backlog] of targets) {
- writeFileSync(bodyFile, `# Critique\n\nP1: ${backlog}`);
- const write = spawnSync(process.execPath, [SCRIPT, 'write', target, bodyFile], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(write.status, 0, `stderr: ${write.stderr}`);
- }
-
- const results = targets.map(([target, backlog]) => {
- const latest = spawnSync(
- process.execPath,
- [SCRIPT, 'latest', target, '--json'],
- { cwd, encoding: 'utf-8' },
- );
- assert.equal(latest.status, 0, `stderr: ${latest.stderr}`);
- const result = JSON.parse(latest.stdout);
- assert.match(result.body, new RegExp(backlog));
- return result;
- });
- assert.equal(new Set(results.map((result) => result.snapshot_file)).size, 3);
-
- const wrongClose = spawnSync(process.execPath, [
- SCRIPT,
- 'close',
- targets[1][0],
- results[0].snapshot_file,
- ], { cwd, encoding: 'utf-8' });
- assert.equal(wrongClose.status, 2, `stderr: ${wrongClose.stderr}`);
- const httpStillOpen = spawnSync(
- process.execPath,
- [SCRIPT, 'latest', targets[0][0]],
- { cwd, encoding: 'utf-8' },
- );
- assert.equal(httpStillOpen.status, 0, `stderr: ${httpStillOpen.stderr}`);
- assert.match(httpStillOpen.stdout, /http backlog/);
-
- const closeHttp = spawnSync(process.execPath, [
- SCRIPT,
- 'close',
- targets[0][0],
- results[0].snapshot_file,
- ], { cwd, encoding: 'utf-8' });
- assert.equal(closeHttp.status, 0, `stderr: ${closeHttp.stderr}`);
-
- for (const [target, backlog] of targets.slice(1)) {
- const latest = spawnSync(process.execPath, [SCRIPT, 'latest', target], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(latest.status, 0, `stderr: ${latest.stderr}`);
- assert.match(latest.stdout, new RegExp(backlog));
- }
- });
-
- it('latest --json returns the exact snapshot identity and body', () => {
- const target = 'https://example.com/exact';
- const snapshot = writeSnapshot({
- slug: 'example-com-exact',
- meta: { total_score: 20 },
- body: 'exact backlog',
- cwd,
- });
- const r = spawnSync(process.execPath, [SCRIPT, 'latest', target, '--json'], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(r.status, 0, `stderr: ${r.stderr}`);
- const result = JSON.parse(r.stdout);
- assert.equal(result.snapshot_file, basename(snapshot));
- assert.match(result.body, /exact backlog/);
- });
-
- it('close subcommand closes the identified snapshot and preserves its trend', () => {
- const snapshot = writeSnapshot({
- slug: 'index-astro',
- meta: { total_score: 20 },
- body: 'open',
- cwd,
- });
- const r = spawnSync(process.execPath, [
- SCRIPT,
- 'close',
- 'index-astro',
- basename(snapshot),
- ], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(r.status, 0, `stderr: ${r.stderr}`);
- assert.equal(readLatestSnapshot('index-astro', { cwd }), null);
- assert.equal(readTrend('index-astro', { cwd }).length, 1);
- assert.equal(readTrend('index-astro', { cwd })[0].closed, true);
- });
-
- it('close subcommand leaves a newer critique backlog active', () => {
- const target = 'https://example.com/index';
- const first = writeSnapshot({
- slug: 'example-com-index',
- meta: { total_score: 20 },
- body: 'first backlog',
- cwd,
- now: new Date('2026-05-12T00:00:00Z'),
- });
- const read = spawnSync(process.execPath, [SCRIPT, 'latest', target, '--json'], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(read.status, 0, `stderr: ${read.stderr}`);
- assert.equal(JSON.parse(read.stdout).snapshot_file, basename(first));
-
- const newer = writeSnapshot({
- slug: 'example-com-index',
- meta: { total_score: 30 },
- body: 'newer unprocessed backlog',
- cwd,
- now: new Date('2026-05-12T00:00:01Z'),
- });
- const close = spawnSync(process.execPath, [
- SCRIPT,
- 'close',
- target,
- basename(first),
- ], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(close.status, 0, `stderr: ${close.stderr}`);
- assert.equal(readLatestSnapshot('example-com-index', { cwd }).path, newer);
- assert.equal(readLatestSnapshotAcrossTargets({ cwd }).path, newer);
- const trend = readTrend('example-com-index', { cwd });
- assert.equal(trend[0].closed, true);
- assert.equal(trend[1].closed, undefined);
- });
-
- it('close subcommand exits 2 when the identified snapshot is already closed', () => {
- const snapshot = writeSnapshot({
- slug: 'index-astro',
- meta: { total_score: 20 },
- body: 'open',
- cwd,
- });
- closeSnapshot(snapshot, { cwd });
- const r = spawnSync(process.execPath, [
- SCRIPT,
- 'close',
- 'index-astro',
- basename(snapshot),
- ], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(r.status, 2);
- });
-
- it('close subcommand exits 2 when no snapshot exists', () => {
- const r = spawnSync(process.execPath, [
- SCRIPT,
- 'close',
- 'never-written',
- '2026-05-12T00-00-00Z__never-written.md',
- ], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(r.status, 2);
- });
-
- it('close subcommand requires the identity returned by latest --json', () => {
- const r = spawnSync(process.execPath, [SCRIPT, 'close', 'index-astro'], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(r.status, 1);
- assert.match(r.stderr, /snapshot-file/);
- });
-
- it('close subcommand rejects a snapshot identity from another slug', () => {
- const home = writeSnapshot({ slug: 'home', meta: { total_score: 20 }, body: 'home', cwd });
- const r = spawnSync(process.execPath, [SCRIPT, 'close', 'pricing', basename(home)], {
- cwd,
- encoding: 'utf-8',
- });
- assert.equal(r.status, 2);
- assert.notEqual(readLatestSnapshot('home', { cwd }), null);
- });
-});
-
-describe('readTrend', () => {
- it('returns last N entries oldest → newest, filtered by slug', () => {
- for (let i = 0; i < 6; i++) {
- writeSnapshot({
- slug: 'index-astro',
- meta: { total_score: 20 + i },
- body: `run ${i}`,
- cwd,
- now: new Date(2026, 4, i + 1),
- });
- }
- writeSnapshot({ slug: 'pricing-astro', meta: { total_score: 99 }, body: 'unrelated', cwd });
- const trend = readTrend('index-astro', { limit: 5, cwd });
- assert.equal(trend.length, 5);
- assert.equal(trend[0].total_score, 21); // dropped the oldest
- assert.equal(trend[4].total_score, 25);
- });
-
- it('returns empty when no snapshots', () => {
- assert.deepEqual(readTrend('nope', { cwd }), []);
- });
-});
diff --git a/tests/design-parser.test.mjs b/tests/design-parser.test.mjs
deleted file mode 100644
index a2cee5962..000000000
--- a/tests/design-parser.test.mjs
+++ /dev/null
@@ -1,342 +0,0 @@
-/**
- * Tests for design-parser.mjs — frontmatter + body extraction.
- * Run with: node --test tests/design-parser.test.mjs
- */
-
-import { describe, it } from 'node:test';
-import assert from 'node:assert/strict';
-import { assessCoverage, parseDesignMd } from '../skill/scripts/lib/design-parser.mjs';
-
-describe('parseDesignMd frontmatter branch', () => {
- it('returns null frontmatter when the file has no YAML header', () => {
- const md = `# Design System: Demo
-
-## 1. Overview
-
-Some prose.
-`;
- const model = parseDesignMd(md);
- assert.equal(model.schemaVersion, 2);
- assert.equal(model.frontmatter, null);
- assert.equal(model.title, 'Design System: Demo');
- });
-
- it('parses a Stitch-shaped frontmatter and strips it from the body', () => {
- const md = `---
-name: Demo System
-description: A quiet editorial look.
-colors:
- primary: "#b8422e"
- neutral-bg: "#faf7f2"
-typography:
- display:
- fontFamily: "Cormorant Garamond, Georgia, serif"
- fontWeight: 300
- lineHeight: 1
- body:
- fontFamily: "Inter, sans-serif"
-rounded:
- sm: "4px"
- md: "8px"
-components:
- button-primary:
- backgroundColor: "{colors.primary}"
- textColor: "{colors.neutral-bg}"
- rounded: "{rounded.sm}"
----
-
-# Design System: Demo
-
-## 1. Overview
-
-Opening prose.
-`;
- const model = parseDesignMd(md);
- assert.equal(model.schemaVersion, 2);
- assert.equal(model.title, 'Design System: Demo');
- assert.ok(model.frontmatter);
- assert.equal(model.frontmatter.name, 'Demo System');
- assert.equal(model.frontmatter.description, 'A quiet editorial look.');
- assert.equal(model.frontmatter.colors.primary, '#b8422e');
- assert.equal(model.frontmatter.colors['neutral-bg'], '#faf7f2');
- assert.equal(model.frontmatter.typography.display.fontFamily, 'Cormorant Garamond, Georgia, serif');
- assert.equal(model.frontmatter.typography.display.fontWeight, 300);
- assert.equal(model.frontmatter.typography.display.lineHeight, 1);
- assert.equal(model.frontmatter.rounded.md, '8px');
- assert.equal(model.frontmatter.components['button-primary'].backgroundColor, '{colors.primary}');
- });
-
- it('recovers gracefully when frontmatter has no closing marker', () => {
- // No `---` terminator: the whole file is treated as body, not partial
- // frontmatter. The H1 title still resolves from the body.
- const md = `---
-this is not valid yaml : : :
-no closing marker
-# Design System: Broken
-
-## 1. Overview
-
-Prose.
-`;
- const model = parseDesignMd(md);
- assert.equal(model.frontmatter, null);
- assert.equal(model.title, 'Design System: Broken');
- });
-
- it('ignores line-only comments but preserves unquoted hex values', () => {
- const md = `---
-# Top-level comment
-colors:
- primary: #b8422e
- # mid-block comment
- accent: "#ec4899"
----
-
-# Design System: Commented
-
-## 1. Overview
-
-Prose.
-`;
- const model = parseDesignMd(md);
- assert.equal(model.frontmatter.colors.primary, '#b8422e');
- assert.equal(model.frontmatter.colors.accent, '#ec4899');
- });
-
- it('strips inline comments after quoted OKLCH values', () => {
- const md = `---
-colors:
- kinpaku-gold: "oklch(84% 0.19 80.46)" # primary accent
- gold-hairline: "oklch(58% 0.065 82 / 0.32)" # default rule
----
-
-# Design System: Kinpaku
-
-## 1. Overview
-
-Prose.
-`;
- const model = parseDesignMd(md);
- assert.equal(model.frontmatter.colors['kinpaku-gold'], 'oklch(84% 0.19 80.46)');
- assert.equal(model.frontmatter.colors['gold-hairline'], 'oklch(58% 0.065 82 / 0.32)');
- });
-
- it('normalizes quoted YAML keys in token maps', () => {
- const md = `---
-rounded:
- "2xl": "80px"
- '3xl': "96px"
-colors:
- "brand-gold": "#d9a531"
----
-
-# Design System: Quoted Keys
-
-## 1. Overview
-
-Prose.
-`;
- const model = parseDesignMd(md);
- assert.equal(model.frontmatter.rounded['2xl'], '80px');
- assert.equal(model.frontmatter.rounded['3xl'], '96px');
- assert.equal(model.frontmatter.colors['brand-gold'], '#d9a531');
- assert.equal(model.frontmatter.rounded['"2xl"'], undefined);
- });
-
- it('unescapes quote escapes inside quoted scalars (issue #428)', () => {
- const md = `---
-typography:
- body:
- fontFamily: "\\"IBM Plex Sans\\", system-ui, sans-serif"
-name: 'It''s quiet'
-empty: "
----
-
-# Design System: Escaped
-
-## 1. Overview
-
-Prose.
-`;
- const model = parseDesignMd(md);
- // YAML double-quoted scalars process backslash escapes.
- assert.equal(model.frontmatter.typography.body.fontFamily, '"IBM Plex Sans", system-ui, sans-serif');
- // Single-quoted scalars escape the quote by doubling it.
- assert.equal(model.frontmatter.name, "It's quiet");
- // A lone quote satisfies startsWith and endsWith at once; keep it literal
- // instead of slicing it into an empty string.
- assert.equal(model.frontmatter.empty, '"');
- });
-
- it('decodes hex, Unicode, and whitespace escapes in double-quoted scalars', () => {
- const md = `---
-colors:
- accent: "\\x23b8422e"
-typography:
- accent:
- fontFamily: "S\\u00f6hne, sans-serif"
- label:
- fontFamily: "IBM\\ Plex\\ Serif, serif"
- mono:
- fontFamily: "Space\\_Grotesk, sans-serif"
-emoji: "\\U0001F44D"
-bad-hex: "\\xZZ nope"
-bad-range: "\\UFFFFFFFF nope"
----
-
-# Design System: Hex Escapes
-
-## 1. Overview
-
-Prose.
-`;
- const model = parseDesignMd(md);
- assert.equal(model.frontmatter.colors.accent, '#b8422e');
- assert.equal(model.frontmatter.typography.accent.fontFamily, 'Söhne, sans-serif');
- // \ (escaped space) and \_ (non-breaking space) are valid YAML escapes.
- assert.equal(model.frontmatter.typography.label.fontFamily, 'IBM Plex Serif, serif');
- assert.equal(model.frontmatter.typography.mono.fontFamily, 'Space\u00a0Grotesk, sans-serif');
- assert.equal(model.frontmatter.emoji, '\u{1F44D}');
- // Malformed or out-of-range sequences stay literal.
- assert.equal(model.frontmatter['bad-hex'], '\\xZZ nope');
- assert.equal(model.frontmatter['bad-range'], '\\UFFFFFFFF nope');
- });
-});
-
-describe('parseDesignMd overview branch', () => {
- it('joins wrapped Key Characteristics bullets without leaking continuations into philosophy', () => {
- const md = `# Design System: Example
-
-## Overview
-
-**Creative North Star: "Structured clarity"**
-
-**Key Characteristics:**
-
-- Status remains understandable without relying on color
- alone.
-- Navigation controls remain visible when the viewport becomes
- narrow.
-`;
- const overview = parseDesignMd(md).overview;
-
- assert.deepEqual(overview.keyCharacteristics, [
- 'Status remains understandable without relying on color alone.',
- 'Navigation controls remain visible when the viewport becomes narrow.',
- ]);
- assert.deepEqual(overview.philosophy, []);
- });
-});
-
-describe('parseDesignMd named rules', () => {
- it('preserves format precedence while deduplicating later definitions', () => {
- const md = `# Design System: Rules
-
-## Layout
-
-### The "Rhythm" Rule
-Ignore this duplicate heading definition.
-
-### The Fallback Principle
-Use the heading definition.
-
-### Named Rules
-- **The Fallback Principle:** Ignore this duplicate bullet definition.
-- **The Layering Principle:** Use the bullet definition.
-
-**The Rhythm Rule.** Keep the first definition.
-`;
-
- assert.deepEqual(parseDesignMd(md).layout.rules, [
- { name: 'The Rhythm Rule', body: 'Keep the first definition.' },
- { name: 'The Fallback Principle', body: 'Use the heading definition.' },
- { name: 'The Layering Principle', body: 'Use the bullet definition.' },
- ]);
- });
-});
-
-describe('parseDesignMd canonical sections', () => {
- it('preserves content and named rules from all eight canonical sections', () => {
- const md = `# Design System: Complete
-
-## Overview
-
-**Creative North Star: "Structured clarity"**
-
-## Colors
-
-### Primary
-- **Ink** (#111111): Primary text.
-
-## Typography
-
-**Body Font:** Inter (with sans-serif)
-
-## Layout: Responsive rhythm
-
-Primary regions use a twelve-column grid that collapses to one column on narrow screens.
-
-### Named Rules
-**The Spatial Hierarchy Rule.** Primary content must remain visually dominant.
-
-## Elevation & Depth
-
-Surfaces use tonal layering instead of shadows.
-
-## Shapes
-
-Selected objects use a double outline and clipped corners.
-
-### The "Recognizable Silhouette" Rule
-Repeated geometry must remain recognizable without color.
-
-## Components
-
-### Button
-- **Primary:** Uses the accent color.
-
-## Do's and Don'ts
-
-### Do
-- Preserve the spatial hierarchy.
-
-### Don't
-- Flatten every surface.
-`;
- const model = parseDesignMd(md);
-
- assert.equal(model.layout.subtitle, 'Responsive rhythm');
- assert.equal(
- model.layout.description,
- 'Primary regions use a twelve-column grid that collapses to one column on narrow screens.',
- );
- assert.deepEqual(model.layout.rules, [{
- name: 'The Spatial Hierarchy Rule',
- body: 'Primary content must remain visually dominant.',
- }]);
- assert.equal(model.shapes.description, 'Selected objects use a double outline and clipped corners.');
- assert.deepEqual(model.shapes.rules, [{
- name: 'The Recognizable Silhouette Rule',
- body: 'Repeated geometry must remain recognizable without color.',
- }]);
-
- const coverage = assessCoverage(model);
- assert.deepEqual(
- Object.entries(coverage).filter(([, v]) => v === 'missing').map(([k]) => k),
- [],
- 'a section present in the markdown must not be reported as missing',
- );
- assert.deepEqual(Object.keys(coverage), [
- 'overview',
- 'colors',
- 'typography',
- 'layout',
- 'elevation',
- 'shapes',
- 'components',
- 'dosDonts',
- ]);
- assert.deepEqual(coverage.layout, { description: true, rules: 1 });
- assert.deepEqual(coverage.shapes, { description: true, rules: 1 });
- });
-});
diff --git a/tests/design-system.test.mjs b/tests/design-system.test.mjs
deleted file mode 100644
index ebd83243f..000000000
--- a/tests/design-system.test.mjs
+++ /dev/null
@@ -1,688 +0,0 @@
-/**
- * Design-system normalization and source-rule tests.
- * Run with: node --test tests/design-system.test.mjs
- */
-
-import { describe, it, afterEach } 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 {
- checkSourceDesignSystem,
- collectStaticDesignSystemFindings,
- isAllowedColorRaw,
- isAllowedShadowColorRaw,
- isAllowedFont,
- isAllowedRadiusRaw,
- isAllowedFontSizeRaw,
- loadDesignSystemForCwd,
- normalizeDesignSystem,
-} from '../cli/engine/design-system.mjs';
-
-const tempDirs = [];
-
-function mkTmp() {
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-design-system-'));
- tempDirs.push(dir);
- return dir;
-}
-
-function sampleDesignSystem() {
- return normalizeDesignSystem({
- frontmatter: {
- typography: {
- display: { fontFamily: 'Avenir Next, Georgia, serif', fontSize: 'clamp(2.5rem, 6vw, 4rem)' },
- body: { fontFamily: 'IBM Plex Sans, Arial, sans-serif', fontSize: '16px' },
- label: { fontFamily: 'IBM Plex Sans, Arial, sans-serif', fontSize: '0.875rem' },
- },
- colors: {
- ink: '#241f1a',
- paper: '#f7f4ee',
- accent: '#b8422e',
- gold: 'oklch(84% 0.19 80.46)',
- },
- rounded: {
- sm: '4px',
- md: '8px',
- '"2xl"': '80px',
- full: '999px',
- },
- },
- sidecar: {
- extensions: {
- colorMeta: {
- gold: {
- canonical: 'oklch(84% 0.19 80.46)',
- tonalRamp: ['#d9a531', '#b98518'],
- },
- },
- roundedMeta: {
- soft: {
- canonical: '12px',
- values: ['24px'],
- },
- },
- },
- },
- });
-}
-
-afterEach(() => {
- while (tempDirs.length) {
- fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
- }
-});
-
-describe('normalizeDesignSystem()', () => {
- it('normalizes typography, colors, sidecar ramps, and quoted rounded keys', () => {
- const designSystem = sampleDesignSystem();
-
- assert.equal(isAllowedFont('avenir next', designSystem), true);
- assert.equal(isAllowedFont('ibm plex sans', designSystem), true);
- assert.equal(isAllowedFont('system-ui', designSystem), true);
- assert.equal(isAllowedFont('poppins', designSystem), false);
-
- assert.equal(isAllowedColorRaw('#241f1a', designSystem), true);
- assert.equal(isAllowedColorRaw('oklch(84% 0.19 80.46 / 0.5)', designSystem), true);
- assert.equal(isAllowedColorRaw('#d9a531', designSystem), true);
- assert.equal(isAllowedColorRaw('#ff00aa', designSystem), false);
- assert.equal(isAllowedColorRaw('var(--brand-accent)', designSystem), true);
- assert.equal(isAllowedColorRaw('currentColor', designSystem), true);
-
- assert.equal(isAllowedRadiusRaw('0', designSystem), true);
- assert.equal(isAllowedRadiusRaw('50%', designSystem), true);
- assert.equal(isAllowedRadiusRaw('80px', designSystem), true);
- assert.equal(isAllowedRadiusRaw('12px', designSystem), true);
- assert.equal(isAllowedRadiusRaw('24px', designSystem), true);
- assert.equal(isAllowedRadiusRaw('100px', designSystem), true);
- assert.equal(isAllowedRadiusRaw('9999px', designSystem), true);
- assert.equal(isAllowedRadiusRaw('18px', designSystem), false);
-
- assert.equal(isAllowedFontSizeRaw('16px', designSystem), true);
- assert.equal(isAllowedFontSizeRaw('1rem', designSystem), true);
- assert.equal(isAllowedFontSizeRaw('0.875rem', designSystem), true);
- assert.equal(isAllowedFontSizeRaw('14px', designSystem), true);
- assert.equal(isAllowedFontSizeRaw('12.5px', designSystem), false);
- assert.equal(isAllowedFontSizeRaw('1.2em', designSystem), true);
- assert.equal(isAllowedFontSizeRaw('var(--text-body)', designSystem), true);
-
- // Fluid values are judged on their endpoints. This fixture documents 14px,
- // 16px, and the display role's 40px/64px endpoints, so a 2rem (32px) max is
- // off the ramp even though the 1rem min is on it.
- assert.equal(isAllowedFontSizeRaw('clamp(1rem, 2vw, 2rem)', designSystem), false);
- assert.equal(isAllowedFontSizeRaw('clamp(2.5rem, 6vw, 4rem)', designSystem), true);
- });
-
- it('reads a typography.scale map as literal ramp steps', () => {
- const designSystem = normalizeDesignSystem({
- frontmatter: {
- typography: {
- scale: {
- micro: '0.5625rem', // 9px
- body: '1rem', // 16px
- title: '1.5rem', // 24px
- },
- body: { fontFamily: 'IBM Plex Sans, Arial, sans-serif', fontSize: '1rem' },
- },
- },
- });
-
- assert.equal(designSystem.hasFontSizes, true);
- assert.equal(isAllowedFontSizeRaw('9px', designSystem), true);
- assert.equal(isAllowedFontSizeRaw('0.5625rem', designSystem), true);
- assert.equal(isAllowedFontSizeRaw('24px', designSystem), true);
- // Off every step by more than the 0.5px tolerance.
- assert.equal(isAllowedFontSizeRaw('0.82rem', designSystem), false);
- assert.equal(isAllowedFontSizeRaw('20px', designSystem), false);
- });
-
- it('accepts both clamp() endpoints as ramp steps', () => {
- const designSystem = normalizeDesignSystem({
- frontmatter: {
- typography: {
- scale: { body: '1rem' },
- display: { fontFamily: 'Alumni Sans, sans-serif', fontSize: 'clamp(3.4rem, 6.5vw, 5.6rem)' },
- },
- },
- });
-
- // 3.4rem = 54.4px (min) and 5.6rem = 89.6px (max) are both documented.
- assert.equal(isAllowedFontSizeRaw('3.4rem', designSystem), true);
- assert.equal(isAllowedFontSizeRaw('5.6rem', designSystem), true);
- assert.equal(isAllowedFontSizeRaw('54.4px', designSystem), true);
- assert.equal(isAllowedFontSizeRaw('89.6px', designSystem), true);
- // The vw middle term is viewport-relative, never a fixed step.
- assert.equal(isAllowedFontSizeRaw('6.5px', designSystem), false);
- // An arbitrary size between the endpoints is still off the ramp.
- assert.equal(isAllowedFontSizeRaw('4.2rem', designSystem), false);
- });
-
- it('validates clamp() endpoints in usage, not just in DESIGN.md', () => {
- const designSystem = normalizeDesignSystem({
- frontmatter: {
- typography: {
- scale: { body: '1rem', title: '1.5rem' }, // 16px, 24px
- },
- },
- });
-
- // Both endpoints documented.
- assert.equal(isAllowedFontSizeRaw('clamp(1rem, 2vw, 1.5rem)', designSystem), true);
- // Neither endpoint is a step: 23.2px and 28.8px.
- assert.equal(isAllowedFontSizeRaw('clamp(1.45rem, 1.8vw, 1.8rem)', designSystem), false);
- // One bad endpoint is enough.
- assert.equal(isAllowedFontSizeRaw('clamp(1rem, 2vw, 1.8rem)', designSystem), false);
- // The viewport term interpolates and is never judged as a step.
- assert.equal(isAllowedFontSizeRaw('clamp(1rem, 6.5vw, 1.5rem)', designSystem), true);
- // Unjudgeable endpoints abstain rather than guess.
- assert.equal(isAllowedFontSizeRaw('clamp(var(--a), 2vw, 1.5rem)', designSystem), true);
- assert.equal(isAllowedFontSizeRaw('clamp(var(--a), 2vw, var(--b))', designSystem), true);
- // Malformed or unparseable fluid values abstain.
- assert.equal(isAllowedFontSizeRaw('clamp(1.45rem, 1.8vw)', designSystem), true);
- // Non-clamp functional values keep abstaining.
- assert.equal(isAllowedFontSizeRaw('calc(1rem + 3px)', designSystem), true);
- assert.equal(isAllowedFontSizeRaw('var(--text-body)', designSystem), true);
- });
-
- it("accepts DESIGN.md's own fluid roles when used verbatim in source", () => {
- // The endpoints a fluid role declares are documented sizes, so authoring
- // that exact clamp must not flag. Regression guard for the asymmetry where
- // the extractor read clamp endpoints but the checker never validated them.
- const designSystem = normalizeDesignSystem({
- frontmatter: {
- typography: {
- scale: { body: '1rem' },
- display: { fontFamily: 'Alumni Sans, sans-serif', fontSize: 'clamp(3.4rem, 6.5vw, 5.6rem)' },
- },
- },
- });
-
- assert.equal(isAllowedFontSizeRaw('clamp(3.4rem, 6.5vw, 5.6rem)', designSystem), true);
- assert.equal(isAllowedFontSizeRaw('clamp(3.4rem, 6.5vw, 6rem)', designSystem), false);
- });
-
- it('strips CSS priority markers from the font-size ignore value', () => {
- // The ignoreValue is what a `hooks ignore-value` waiver has to match, so a
- // size must not need two different waivers depending on whether the
- // declaration carries !important. font-family already behaves this way.
- const designSystem = normalizeDesignSystem({
- frontmatter: { typography: { scale: { body: '1rem' } } },
- });
- const findings = checkSourceDesignSystem(
- '.a { font-size: 1.4rem !important; }\n.b { font-size: 1.4rem; }',
- '/tmp/important.css',
- { designSystem },
- );
- const sizes = findings.filter((f) => f.antipattern === 'design-system-font-size');
- assert.equal(sizes.length, 2);
- assert.deepEqual(sizes.map((f) => f.ignoreValue), ['1.4rem', '1.4rem']);
- });
-
- it('reports which fluid endpoint is off the ramp', () => {
- const designSystem = normalizeDesignSystem({
- frontmatter: { typography: { scale: { body: '1rem' } } },
- });
- const findings = checkSourceDesignSystem(
- '.a { font-size: clamp(1.45rem, 1.8vw, 1.8rem) !important; }',
- '/tmp/fluid.css',
- { designSystem },
- );
- const sizes = findings.filter((f) => f.antipattern === 'design-system-font-size');
- assert.equal(sizes.length, 1);
- assert.match(sizes[0].snippet, /1\.45rem/);
- assert.match(sizes[0].snippet, /1\.8rem/);
- assert.equal(sizes[0].ignoreValue, '1.45rem');
- });
-
- it('does not let clamp() endpoints alone switch the font-size rule on', () => {
- // A fully fluid system enumerates no discrete ramp, so inferring one from
- // clamp endpoints would flag every intermediate size. Keep abstaining.
- const designSystem = normalizeDesignSystem({
- frontmatter: {
- typography: {
- display: { fontFamily: 'Avenir Next, Georgia, serif', fontSize: 'clamp(2.5rem, 6vw, 4rem)' },
- body: { fontFamily: 'IBM Plex Sans, Arial, sans-serif', fontSize: 'clamp(1rem, 2vw, 1.125rem)' },
- },
- },
- });
-
- assert.equal(designSystem.hasFontSizes, false);
- assert.equal(isAllowedFontSizeRaw('12.5px', designSystem), true);
- });
-});
-
-describe('loadDesignSystemForCwd()', () => {
- it('loads DESIGN.md plus .impeccable/design.json and marks stale sidecars', () => {
- const cwd = mkTmp();
- fs.mkdirSync(path.join(cwd, '.impeccable'), { recursive: true });
- const designMd = path.join(cwd, 'DESIGN.md');
- const sidecarJson = path.join(cwd, '.impeccable', 'design.json');
-
- fs.writeFileSync(designMd, `---
-typography:
- body:
- fontFamily: "IBM Plex Sans, Arial, sans-serif"
-colors:
- ink: "#241f1a"
-rounded:
- "2xl": "80px"
----
-
-# Design System
-`);
- fs.writeFileSync(sidecarJson, JSON.stringify({
- extensions: {
- colorMeta: {
- accent: {
- canonical: '#b8422e',
- tonalRamp: ['#d55a42'],
- },
- },
- roundedMeta: {
- lg: { canonical: '24px' },
- },
- shadows: [
- { name: 'ambient-low', value: '0 4px 24px rgba(0,0,0,0.12)', purpose: 'Diffuse hover glow.' },
- ],
- },
- }));
-
- fs.utimesSync(sidecarJson, new Date('2026-01-01T00:00:00Z'), new Date('2026-01-01T00:00:00Z'));
- fs.utimesSync(designMd, new Date('2026-01-02T00:00:00Z'), new Date('2026-01-02T00:00:00Z'));
-
- const loaded = loadDesignSystemForCwd(cwd);
- assert.equal(loaded.present, true);
- assert.equal(loaded.sourcePath, designMd);
- assert.equal(loaded.sidecarPath, sidecarJson);
- assert.equal(loaded.mdNewerThanJson, true);
- assert.equal(isAllowedColorRaw('#d55a42', loaded), true);
- assert.equal(isAllowedRadiusRaw('80px', loaded), true);
- assert.equal(isAllowedRadiusRaw('24px', loaded), true);
- assert.equal(isAllowedShadowColorRaw('rgba(0, 0, 0, 0.12)', loaded), true);
- assert.equal(isAllowedShadowColorRaw('rgba(0, 0, 0, 0.5)', loaded), false);
- });
-
- it('unescapes YAML-escaped quotes around multi-word font families (issue #428)', () => {
- // A YAML double-quoted scalar processes backslash escapes, so a stack that
- // quotes a multi-word family the CSS way arrives as
- // fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif"
- // Before the fix the family reached allowedFonts as '\"ibm plex sans' and
- // the rule flagged fonts DESIGN.md declares.
- const cwd = mkTmp();
- fs.writeFileSync(path.join(cwd, 'DESIGN.md'), `---
-typography:
- display:
- fontFamily: "Archivo, system-ui, sans-serif"
- body:
- fontFamily: "\\"IBM Plex Sans\\", system-ui, sans-serif"
- data:
- fontFamily: '"IBM Plex Mono", ui-monospace, monospace'
- accent:
- fontFamily: "S\\u00f6hne, sans-serif"
- label:
- fontFamily: "IBM\\ Plex\\ Serif, serif"
- mono:
- fontFamily: "Space\\_Grotesk, sans-serif"
-colors:
- accent: "\\x23b8422e"
----
-
-# Design System
-`);
-
- const loaded = loadDesignSystemForCwd(cwd);
- assert.deepEqual(
- [...loaded.allowedFonts].sort(),
- ['archivo', 'ibm plex mono', 'ibm plex sans', 'ibm plex serif', 'space grotesk', 'söhne'],
- );
- assert.equal(isAllowedFont('ibm plex sans', loaded), true);
- assert.equal(isAllowedFont('ibm plex mono', loaded), true);
- // Escaped space (\ ) and non-breaking space (\_) forms; NBSP collapses to
- // a plain space in normalizeFontName, so the CSS declaration matches.
- assert.equal(isAllowedFont('ibm plex serif', loaded), true);
- assert.equal(isAllowedFont('space grotesk', loaded), true);
- assert.equal(isAllowedFont('comic sans ms', loaded), false);
- // \x escapes decode too: "\x23b8422e" is #b8422e.
- assert.equal(isAllowedColorRaw('#b8422e', loaded), true);
- assert.equal(isAllowedColorRaw('#ff00aa', loaded), false);
-
- const findings = checkSourceDesignSystem(`
-body { font-family: "IBM Plex Sans", system-ui, sans-serif; }
-code { font-family: "IBM Plex Mono", ui-monospace, monospace; }
-h1 { font-family: Archivo, system-ui, sans-serif; }
-em { font-family: "Söhne", sans-serif; color: #b8422e; }
-small { font-family: "IBM Plex Serif", serif; }
-pre { font-family: "Space Grotesk", sans-serif; }
-`, '/tmp/escaped-fonts.css', { designSystem: loaded });
- assert.deepEqual(findings, []);
- });
-});
-
-describe('checkSourceDesignSystem()', () => {
- it('reports source fonts, literal colors, and radii outside DESIGN.md', () => {
- const designSystem = sampleDesignSystem();
- const findings = checkSourceDesignSystem(`
-.good {
- font-family: "IBM Plex Sans", Arial, sans-serif;
- color: #241f1a;
- background: rgba(184, 66, 46, 0.45);
- border-radius: 8px;
-}
-
-.bad {
- font-family: "Poppins", sans-serif;
- color: #ff00aa;
- background: rgba(255, 0, 170, 1);
- border-radius: 18px;
-}
-`, '/tmp/source.css', { designSystem });
-
- assert.deepEqual(
- findings.map((item) => item.antipattern),
- ['design-system-font', 'design-system-color', 'design-system-color', 'design-system-radius'],
- );
- assert.deepEqual(
- findings.map((item) => item.ignoreValue),
- ['Poppins', '#ff00aa', 'rgba(255, 0, 170, 1)', '18px'],
- );
- });
-
- it('strips CSS priority markers before checking font-family declarations', () => {
- const designSystem = sampleDesignSystem();
- const findings = checkSourceDesignSystem(`
-.good {
- font-family: "IBM Plex Sans", Arial, sans-serif !important;
-}
-
-.also-good {
- font-family: "Avenir Next" !important;
-}
-
-.bad {
- font-family: "Poppins" !important;
-}
-`, '/tmp/important.css', { designSystem });
-
- assert.deepEqual(
- findings.map((item) => item.ignoreValue),
- ['Poppins'],
- );
- });
-
- it('does not treat issue labels, HTML entities, or font variables as literal design values', () => {
- const designSystem = sampleDesignSystem();
- const findings = checkSourceDesignSystem(`
-#155
-↔
-const MONO = 'SFMono-Regular, Roboto Mono, Consolas, monospace';
-const FONT = 'IBM Plex Sans, Arial, sans-serif';
-const COLOR_SAMPLE = 'rgba(255, 0, 170, 1)';
-const COLOR_NOTE = 'oklch(60% 0.2 20)';
-button.innerHTML = \`Pick\`;
-scale.style.cssText = 'font-family:' + MONO + '; font-size: 10px;';
-.demo [style*="background: #fef3c7"] {
- border-color: #ff00aa;
-}
-
-.bad {
- font-family: "Poppins", sans-serif;
- color: #cc00ff;
-}
-`, '/tmp/source.jsx', { designSystem });
-
- assert.deepEqual(
- findings.map((item) => item.ignoreValue),
- ['10px', '#ff00aa', 'Poppins', '#cc00ff'],
- );
- });
-
- it('reports literal font sizes outside the DESIGN.md type ramp', () => {
- const designSystem = sampleDesignSystem();
- const source = `.off-ramp {
- font-size: 12.5px;
-}
-const label = { fontSize: "11px" };
-const badge = { className: "text-[10px]" };
-/* font-size: 9px; */
-.on-ramp {
- font-size: 1rem;
-}
-`;
- const findings = checkSourceDesignSystem(source, '/tmp/sizes.css', { designSystem });
- const fontSizeFindings = findings.filter((item) => item.antipattern === 'design-system-font-size');
-
- assert.equal(fontSizeFindings.length, 3);
- assert.deepEqual(
- fontSizeFindings.map((item) => item.ignoreValue),
- ['12.5px', '11px', '10px'],
- );
- assert.deepEqual(
- fontSizeFindings.map((item) => item.line),
- [2, 4, 5],
- );
- });
-
- it('abstains on font-size checks when DESIGN.md has no literal ramp steps', () => {
- const designSystem = normalizeDesignSystem({
- frontmatter: {
- typography: {
- display: { fontFamily: 'Avenir Next, Georgia, serif', fontSize: 'clamp(2.5rem, 6vw, 4rem)' },
- body: { fontFamily: 'IBM Plex Sans, Arial, sans-serif', fontSize: 'clamp(1rem, 2vw, 1.125rem)' },
- },
- },
- });
- assert.equal(designSystem.hasFontSizes, false);
-
- const findings = checkSourceDesignSystem('.bad { font-size: 12.5px; }', '/tmp/clamp-only.css', { designSystem });
- assert.equal(findings.some((item) => item.antipattern === 'design-system-font-size'), false);
- });
-});
-
-describe('sidecar shadow tokens (issue #547)', () => {
- // Mirrors the sidecar `extensions.shadows` schema from document.md Step 4b.
- function shadowDesignSystem() {
- return normalizeDesignSystem({
- frontmatter: {
- colors: { ink: '#241f1a', paper: '#f7f4ee' },
- },
- sidecar: {
- extensions: {
- shadows: [
- {
- name: 'outset',
- value: 'inset 0 1px 0 oklch(1 0 0 / 0.07), 0 1px 2px oklch(0 0 0 / 0.28), 0 4px 12px oklch(0 0 0 / 0.22)',
- purpose: 'Default card shadow.',
- },
- ],
- },
- },
- });
- }
-
- it('matches documented shadow colors on alpha, not just r/g/b', () => {
- const designSystem = shadowDesignSystem();
- assert.equal(isAllowedShadowColorRaw('oklch(0 0 0 / 0.28)', designSystem), true);
- assert.equal(isAllowedShadowColorRaw('rgba(0, 0, 0, 0.28)', designSystem), true);
- assert.equal(isAllowedShadowColorRaw('oklch(1 0 0 / 0.07)', designSystem), true);
- // Same black, undocumented alpha: the r/g/b channels alone must not match.
- assert.equal(isAllowedShadowColorRaw('oklch(0 0 0 / 55%)', designSystem), false);
- assert.equal(isAllowedShadowColorRaw('#000', designSystem), false);
- // Shadow tokens must not switch the general color rule's allowlist on.
- assert.equal(isAllowedColorRaw('oklch(0 0 0 / 0.28)', designSystem), false);
- });
-
- it('allows documented shadow colors in shadow contexts only', () => {
- const designSystem = shadowDesignSystem();
- const findings = checkSourceDesignSystem(`
-.a { box-shadow: 0 1px 2px oklch(0 0 0 / 0.28); }
-.b { box-shadow: 0 20px 50px oklch(0 0 0 / 55%); }
-.c { background: #000; }
-.d { background: oklch(0 0 0 / 0.28); }
-.e { text-shadow: 0 1px 2px oklch(0 0 0 / 0.28); }
-.f { box-shadow: inset 0 1px 0 oklch(1 0 0 / 0.07), 0 4px 12px oklch(0 0 0 / 0.22); }
-const card = { boxShadow: "0 1px 2px rgba(0, 0, 0, 0.28)" };
-const layered = { boxShadow: "0 1px 2px rgba(0, 0, 0, 0.28), 0 4px 12px rgba(0, 0, 0, 0.22)" };
-const bad = { color: "rgba(0, 0, 0, 0.28)" };
-const leak = { boxShadow: "0 1px 2px rgba(0, 0, 0, 0.28)", color: "rgba(0, 0, 0, 0.28)" };
-`, '/tmp/shadows.css', { designSystem });
- const colors = findings.filter((item) => item.antipattern === 'design-system-color');
-
- // .a, .e, .f, and both JS boxShadow strings (including the second layer
- // past the comma) pass; .b (undocumented alpha), .c (forbidden ground),
- // .d (documented alpha outside a shadow), and both JS color keys still
- // fire — the `leak` line proves the closing quote stops the shadow
- // context from reaching a later property. A fix that silences .d has
- // stopped discriminating between shadow usage and page grounds.
- assert.deepEqual(
- colors.map((item) => [item.line, item.ignoreValue]),
- [
- [3, 'oklch(0 0 0 / 55%)'],
- [4, '#000'],
- [5, 'oklch(0 0 0 / 0.28)'],
- [10, 'rgba(0, 0, 0, 0.28)'],
- [11, 'rgba(0, 0, 0, 0.28)'],
- ],
- );
- });
-
- it('abstains from the color rule entirely when only shadows are documented', () => {
- // A shadows-only sidecar must not switch hasColors on: with no palette to
- // measure against, the engine abstains rather than guesses, same as every
- // other design-system rule.
- const designSystem = normalizeDesignSystem({
- sidecar: {
- extensions: {
- shadows: [{ name: 'outset', value: '0 1px 2px oklch(0 0 0 / 0.28)' }],
- },
- },
- });
- assert.equal(designSystem.hasColors, false);
- const findings = checkSourceDesignSystem(
- '.c { background: #000; }',
- '/tmp/shadows-only.css',
- { designSystem },
- );
- assert.equal(findings.some((item) => item.antipattern === 'design-system-color'), false);
- });
-
- it('keeps shadow context across template interpolations', () => {
- const designSystem = shadowDesignSystem();
- const findings = checkSourceDesignSystem(`
-const card = { boxShadow: \`0 \${offset}px 2px rgba(0, 0, 0, 0.28)\` };
- box-shadow: 0 1px \${blur}px rgba(0, 0, 0, 0.28);
-const fn = { boxShadow: \`0 \${getShadow('lg')} 2px rgba(0, 0, 0, 0.28)\` };
-const tern = { boxShadow: \`0 1px \${dark ? "4px" : "2px"} rgba(0, 0, 0, 0.28)\` };
- box-shadow: 0 \${theme('blur')} rgba(0, 0, 0, 0.28);
-const nested = { boxShadow: \`0 \${getOffset({ size: 2 })}px 2px rgba(0, 0, 0, 0.28)\` };
-const nestedQ = { boxShadow: \`0 \${getOffset({ size: 'lg' })}px rgba(0, 0, 0, 0.28)\` };
-const leak = { boxShadow: \`0 \${offset}px rgba(0, 0, 0, 0.28)\`, color: "rgba(0, 0, 0, 0.28)" };
-`, '/tmp/interpolated.js', { designSystem });
- const colors = findings.filter((item) => item.antipattern === 'design-system-color');
-
- // The documented shadow color passes after a \${...} interpolation in
- // the JS template literal and the CSS-in-JS line, including
- // interpolations carrying quoted function arguments, ternary branches,
- // and one level of object-literal braces; the color key on the leak line
- // still fires because it sits past the template's closing backtick.
- assert.deepEqual(
- colors.map((item) => [item.line, item.ignoreValue]),
- [[9, 'rgba(0, 0, 0, 0.28)']],
- );
- });
-
- it('does not allow a later declaration to inherit shadow context from earlier on the line', () => {
- const designSystem = shadowDesignSystem();
- const findings = checkSourceDesignSystem(
- '.x { box-shadow: 0 1px 2px oklch(0 0 0 / 0.28); background: oklch(0 0 0 / 0.28); }',
- '/tmp/one-line.css',
- { designSystem },
- );
- const colors = findings.filter((item) => item.antipattern === 'design-system-color');
- assert.equal(colors.length, 1);
- assert.equal(colors[0].ignoreValue, 'oklch(0 0 0 / 0.28)');
- });
-});
-
-describe('collectStaticDesignSystemFindings()', () => {
- function makeElement(tagName, { text = '', attrs = {}, style = {}, parentElement = null } = {}) {
- return {
- tagName: tagName.toUpperCase(),
- textContent: text,
- parentElement,
- _style: style,
- childNodes: text ? [{ nodeType: 3, textContent: text }] : [],
- getAttribute(name) {
- return Object.prototype.hasOwnProperty.call(attrs, name) ? attrs[name] : null;
- },
- };
- }
-
- function makeWindow() {
- const defaults = {
- color: 'rgb(36, 31, 26)',
- backgroundColor: 'rgba(0, 0, 0, 0)',
- borderTopWidth: '0px',
- borderRightWidth: '0px',
- borderBottomWidth: '0px',
- borderLeftWidth: '0px',
- borderTopColor: 'rgb(36, 31, 26)',
- borderRightColor: 'rgb(36, 31, 26)',
- borderBottomColor: 'rgb(36, 31, 26)',
- borderLeftColor: 'rgb(36, 31, 26)',
- outlineWidth: '0px',
- outlineColor: 'rgb(36, 31, 26)',
- borderRadius: '0px',
- display: '',
- visibility: 'visible',
- fontFamily: 'IBM Plex Sans, Arial, sans-serif',
- };
- return {
- getComputedStyle(el) {
- return { ...defaults, ...(el?._style || {}) };
- },
- };
- }
-
- it('skips non-rendered tags and hidden elements in the static DOM pass', () => {
- const designSystem = sampleDesignSystem();
- const hiddenParent = makeElement('section', { attrs: { hidden: '' } });
- const elements = [
- makeElement('style', {
- text: '.hidden { color: #ff00aa; font-family: Poppins; }',
- style: { color: 'rgb(0, 0, 0)', fontFamily: 'Poppins, sans-serif' },
- }),
- makeElement('script', {
- text: 'const color = "#ff00aa";',
- style: { color: 'rgb(0, 0, 0)', fontFamily: 'Poppins, sans-serif' },
- }),
- makeElement('div', {
- text: 'Hidden Drift',
- parentElement: hiddenParent,
- style: { color: 'rgb(255, 0, 170)', fontFamily: 'Poppins, sans-serif', borderRadius: '18px' },
- }),
- makeElement('div', {
- text: 'Display None Drift',
- style: { display: 'none', color: 'rgb(255, 0, 170)', fontFamily: 'Poppins, sans-serif', borderRadius: '18px' },
- }),
- makeElement('div', {
- text: 'Visible Drift',
- style: { color: 'rgb(255, 0, 170)', fontFamily: 'Poppins, sans-serif', borderRadius: '18px' },
- }),
- ];
- const findings = collectStaticDesignSystemFindings(
- { querySelectorAll: () => elements },
- makeWindow(),
- '/tmp/page.html',
- designSystem,
- );
- const snippets = findings.map(item => item.snippet).join('\n');
-
- assert.match(snippets, /Visible Drift/);
- assert.doesNotMatch(snippets, /Hidden Drift/);
- assert.doesNotMatch(snippets, /Display None Drift/);
- assert.doesNotMatch(snippets, /\.hidden/);
- assert.doesNotMatch(snippets, /const color/);
- });
-});
diff --git a/tests/detect-antipatterns-browser.test.mjs b/tests/detect-antipatterns-browser.test.mjs
deleted file mode 100644
index 0a264bf28..000000000
--- a/tests/detect-antipatterns-browser.test.mjs
+++ /dev/null
@@ -1,1373 +0,0 @@
-/**
- * Puppeteer-backed fixture tests for browser-only detection rules.
- *
- * Some detection rules (cramped-padding, line-length, body-text-viewport-edge)
- * need real browser layout — they read getBoundingClientRect and real
- * getComputedStyle results that the static HTML/CSS engine intentionally
- * does not invent.
- *
- * This file uses detectUrl() (Puppeteer) to load fixtures in headless Chrome
- * via a temporary static HTTP server, so the fixtures can use absolute
- * ',
- '
',
- '',
- ].join('\n');
- const cssDelimiters = [
- '',
- '
',
- '',
- ].join('\n');
-
- for (const filePath of ['hero.astro', 'hero.vue', 'hero.svelte']) {
- expect(detectText(htmlDelimiters, filePath).filter(r => r.antipattern === 'broken-image')).toHaveLength(1);
- expect(detectText(cssDelimiters, filePath).filter(r => r.antipattern === 'broken-image')).toHaveLength(1);
- }
- });
-
- test('ignores preprocessor line comments in stylesheets', () => {
- const source = '// font-family: Inter\n.hero { color: red; }';
-
- for (const filePath of ['hero.scss', 'hero.sass', 'hero.less']) {
- expect(detectText(source, filePath).filter(r => r.antipattern === 'overused-font')).toHaveLength(0);
- }
- });
-
- test('still detects live font-family after a preprocessor line comment', () => {
- const source = '// skip this\n.hero { font-family: Inter; }';
-
- const findings = detectText(source, 'hero.scss').filter(r => r.antipattern === 'overused-font');
- expect(findings).toHaveLength(1);
- expect(findings[0].line).toBe(2);
- });
-
- test('does not blank https URLs in SCSS', () => {
- const source = '.hero { background: url(https://example.com/i.png); }\n.hero { font-family: Inter; }';
-
- const findings = detectText(source, 'hero.scss').filter(r => r.antipattern === 'overused-font');
- expect(findings).toHaveLength(1);
- expect(findings[0].line).toBe(2);
- });
-
- test('ignores frontmatter comments after a --- line inside a template literal', () => {
- const source = [
- '---',
- 'const md = `',
- '---',
- '`;',
- '//
',
- '---',
- 'ok
',
- ].join('\n');
-
- expect(detectText(source, 'hero.astro').filter(r => r.antipattern === 'broken-image')).toHaveLength(0);
- });
-
- test('ignores preprocessor line comments in component style blocks', () => {
- const source = [
- '',
- ].join('\n');
-
- for (const filePath of ['hero.astro', 'hero.vue', 'hero.svelte']) {
- expect(detectText(source, filePath).filter(r => r.antipattern === 'overused-font')).toHaveLength(0);
- }
- });
-
- test('still detects live font-family after a style-block line comment', () => {
- const source = [
- '',
- ].join('\n');
-
- const findings = detectText(source, 'hero.vue').filter(r => r.antipattern === 'overused-font');
- expect(findings).toHaveLength(1);
- expect(findings[0].line).toBe(3);
- });
-
- test('ignores frontmatter comments after a regex literal that contains quotes', () => {
- const source = [
- '---',
- 'const re = /["\']/;',
- '//
',
- '---',
- 'ok
',
- ].join('\n');
-
- expect(detectText(source, 'hero.astro').filter(r => r.antipattern === 'broken-image')).toHaveLength(0);
- });
-
- test('keeps live font-family after a protocol-relative URL in SCSS', () => {
- const sources = [
- '.hero { background: url( //cdn.example.com/i.png); font-family: Inter; }',
- '.hero { background: url(#{$prefix}//cdn.example.com/i.png); font-family: Inter; }',
- ];
-
- for (const source of sources) {
- expect(detectText(source, 'hero.scss').filter(r => r.antipattern === 'overused-font')).toHaveLength(1);
- }
- expect(detectText(
- '.hero { background: url(@{prefix}//cdn.example.com/i.png); font-family: Inter; }',
- 'hero.less',
- ).filter(r => r.antipattern === 'overused-font')).toHaveLength(1);
- });
-});
-
-describe('detectText — CSS borders', () => {
- test('detects border-left shorthand', () => {
- const f = detectText('.card { border-left: 4px solid #3b82f6; }', 'test.css');
- expect(f.some(r => r.antipattern === 'side-tab')).toBe(true);
- });
-
- test('detects border-left shorthand in Sass', () => {
- const f = detectText(".card\n border-left: 4px solid #3b82f6", 'test.sass');
- expect(f.some(r => r.antipattern === 'side-tab')).toBe(true);
- });
-
- test('ignores neutral border', () => {
- const f = detectText('.card { border-left: 4px solid #e5e7eb; }', 'test.css');
- expect(f.filter(r => r.antipattern === 'side-tab')).toHaveLength(0);
- });
-
- test('skips blockquote', () => {
- const f = detectText('', 'test.html');
- expect(f.filter(r => r.antipattern === 'side-tab')).toHaveLength(0);
- });
-});
-
-describe('detectText — overused fonts', () => {
- test('detects Inter', () => {
- const f = detectText("body { font-family: 'Inter', sans-serif; }", 'test.css');
- expect(f.some(r => r.antipattern === 'overused-font')).toBe(true);
- });
-
- test('detects Fraunces (current AI-default monoculture)', () => {
- const f = detectText("h1 { font-family: 'Fraunces', Georgia, serif; }", 'test.css');
- expect(f.some(r => r.antipattern === 'overused-font')).toBe(true);
- });
-
- test('detects Geist (Vercel-default monoculture)', () => {
- const f = detectText("body { font-family: 'Geist', sans-serif; }", 'test.css');
- expect(f.some(r => r.antipattern === 'overused-font')).toBe(true);
- });
-
- test('does not flag distinctive fonts', () => {
- const f = detectText("body { font-family: 'Karla', sans-serif; }", 'test.css');
- expect(f.filter(r => r.antipattern === 'overused-font')).toHaveLength(0);
- });
-
- test('detects overused Google Fonts css2 family after first family param', () => {
- const page = pageWithGoogleFonts('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300&family=Inter:wght@400;500;600&display=swap');
- const f = detectText(page, 'index.html');
- expect(f.some(r => r.antipattern === 'overused-font' && /Inter/i.test(r.snippet))).toBe(true);
- });
-
- test('does not flag single-font for combined Google Fonts css2 families', () => {
- const page = pageWithGoogleFonts('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300&family=Jost:wght@300;400;500&display=swap');
- const f = detectText(page, 'index.html');
- expect(f.filter(r => r.antipattern === 'single-font')).toHaveLength(0);
- });
-
- test('keeps legacy Google Fonts css pipe-separated families multi-font', () => {
- const page = pageWithGoogleFonts('https://fonts.googleapis.com/css?family=Cormorant+Garamond|Jost&display=swap');
- const f = detectText(page, 'index.html');
- expect(f.filter(r => r.antipattern === 'single-font')).toHaveLength(0);
- });
-
- test('page typography parses repeated Google Fonts css2 family params', () => {
- const f = pageTypographyForGoogleFonts('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300&family=Inter:wght@400;500;600&display=swap');
- expect(f.some(r => r.id === 'overused-font' && /inter/i.test(r.snippet))).toBe(true);
- expect(f.filter(r => r.id === 'single-font')).toHaveLength(0);
- });
-});
-
-describe('detectHtml — overused fonts system stack', () => {
- test('Inter before a system stack still flags overused-font', async () => {
- const page = `Hello
world
`;
- await withStaticFixture({ 'index.html': page }, async ({ file }) => {
- const f = await detectHtml(file);
- expect(f.some(r => r.antipattern === 'overused-font' && /inter/i.test(r.snippet))).toBe(true);
- });
- });
-
- test('checkPageTypography regex path skips Roboto in system stack', () => {
- const html = `Hello
world
`;
- const doc = {
- styleSheets: [],
- documentElement: { outerHTML: html },
- querySelectorAll() { return []; },
- };
- const win = { getComputedStyle() { return { fontSize: '16px' }; } };
- const f = checkPageTypography(doc, win);
- expect(f.filter(r => r.id === 'overused-font')).toHaveLength(0);
- });
-});
-
-describe('detectText — flat type hierarchy', () => {
- test('flags sizes too close together', () => {
- const page = '';
- const f = detectText(page, 'test.html');
- expect(f.some(r => r.antipattern === 'flat-type-hierarchy')).toBe(true);
- });
-
- test('passes good hierarchy', () => {
- const page = '';
- const f = detectText(page, 'test.html');
- expect(f.filter(r => r.antipattern === 'flat-type-hierarchy')).toHaveLength(0);
- });
-});
-
-// Static HTML/CSS fixture tests moved to detect-antipatterns-fixtures.test.mjs (run via node --test)
-
-// ---------------------------------------------------------------------------
-// Full page vs partial detection
-// ---------------------------------------------------------------------------
-
-describe('isFullPage', () => {
- test('detects DOCTYPE', () => expect(isFullPage('')).toBe(true));
- test('detects ', () => expect(isFullPage('')).toBe(true));
- test('detects ', () => expect(isFullPage('')).toBe(true));
- test('rejects component/partial', () => expect(isFullPage('content
')).toBe(false));
- test('rejects JSX', () => expect(isFullPage('export default function Card() { return hi
}')).toBe(false));
-});
-
-describe('partials skip page-level checks', () => {
- test('regex: partial with flat hierarchy is not flagged', () => {
- const partial = 'text
\ntext
\ntext
';
- const f = detectText(partial, 'card.tsx');
- expect(f.filter(r => r.antipattern === 'flat-type-hierarchy')).toHaveLength(0);
- });
-
- test('regex: partial with single overused font is not flagged for single-font', () => {
- const partial = `text
\n`.repeat(25);
- const f = detectText(partial, 'card.tsx');
- expect(f.filter(r => r.antipattern === 'single-font')).toHaveLength(0);
- });
-
- test('regex: partial still flags border anti-patterns', () => {
- const partial = 'card
';
- const f = detectText(partial, 'card.tsx');
- expect(f.some(r => r.antipattern === 'side-tab')).toBe(true);
- });
-
- test('regex: full page with flat hierarchy IS flagged', () => {
- const page = '\n' +
- 'h1
\nh2
\n' +
- 'p
\ns\n' +
- 'sm\n';
- const f = detectText(page, 'index.html');
- expect(f.some(r => r.antipattern === 'flat-type-hierarchy')).toBe(true);
- });
-});
-
-describe('detectText — numeric content', () => {
- test('does not infer section scaffolding from raw numeric sequences', () => {
- const page = '' +
- '- 01 — Glassline — 03:11
' +
- '- 02 — Tidal Memory — 04:08
' +
- '- 03 — Pale Signal — 05:02
' +
- '';
- const f = detectText(page, 'test.html');
- expect(f.some(r => r.antipattern === 'numbered-section-markers')).toBe(false);
- });
-
- test('does not infer section scaffolding from JS source with embedded numbers', () => {
- const source = `
- const shell = 'Preview';
- const palette = 'oklch(86% 0.07 84 / 0.08)';
- const shadow = '0 0 0 1px oklch(0% 0 0 / 0.04), 0 4px 16px oklch(0% 0 0 / 0.05), 0 1px 3px oklch(0% 0 0 / 0.06)';
- const size = '11.5px';
- const eye = '';
- const shader = 'float band = bandAt(uv.y - y, 0.05, 0.32);';
- const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
- `;
- const f = detectText(source, 'live-browser.js');
- expect(f.filter(r => r.antipattern === 'numbered-section-markers')).toHaveLength(0);
- });
-});
-
-// ---------------------------------------------------------------------------
-// Layout anti-patterns
-// ---------------------------------------------------------------------------
-
-describe('detectHtml — layout', () => {
- test('detects monotonous spacing via regex', () => {
- // A page where every padding/margin is 16px
- const html = '' +
- ''.repeat(5) +
- '';
- const f = detectText(html, 'test.html');
- expect(f.some(r => r.antipattern === 'monotonous-spacing')).toBe(true);
- });
-
-});
-
-// ---------------------------------------------------------------------------
-// Text overflow screen-reader-only handling
-// ---------------------------------------------------------------------------
-
-describe('checkElementTextOverflowDOM', () => {
- function baseTextStyle(overrides = {}) {
- return {
- position: 'static',
- width: '160px',
- height: '20px',
- overflow: 'visible',
- overflowX: 'visible',
- overflowY: 'visible',
- clipPath: 'none',
- clip: 'auto',
- ...overrides,
- };
- }
-
- function mockTextElement({
- className = 'flag-overflow',
- style = baseTextStyle(),
- clientWidth = 24,
- clientHeight = 20,
- scrollWidth = 80,
- rectWidth = clientWidth,
- rectHeight = clientHeight,
- } = {}) {
- return {
- tagName: 'DIV',
- className,
- childNodes: [{ nodeType: 3, textContent: 'A long accessible label that overflows its box' }],
- parentElement: null,
- clientWidth,
- clientHeight,
- scrollWidth,
- __style: style,
- getAttribute(name) {
- return name === 'class' ? className : null;
- },
- getBoundingClientRect() {
- return { width: rectWidth, height: rectHeight };
- },
- };
- }
-
- function withMockComputedStyle(callback) {
- const original = globalThis.getComputedStyle;
- globalThis.getComputedStyle = (el) => el.__style;
- try {
- return callback();
- } finally {
- if (original === undefined) delete globalThis.getComputedStyle;
- else globalThis.getComputedStyle = original;
- }
- }
-
- test('classifies clip-path sr-only text as visually hidden', () => {
- expect(isScreenReaderOnlyTextStyle(baseTextStyle({
- position: 'absolute',
- width: '1px',
- height: '1px',
- overflow: 'hidden',
- overflowX: 'hidden',
- overflowY: 'hidden',
- clipPath: 'inset(50%)',
- }), { width: 1, height: 1 })).toBe(true);
- });
-
- test('classifies legacy clip rect sr-only text as visually hidden', () => {
- expect(isScreenReaderOnlyTextStyle(baseTextStyle({
- position: 'absolute',
- width: '1px',
- height: '1px',
- overflow: 'hidden',
- overflowX: 'hidden',
- overflowY: 'hidden',
- clip: 'rect(0, 0, 0, 0)',
- }), { width: 1, height: 1 })).toBe(true);
- });
-
- test('classifies tiny absolute overflow-hidden text as visually hidden without clip', () => {
- expect(isScreenReaderOnlyTextStyle(baseTextStyle({
- position: 'absolute',
- width: '1px',
- height: '1px',
- overflow: 'hidden',
- overflowX: 'hidden',
- overflowY: 'hidden',
- }), { width: 1, height: 1 })).toBe(true);
- });
-
- test('classifies fully clipped text as visually hidden without tiny sizing', () => {
- expect(isScreenReaderOnlyTextStyle(baseTextStyle({
- position: 'absolute',
- width: '160px',
- height: '20px',
- overflow: 'visible',
- clipPath: 'inset(50%)',
- }), { width: 160, height: 20 })).toBe(true);
- });
-
- test('flags visible overflowing text', () => {
- const findings = withMockComputedStyle(() => checkElementTextOverflowDOM(mockTextElement()));
-
- expect(findings).toHaveLength(1);
- expect(findings[0].id).toBe('text-overflow');
- expect(findings[0].snippet).toContain('.flag-overflow');
- });
-
- test('skips overflowing sr-only text', () => {
- const srOnly = mockTextElement({
- className: 'pass-sr-only-clip-path',
- style: baseTextStyle({
- position: 'absolute',
- width: '1px',
- height: '1px',
- overflow: 'hidden',
- overflowX: 'hidden',
- overflowY: 'hidden',
- clipPath: 'inset(50%)',
- }),
- clientWidth: 1,
- clientHeight: 1,
- scrollWidth: 240,
- rectWidth: 1,
- rectHeight: 1,
- });
-
- const findings = withMockComputedStyle(() => checkElementTextOverflowDOM(srOnly));
-
- expect(findings).toHaveLength(0);
- });
-
- test('does not classify tiny visible text as sr-only', () => {
- const style = baseTextStyle({
- position: 'absolute',
- width: '1px',
- height: '1px',
- });
-
- expect(isScreenReaderOnlyTextStyle(style, { width: 1, height: 1 })).toBe(false);
- });
-});
-
-// ---------------------------------------------------------------------------
-// Motion anti-patterns
-// ---------------------------------------------------------------------------
-
-describe('checkElementMotion', () => {
- function mockStyle(overrides) {
- return { transitionProperty: '', animationName: 'none', animationTimingFunction: '', transitionTimingFunction: '', ...overrides };
- }
-
- test('detects bounce animation name', () => {
- const f = checkElementMotion('div', mockStyle({ animationName: 'bounce' }));
- expect(f.some(r => r.id === 'bounce-easing')).toBe(true);
- });
-
- test('detects elastic animation name', () => {
- const f = checkElementMotion('div', mockStyle({ animationName: 'elastic-in' }));
- expect(f.some(r => r.id === 'bounce-easing')).toBe(true);
- });
-
- test('detects overshoot cubic-bezier in animation timing', () => {
- const f = checkElementMotion('div', mockStyle({
- animationTimingFunction: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
- }));
- expect(f.some(r => r.id === 'bounce-easing')).toBe(true);
- });
-
- test('detects overshoot cubic-bezier in transition timing', () => {
- const f = checkElementMotion('div', mockStyle({
- transitionTimingFunction: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
- }));
- expect(f.some(r => r.id === 'bounce-easing')).toBe(true);
- });
-
- test('passes standard ease-out-quart', () => {
- const f = checkElementMotion('div', mockStyle({
- transitionTimingFunction: 'cubic-bezier(0.25, 1, 0.5, 1)',
- }));
- expect(f.filter(r => r.id === 'bounce-easing')).toHaveLength(0);
- });
-
- test('passes standard ease', () => {
- const f = checkElementMotion('div', mockStyle({
- transitionTimingFunction: 'cubic-bezier(0.25, 0.1, 0.25, 1.0)',
- }));
- expect(f.filter(r => r.id === 'bounce-easing')).toHaveLength(0);
- });
-
- test('detects width transition', () => {
- const f = checkElementMotion('div', mockStyle({ transitionProperty: 'width' }));
- expect(f.some(r => r.id === 'layout-transition')).toBe(true);
- });
-
- test('detects height transition', () => {
- const f = checkElementMotion('div', mockStyle({ transitionProperty: 'height' }));
- expect(f.some(r => r.id === 'layout-transition')).toBe(true);
- });
-
- test('detects padding transition', () => {
- const f = checkElementMotion('div', mockStyle({ transitionProperty: 'padding' }));
- expect(f.some(r => r.id === 'layout-transition')).toBe(true);
- });
-
- test('detects margin transition', () => {
- const f = checkElementMotion('div', mockStyle({ transitionProperty: 'margin' }));
- expect(f.some(r => r.id === 'layout-transition')).toBe(true);
- });
-
- test('detects max-height transition', () => {
- const f = checkElementMotion('div', mockStyle({ transitionProperty: 'max-height' }));
- expect(f.some(r => r.id === 'layout-transition')).toBe(true);
- });
-
- test('detects layout prop among mixed transitions', () => {
- const f = checkElementMotion('div', mockStyle({ transitionProperty: 'opacity, width, color' }));
- expect(f.some(r => r.id === 'layout-transition')).toBe(true);
- });
-
- test('passes transform transition', () => {
- const f = checkElementMotion('div', mockStyle({ transitionProperty: 'transform' }));
- expect(f.filter(r => r.id === 'layout-transition')).toHaveLength(0);
- });
-
- test('passes opacity transition', () => {
- const f = checkElementMotion('div', mockStyle({ transitionProperty: 'opacity' }));
- expect(f.filter(r => r.id === 'layout-transition')).toHaveLength(0);
- });
-
- test('skips transition: all', () => {
- const f = checkElementMotion('div', mockStyle({ transitionProperty: 'all' }));
- expect(f.filter(r => r.id === 'layout-transition')).toHaveLength(0);
- });
-
- test('skips safe tags', () => {
- const f = checkElementMotion('button', mockStyle({
- animationName: 'bounce', transitionProperty: 'width',
- }));
- expect(f).toHaveLength(0);
- });
-});
-
-describe('detectText — motion', () => {
- test('detects animate-bounce Tailwind class', () => {
- const f = detectText('loading
', 'test.html');
- expect(f.some(r => r.antipattern === 'bounce-easing')).toBe(true);
- });
-
- test('detects animation: bounce CSS', () => {
- const f = detectText('.icon { animation: bounce-ball 1s infinite; }', 'test.css');
- const finding = f.find(r => r.antipattern === 'bounce-easing');
- expect(finding).toBeTruthy();
- expect(finding.snippet).toBe('animation: bounce-ball');
- });
-
- test('detects animation-name: elastic', () => {
- const f = detectText('.card { animation-name: elastic; }', 'test.css');
- expect(f.some(r => r.antipattern === 'bounce-easing')).toBe(true);
- });
-
- test('detects overshoot cubic-bezier', () => {
- const f = detectText('.btn { transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); }', 'test.css');
- expect(f.some(r => r.antipattern === 'bounce-easing')).toBe(true);
- });
-
- test('passes standard cubic-bezier', () => {
- const f = detectText('.btn { transition: transform 0.4s cubic-bezier(0.25, 1, 0.5, 1); }', 'test.css');
- expect(f.filter(r => r.antipattern === 'bounce-easing')).toHaveLength(0);
- });
-
- test('detects transition: width', () => {
- const f = detectText('.sidebar { transition: width 0.3s ease; }', 'test.css');
- expect(f.some(r => r.antipattern === 'layout-transition')).toBe(true);
- });
-
- test('detects transition: height', () => {
- const f = detectText('.panel { transition: height 0.4s ease-out; }', 'test.css');
- expect(f.some(r => r.antipattern === 'layout-transition')).toBe(true);
- });
-
- test('detects transition: max-height', () => {
- const f = detectText('.accordion { transition: max-height 0.5s ease; }', 'test.css');
- expect(f.some(r => r.antipattern === 'layout-transition')).toBe(true);
- });
-
- test('detects transition-property: width', () => {
- const f = detectText('.box { transition-property: width; transition-duration: 0.3s; }', 'test.css');
- expect(f.some(r => r.antipattern === 'layout-transition')).toBe(true);
- });
-
- test('skips transition: all', () => {
- const f = detectText('.card { transition: all 0.3s ease; }', 'test.css');
- expect(f.filter(r => r.antipattern === 'layout-transition')).toHaveLength(0);
- });
-
- test('skips transition: transform', () => {
- const f = detectText('.card { transition: transform 0.3s ease; }', 'test.css');
- expect(f.filter(r => r.antipattern === 'layout-transition')).toHaveLength(0);
- });
-
- test('skips transition: opacity', () => {
- const f = detectText('.btn { transition: opacity 0.2s ease; }', 'test.css');
- expect(f.filter(r => r.antipattern === 'layout-transition')).toHaveLength(0);
- });
-
- test('passes JSX quoted paint-only transition with later layout prop', () => {
- const f = detectText("", 'test.jsx');
- expect(f.filter(r => r.antipattern === 'layout-transition')).toHaveLength(0);
- });
-
- test('passes JSX grid-template-rows transition with later padding and width', () => {
- const f = detectText("", 'test.jsx');
- expect(f.filter(r => r.antipattern === 'layout-transition')).toHaveLength(0);
- });
-
- test('detects JSX quoted width transition', () => {
- const f = detectText("", 'test.jsx');
- expect(f.some(r => r.antipattern === 'layout-transition')).toBe(true);
- });
-
- test('skips JSX quoted transition: all', () => {
- const f = detectText("", 'test.jsx');
- expect(f.filter(r => r.antipattern === 'layout-transition')).toHaveLength(0);
- });
-});
-
-// ---------------------------------------------------------------------------
-// Dark glow anti-pattern
-// ---------------------------------------------------------------------------
-
-describe('checkElementGlow', () => {
- function mockStyle(overrides) {
- return { boxShadow: 'none', backgroundColor: '', ...overrides };
- }
-
- // Dark bg = luminance < 0.1 (e.g. #111827 = gray-900)
- const darkBg = { r: 17, g: 24, b: 39 }; // #111827
- const lightBg = { r: 249, g: 250, b: 251 }; // #f9fafb
- const mediumBg = { r: 107, g: 114, b: 128 }; // #6b7280
-
- test('detects blue glow on dark background', () => {
- const f = checkElementGlow('div', mockStyle({
- boxShadow: 'rgba(59, 130, 246, 0.4) 0px 0px 20px 0px',
- }), darkBg);
- expect(f.some(r => r.id === 'dark-glow')).toBe(true);
- });
-
- test('detects purple glow on dark background', () => {
- const f = checkElementGlow('div', mockStyle({
- boxShadow: 'rgba(139, 92, 246, 0.35) 0px 0px 25px 0px',
- }), darkBg);
- expect(f.some(r => r.id === 'dark-glow')).toBe(true);
- });
-
- test('detects glow in multi-shadow', () => {
- const f = checkElementGlow('div', mockStyle({
- boxShadow: 'rgba(0, 0, 0, 0.3) 0px 4px 6px 0px, rgba(168, 85, 247, 0.3) 0px 0px 30px 0px',
- }), darkBg);
- expect(f.some(r => r.id === 'dark-glow')).toBe(true);
- });
-
- test('passes gray shadow on dark background', () => {
- const f = checkElementGlow('div', mockStyle({
- boxShadow: 'rgba(0, 0, 0, 0.4) 0px 4px 12px 0px',
- }), darkBg);
- expect(f.filter(r => r.id === 'dark-glow')).toHaveLength(0);
- });
-
- test('detects zero-offset colored halo on light background', () => {
- const f = checkElementGlow('div', mockStyle({
- boxShadow: 'rgba(59, 130, 246, 0.4) 0px 0px 20px 0px',
- }), lightBg);
- expect(f.some(r => r.id === 'dark-glow')).toBe(true);
- });
-
- test('detects zero-offset colored halo on medium gray background', () => {
- const f = checkElementGlow('div', mockStyle({
- boxShadow: 'rgba(59, 130, 246, 0.5) 0px 0px 20px 0px',
- }), mediumBg);
- expect(f.some(r => r.id === 'dark-glow')).toBe(true);
- });
-
- test('passes offset colored drop shadow on light background', () => {
- const f = checkElementGlow('div', mockStyle({
- boxShadow: 'rgba(59, 130, 246, 0.4) 0px 8px 20px 0px',
- }), lightBg);
- expect(f.filter(r => r.id === 'dark-glow')).toHaveLength(0);
- });
-
- test('passes achromatic zero-offset shadow on light background', () => {
- const f = checkElementGlow('div', mockStyle({
- boxShadow: 'rgba(0, 0, 0, 0.15) 0px 0px 24px 0px',
- }), lightBg);
- expect(f.filter(r => r.id === 'dark-glow')).toHaveLength(0);
- });
-
- test('detects oklch glow on dark background', () => {
- const f = checkElementGlow('div', mockStyle({
- boxShadow: '0 0 12px oklch(0.85 0.12 200 / 0.5)',
- }), darkBg);
- expect(f.some(r => r.id === 'dark-glow')).toBe(true);
- });
-
- test('detects oklch zero-offset glow on light background', () => {
- const f = checkElementGlow('div', mockStyle({
- boxShadow: 'oklch(0.65 0.2 300 / 0.45) 0px 0px 20px 0px',
- }), lightBg);
- expect(f.some(r => r.id === 'dark-glow')).toBe(true);
- });
-
- test('passes achromatic oklch glow (white halo) on dark background', () => {
- const f = checkElementGlow('div', mockStyle({
- boxShadow: '0 0 12px oklch(1 0 0 / .7)',
- }), darkBg);
- expect(f.filter(r => r.id === 'dark-glow')).toHaveLength(0);
- });
-
- test('detects hex glow on dark background', () => {
- const f = checkElementGlow('div', mockStyle({
- boxShadow: '0 0 16px #3b82f6',
- }), darkBg);
- expect(f.some(r => r.id === 'dark-glow')).toBe(true);
- });
-
- test('detects hsl glow on dark background', () => {
- const f = checkElementGlow('div', mockStyle({
- boxShadow: '0 0 22px hsl(280, 80%, 60%)',
- }), darkBg);
- expect(f.some(r => r.id === 'dark-glow')).toBe(true);
- });
-
- test('skips unresolvable var() shadow color instead of guessing', () => {
- const f = checkElementGlow('div', mockStyle({
- boxShadow: '0 0 10px var(--ok)',
- }), darkBg);
- expect(f.filter(r => r.id === 'dark-glow')).toHaveLength(0);
- });
-
- test('detects chromatic text-shadow glow on any background', () => {
- const f = checkElementGlow('h1', mockStyle({
- textShadow: 'rgb(34, 211, 238) 0px 0px 12px',
- }), lightBg);
- expect(f.some(r => r.id === 'dark-glow')).toBe(true);
- });
-
- test('passes offset neutral text-shadow', () => {
- const f = checkElementGlow('h1', mockStyle({
- textShadow: 'rgba(0, 0, 0, 0.6) 0px 1px 2px',
- }), darkBg);
- expect(f.filter(r => r.id === 'dark-glow')).toHaveLength(0);
- });
-
- test('passes focus ring (spread only, no blur)', () => {
- const f = checkElementGlow('div', mockStyle({
- boxShadow: 'rgba(59, 130, 246, 0.5) 0px 0px 0px 3px',
- }), darkBg);
- expect(f.filter(r => r.id === 'dark-glow')).toHaveLength(0);
- });
-
- test('passes subtle shadow (blur < 5px)', () => {
- const f = checkElementGlow('div', mockStyle({
- boxShadow: 'rgba(59, 130, 246, 0.2) 0px 1px 3px 0px',
- }), darkBg);
- expect(f.filter(r => r.id === 'dark-glow')).toHaveLength(0);
- });
-
- test('passes no shadow', () => {
- const f = checkElementGlow('div', mockStyle({ boxShadow: 'none' }), darkBg);
- expect(f.filter(r => r.id === 'dark-glow')).toHaveLength(0);
- });
-
- test('detects glow on buttons (not skipped by safe tags)', () => {
- const f = checkElementGlow('button', mockStyle({
- boxShadow: 'rgba(59, 130, 246, 0.4) 0px 0px 20px 0px',
- }), darkBg);
- expect(f.some(r => r.id === 'dark-glow')).toBe(true);
- });
-});
-
-describe('detectText — dark glow', () => {
- test('detects colored box-shadow glow on dark background', () => {
- const html = 'glow
';
- const f = detectText(html, 'test.html');
- expect(f.some(r => r.antipattern === 'dark-glow')).toBe(true);
- });
-
- test('skips gray shadow on dark background', () => {
- const html = 'shadow
';
- const f = detectText(html, 'test.html');
- expect(f.filter(r => r.antipattern === 'dark-glow')).toHaveLength(0);
- });
-
- test('detects zero-offset colored halo on light page', () => {
- const html = 'glow
';
- const f = detectText(html, 'test.html');
- expect(f.some(r => r.antipattern === 'dark-glow')).toBe(true);
- });
-
- test('skips offset colored drop shadow on light page', () => {
- const html = 'shadow
';
- const f = detectText(html, 'test.html');
- expect(f.filter(r => r.antipattern === 'dark-glow')).toHaveLength(0);
- });
-
- test('detects oklch glow on dark oklch page', () => {
- const html = 'glow
';
- const f = detectText(html, 'test.html');
- expect(f.some(r => r.antipattern === 'dark-glow')).toBe(true);
- });
-
- test('resolves single-level var() shadow colors', () => {
- const html = 'lamp
';
- const f = detectText(html, 'test.html');
- expect(f.some(r => r.antipattern === 'dark-glow')).toBe(true);
- });
-
- test('skips unresolvable var() shadow colors', () => {
- const html = 'lamp
';
- const f = detectText(html, 'test.html');
- expect(f.filter(r => r.antipattern === 'dark-glow')).toHaveLength(0);
- });
-
- test('detects chromatic text-shadow glow', () => {
- const html = 'glow
';
- const f = detectText(html, 'test.html');
- expect(f.some(r => r.antipattern === 'dark-glow')).toBe(true);
- });
-
- test('skips achromatic zero-offset halo (soft elevation)', () => {
- const html = 'card
';
- const f = detectText(html, 'test.html');
- expect(f.filter(r => r.antipattern === 'dark-glow')).toHaveLength(0);
- });
-});
-
-// ---------------------------------------------------------------------------
-// Static HTML/CSS engine
-// ---------------------------------------------------------------------------
-
-describe('detectHtml — static HTML/CSS engine', () => {
- test('inlines local linked stylesheets', async () => {
- const f = await detectHtml(path.join(FIXTURES, 'linked-stylesheet.html'));
- expect(findingIds(f)).toContain('side-tab');
- });
-
- test('resolves root-relative linked stylesheets with cache-busting query', async () => {
- await withStaticFixture({
- 'index.html': `
-
- Card
`,
- 'static/app.css': '.card { border-left: 5px solid #3b82f6; border-radius: 4px; }',
- }, async ({ file }) => {
- const f = await detectHtml(file);
- expect(findingIds(f)).toContain('side-tab');
- });
- });
-
- test('resolves root-relative linked stylesheets from nested pages via ancestor walk', async () => {
- await withStaticFixture({
- 'pages/about.html': `
-
- Card
`,
- 'static/app.css': '.card { border-left: 5px solid #3b82f6; border-radius: 4px; }',
- }, async ({ dir }) => {
- const f = await detectHtml(path.join(dir, 'pages', 'about.html'));
- expect(findingIds(f)).toContain('side-tab');
- });
- });
-
- test('does not resolve root-relative sheets above the project root', async () => {
- await withStaticFixture({
- 'project/package.json': '{}',
- 'project/index.html': `
-
- Card
`,
- 'static/app.css': '.card { border-left: 5px solid #3b82f6; border-radius: 4px; }',
- }, async ({ dir }) => {
- const f = await detectHtml(path.join(dir, 'project', 'index.html'));
- expect(findingIds(f)).not.toContain('side-tab');
- });
- });
-
- test('does not follow root-relative .. segments out of the page directory', async () => {
- await withStaticFixture({
- 'project/package.json': '{}',
- 'project/index.html': `
-
- Card
`,
- 'outside.css': '.card { border-left: 5px solid #3b82f6; border-radius: 4px; }',
- }, async ({ dir }) => {
- const f = await detectHtml(path.join(dir, 'project', 'index.html'));
- expect(findingIds(f)).not.toContain('side-tab');
- });
- });
-
- test('warns when a linked stylesheet cannot be read', async () => {
- const writes = [];
- const origWrite = process.stderr.write.bind(process.stderr);
- process.stderr.write = (chunk, ...args) => {
- writes.push(String(chunk));
- return origWrite(chunk, ...args);
- };
- try {
- await withStaticFixture({
- 'index.html': `
-
- Page
`,
- }, async ({ file, dir }) => {
- await detectHtml(file);
- await detectHtml(file);
- const msg = writes.join('');
- const hits = msg.split('could not read linked stylesheet /missing/app.css').length - 1;
- expect(hits).toBe(2);
- expect(msg).toContain(`resolved to ${path.join(dir, 'missing', 'app.css')}`);
- });
- } finally {
- process.stderr.write = origWrite;
- }
- });
-
- test('gradient-text: a style="" attribute alone carries the page-level flag', async () => {
- await withStaticFixture({
- 'index.html': `t
- Inline gradient heading
- Body copy long enough to make this a real page for the scanners.
- `,
- }, async ({ file }) => {
- const f = await detectHtml(file);
- expect(f.some(r => r.antipattern === 'gradient-text' && /background-clip/.test(r.snippet))).toBe(true);
- });
- });
-
- test('gradient-text: a
- Styled gradient heading
- Body copy long enough to make this a real page for the scanners.
- `,
- }, async ({ file }) => {
- const f = await detectHtml(file);
- expect(f.some(r => r.antipattern === 'gradient-text' && /background-clip/.test(r.snippet))).toBe(true);
- });
- });
-
- test('gradient-text: prose and code samples about the pattern do not flag', async () => {
- await withStaticFixture({
- 'index.html': `changelog
- Changelog
- background-clip: text gradients stop tripping the contrast rules.
- .hero { background: linear-gradient(135deg, #667eea, #764ba2); background-clip: text; }
- Pair bg-clip-text with bg-gradient-to-r and both utilities together are the tell.
- `,
- }, async ({ file }) => {
- const f = await detectHtml(file);
- expect(f.filter(r => r.antipattern === 'gradient-text')).toHaveLength(0);
- expect(f.filter(r => r.antipattern === 'ai-color-palette')).toHaveLength(0);
- });
- });
-
- test('flattens @layer, resolves CSS variables and fallbacks, and skips unsupported selectors', async () => {
- await withStaticFixture({
- 'index.html': `
-
-
-
-
-
- Layer variable side tab
- Fallback variable top accent
-
- `,
- }, async ({ file }) => {
- const profile = [];
- const f = await detectHtml(file, { profile });
- const ids = findingIds(f);
- expect(ids).toContain('side-tab');
- expect(ids).toContain('border-accent-on-rounded');
- expect(profile.some(e => e.engine === 'static-html' && e.ruleId === 'unsupported-selector')).toBe(true);
- });
- });
-
- test('honors specificity, source order, !important, and inline style precedence', async () => {
- await withStaticFixture({
- 'index.html': `
-
-
-
-
-
- Specificity neutral pass
- Source order chromatic flag
- Important neutral pass
- Inline chromatic flag
-
- `,
- }, async ({ file }) => {
- const f = await detectHtml(file);
- expect(findingIds(f).filter(id => id === 'side-tab')).toHaveLength(2);
- });
- });
-
- test('expands background, border, font, transition, and animation shorthands', async () => {
- await withStaticFixture({
- 'index.html': `
-
-
-
-
-
- This tiny paragraph is long enough to trigger both the static font shorthand size and line-height checks.
-
- Border shorthand side tab
- Motion shorthand easing
-
- `,
- }, async ({ file }) => {
- const ids = findingIds(await detectHtml(file));
- expect(ids).toContain('tiny-text');
- expect(ids).toContain('tight-leading');
- expect(ids).toContain('low-contrast');
- expect(ids).toContain('side-tab');
- expect(ids).toContain('bounce-easing');
- expect(ids).toContain('layout-transition');
- });
- });
-});
-
-describe('StaticDocument.closest — compiled selector cache', () => {
- test('compiles each selector once per document', () => {
- let compileCount = 0;
- const compile = (sel) => {
- compileCount++;
- return cssSelect.compile(sel);
- };
- const root = htmlparser2.parseDocument(
- '',
- );
- const doc = new StaticDocument(root, {
- selectAll: cssSelect.selectAll,
- selectOne: cssSelect.selectOne,
- compile,
- domutils,
- });
- const deep = doc.querySelectorAll('span')[0];
- const deep2 = doc.querySelectorAll('span')[1];
- expect(deep.closest('.target-ancestor').node.attribs.class).toBe('target-ancestor');
- deep.closest('.target-ancestor');
- deep2.closest('.target-ancestor');
- expect(compileCount).toBe(1);
- });
-
- test('invalid selector returns null on repeat calls', () => {
- const root = htmlparser2.parseDocument('x
');
- const doc = new StaticDocument(root, {
- selectAll: cssSelect.selectAll,
- selectOne: cssSelect.selectOne,
- compile: cssSelect.compile,
- domutils,
- });
- const p = doc.querySelector('p');
- expect(p.closest('p:has-invalid(')).toBeNull();
- expect(p.closest('p:has-invalid(')).toBeNull();
- });
-});
-
-// ---------------------------------------------------------------------------
-// Side-tab as absolutely-positioned pseudo-element stripe
-// ---------------------------------------------------------------------------
-
-describe('side-tab — pseudo-element stripe variant', () => {
- test('fixture flags both stripe variants and nothing else', async () => {
- const f = await detectHtml(path.join(FIXTURES, 'pseudo-stripe.html'));
- const stripes = f.filter(r => r.antipattern === 'side-tab');
- const snippets = stripes.map(r => r.snippet).join(' | ');
- expect(stripes).toHaveLength(2);
- expect(snippets).toContain('.card-stripe::before');
- expect(snippets).toContain('.row-stripe::after');
- });
-
- test('detects ::before stripe with var() background resolved to chromatic', () => {
- const css = `
- :root { --accent: oklch(0.78 0.145 155); }
- .hero::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 5px; background: var(--accent); }
- `;
- const f = scanCssTextForPseudoStripe(css);
- expect(f).toHaveLength(1);
- expect(f[0].id).toBe('side-tab');
- expect(f[0].snippet).toContain('.hero::before');
- });
-
- test('detects height:100% + right:0 variant', () => {
- const css = '.card::after { position: absolute; right: 0; top: 0; height: 100%; width: 4px; background: #3b82f6; }';
- expect(scanCssTextForPseudoStripe(css)).toHaveLength(1);
- });
-
- test('detects floating stripe inset a few px from each end', () => {
- // The evasion shape from human review: same left-edge accent bar, but
- // backed off the card's corners by a small top/bottom inset so it never
- // touches an edge (and needs no corner rounding to read as a side tab).
- const css = '.row::before { content: ""; position: absolute; left: 0; top: 12px; bottom: 12px; width: 3px; border-radius: 3px; background: oklch(0.65 0.19 15); }';
- const f = scanCssTextForPseudoStripe(css);
- expect(f).toHaveLength(1);
- expect(f[0].id).toBe('side-tab');
- expect(f[0].snippet).toContain('(left: 0)');
- });
-
- test('skips deeply-inset partial rail (not an edge-spanning stripe)', () => {
- const css = '.rail::before { position: absolute; left: 0; top: 40px; bottom: 40px; width: 4px; background: #3b82f6; }';
- expect(scanCssTextForPseudoStripe(css)).toHaveLength(0);
- });
-
- test('unresolvable custom-property color errs toward detection', () => {
- const css = '.card::before { position: absolute; left: 0; top: 0; bottom: 0; width: 5px; background: var(--from-external-sheet); }';
- expect(scanCssTextForPseudoStripe(css)).toHaveLength(1);
- });
-
- test('skips neutral hairline divider', () => {
- const css = '.col::before { position: absolute; left: 0; top: 0; bottom: 0; width: 1px; background: rgba(0,0,0,0.08); }';
- expect(scanCssTextForPseudoStripe(css)).toHaveLength(0);
- });
-
- test('skips neutral 4px rail (chromatic gate)', () => {
- const css = '.timeline::before { position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: rgb(209, 213, 219); }';
- expect(scanCssTextForPseudoStripe(css)).toHaveLength(0);
- });
-
- test('skips 2px stripe below width threshold', () => {
- const css = '.card::before { position: absolute; left: 0; top: 0; bottom: 0; width: 2px; background: #3b82f6; }';
- expect(scanCssTextForPseudoStripe(css)).toHaveLength(0);
- });
-
- test('skips blockquote pseudo decoration', () => {
- const css = 'blockquote::before { position: absolute; left: 0; top: 0; bottom: 0; width: 4px; background: #d97706; }';
- expect(scanCssTextForPseudoStripe(css)).toHaveLength(0);
- });
-
- test('skips non-edge-anchored pseudo (toggle knob)', () => {
- const css = '.switch::before { position: absolute; left: 2px; top: 2px; width: 10px; height: 10px; background: #3b82f6; }';
- expect(scanCssTextForPseudoStripe(css)).toHaveLength(0);
- });
-
- test('skips full-overlay pseudo (inset: 0, no narrow width)', () => {
- const css = '.hero::after { position: absolute; inset: 0; background: #3b82f6; }';
- expect(scanCssTextForPseudoStripe(css)).toHaveLength(0);
- });
-
- // Horizontal (top/bottom) stripe variant
- test('detects top-anchored full-width pseudo stripe', () => {
- const css = '.stat-card::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: #e04a3a; }';
- const f = scanCssTextForPseudoStripe(css);
- expect(f).toHaveLength(1);
- expect(f[0].snippet).toContain('(top: 0)');
- });
-
- test('detects bottom-anchored width:100% pseudo stripe', () => {
- const css = '.promo::after { content: ""; position: absolute; bottom: 0; left: 0; width: 100%; height: 5px; background: oklch(0.62 0.2 30); }';
- const f = scanCssTextForPseudoStripe(css);
- expect(f).toHaveLength(1);
- expect(f[0].snippet).toContain('(bottom: 0)');
- });
-
- test('skips link/button underline affordances (horizontal variant)', () => {
- const link = '.nav-link::after { position: absolute; bottom: 0; left: 0; right: 0; height: 3px; background: #e04a3a; }';
- const anchor = 'a.cta::after { position: absolute; bottom: 0; left: 0; width: 100%; height: 3px; background: #e04a3a; }';
- const btn = '.cta-btn::after { position: absolute; bottom: 0; left: 0; right: 0; height: 3px; background: #e04a3a; }';
- expect(scanCssTextForPseudoStripe(link)).toHaveLength(0);
- expect(scanCssTextForPseudoStripe(anchor)).toHaveLength(0);
- expect(scanCssTextForPseudoStripe(btn)).toHaveLength(0);
- });
-
- test('skips selected-state underlines, flags all-tabs underlines (horizontal variant)', () => {
- const selectedTab = '[role="tab"][aria-selected="true"]::after { position: absolute; bottom: 0; left: 0; right: 0; height: 3px; background: #4a7de0; }';
- const activeItem = '.tabs .item.active::after { position: absolute; bottom: 0; left: 0; right: 0; height: 3px; background: #4a7de0; }';
- expect(scanCssTextForPseudoStripe(selectedTab)).toHaveLength(0);
- expect(scanCssTextForPseudoStripe(activeItem)).toHaveLength(0);
- // The same stripe on EVERY tab in the group is decoration, not state.
- const allTabs = '.tabs .item::after { position: absolute; bottom: 0; left: 0; right: 0; height: 3px; background: #4a7de0; }';
- const roleTab = '[role="tab"]::after { position: absolute; bottom: 0; left: 0; right: 0; height: 3px; background: #4a7de0; }';
- expect(scanCssTextForPseudoStripe(allTabs)).toHaveLength(1);
- expect(scanCssTextForPseudoStripe(roleTab)).toHaveLength(1);
- });
-
- test('skips hover-state underline affordance (horizontal variant)', () => {
- const css = '.item:hover::after { position: absolute; bottom: 0; left: 0; right: 0; height: 3px; background: #e04a3a; }';
- expect(scanCssTextForPseudoStripe(css)).toHaveLength(0);
- });
-
- test('skips 2px and 16px horizontal bars (thickness gates)', () => {
- const thin = '.card::before { position: absolute; top: 0; left: 0; right: 0; height: 2px; background: #e04a3a; }';
- const band = '.card::before { position: absolute; top: 0; left: 0; right: 0; height: 16px; background: #e04a3a; }';
- expect(scanCssTextForPseudoStripe(thin)).toHaveLength(0);
- expect(scanCssTextForPseudoStripe(band)).toHaveLength(0);
- });
-});
-
-// ---------------------------------------------------------------------------
-// Low contrast — modern computed-color serializations (browser adapter path)
-// ---------------------------------------------------------------------------
-
-describe('checkColors — oklch computed colors', () => {
- test('flat dark-on-dark oklch CTA pair parses and fails contrast', () => {
- // Real browsers hand back oklch() strings from getComputedStyle for
- // colors authored in modern spaces; the adapters must not lose them.
- const textColor = parseAnyColor('oklch(0.34 0.01 70)');
- const bgColor = parseAnyColor('oklch(0.22 0.01 70)');
- expect(textColor).toBeTruthy();
- expect(bgColor).toBeTruthy();
- const f = checkColors({
- tag: 'a',
- textColor,
- bgColor,
- effectiveBg: bgColor,
- effectiveBgStops: null,
- fontSize: 14.4,
- fontWeight: 500,
- hasDirectText: true,
- isEmojiOnly: false,
- bgClip: '',
- bgImage: '',
- classList: 'btn btn-primary',
- });
- expect(f.some(r => r.id === 'low-contrast')).toBe(true);
- });
-});
-
-// ---------------------------------------------------------------------------
-// Numbered section labels — pure helpers
-// ---------------------------------------------------------------------------
-
-describe('numbered-section-labels — pure helpers', () => {
- test('parseNumberedLabelText accepts zero-padded and separator forms only', () => {
- expect(parseNumberedLabelText('01')).toEqual({ index: 1, text: '01' });
- expect(parseNumberedLabelText('12')).toEqual({ index: 12, text: '12' });
- expect(parseNumberedLabelText('04 / rollout')).toMatchObject({ index: 4 });
- expect(parseNumberedLabelText('6 · getting started')).toMatchObject({ index: 6 });
- expect(parseNumberedLabelText('7')).toBeNull();
- expect(parseNumberedLabelText('Step 3')).toBeNull();
- expect(parseNumberedLabelText('12 minute read')).toBeNull();
- expect(parseNumberedLabelText('50% off everything')).toBeNull();
- expect(parseNumberedLabelText('')).toBeNull();
- });
-
- test('checkNumberedSectionLabels needs 2+ candidates with 2+ distinct indices', () => {
- const candidate = (index) => ({ index, labelText: String(index).padStart(2, '0'), headingTag: 'h2', headingText: 'Heading' });
- expect(checkNumberedSectionLabels({ candidates: [candidate(1)] })).toHaveLength(0);
- expect(checkNumberedSectionLabels({ candidates: [candidate(1), candidate(1)] })).toHaveLength(0);
- const flagged = checkNumberedSectionLabels({ candidates: [candidate(1), candidate(2)] });
- expect(flagged).toHaveLength(2);
- expect(flagged[0].id).toBe('numbered-section-labels');
- });
-});
-
-// ---------------------------------------------------------------------------
-// Radial-gradient background halo
-// ---------------------------------------------------------------------------
-
-describe('radial-halo', () => {
- const darkRoot = 'body { background: oklch(0.085 0.020 262); }';
-
- test('flags chromatic halo fading to transparent on a dark page', () => {
- const css = `${darkRoot} body { background: radial-gradient(120% 80% at 50% -10%, oklch(0.240 0.045 268) 0%, transparent 55%), oklch(0.085 0.020 262); }`;
- const f = scanCssTextForRadialHalo(css);
- expect(f).toHaveLength(1);
- expect(f[0].snippet).toContain('radial-gradient halo');
- });
-
- test('skips achromatic vignette with no transparent stop', () => {
- const css = `${darkRoot} body { background: radial-gradient(120% 90% at 50% -10%, oklch(0.19 0.02 264) 0%, oklch(0.075 0.01 262) 100%); }`;
- expect(scanCssTextForRadialHalo(css)).toHaveLength(0);
- });
-
- test('skips panel sheen fading to an opaque surface color', () => {
- const css = `${darkRoot} .hero { background: radial-gradient(120% 90% at 85% 0%, oklch(0.255 0.034 262), oklch(0.205 0.032 262) 60%); }`;
- expect(scanCssTextForRadialHalo(css)).toHaveLength(0);
- });
-
- test('skips px-sized dot texture patterns', () => {
- const css = `${darkRoot} .device::before { background-image: radial-gradient(oklch(1 0 0 / 0.018) 1px, transparent 1.4px); }`;
- expect(scanCssTextForRadialHalo(css)).toHaveLength(0);
- });
-
- test('skips translucent light-scene washes (inner alpha below 0.7)', () => {
- const css = `${darkRoot} .hero .light { background: radial-gradient(closest-side, oklch(0.62 0.10 255 / 0.55), oklch(0.42 0.09 258 / 0.22) 45%, transparent 72%); }`;
- expect(scanCssTextForRadialHalo(css)).toHaveLength(0);
- });
-
- test('skips halos on light pages', () => {
- const css = 'body { background: #faf7f2; } .hero { background: radial-gradient(60% 40% at 50% 0%, #7c3aed 0%, transparent 70%); }';
- expect(scanCssTextForRadialHalo(css)).toHaveLength(0);
- });
-
- test('skips declarations that include photographic url() layers', () => {
- const css = `${darkRoot} .hero { background: url(cover.jpg), radial-gradient(60% 40% at 50% 0%, #7c3aed 0%, transparent 70%); }`;
- expect(scanCssTextForRadialHalo(css)).toHaveLength(0);
- });
-
- test('resolves var() color stops', () => {
- const css = `:root { --glow: oklch(0.5 0.18 300); } ${darkRoot} .bg { background: radial-gradient(80% 60% at 50% 0%, var(--glow) 0%, transparent 60%); }`;
- expect(scanCssTextForRadialHalo(css)).toHaveLength(1);
- });
-});
-
-// ---------------------------------------------------------------------------
-// Hover-state contrast + color-mix parsing
-// ---------------------------------------------------------------------------
-
-describe('hover contrast + color-mix', () => {
- test('parseColorMix: mix with transparent keeps color, scales alpha', () => {
- const c = parseColorMix('color-mix(in oklab, rgb(230, 68, 37) 16%, transparent)');
- expect(c.r).toBe(230);
- expect(c.g).toBe(68);
- expect(c.b).toBe(37);
- expect(c.a).toBeCloseTo(0.16, 2);
- });
-
- test('parseColorMix: 50/50 opaque mix averages channels', () => {
- const c = parseColorMix('color-mix(in srgb, rgb(0, 0, 0), rgb(255, 255, 255))');
- expect(c.a).toBe(1);
- expect(Math.abs(c.r - 128)).toBeLessThanOrEqual(1);
- });
-
- test('parseAnyColor routes color-mix expressions', () => {
- const c = parseAnyColor('color-mix(in oklab, oklch(0.625 0.205 33) 16%, transparent)');
- expect(c).not.toBeNull();
- expect(c.a).toBeCloseTo(0.16, 2);
- });
-
- // Every expected value below is what Chrome itself paints for that string
- // (read back from a 1x1 canvas), so the parser is pinned to the browser it
- // has to agree with rather than to my arithmetic.
- describe('parseAnyColor — the color syntaxes a browser reports verbatim', () => {
- const cases = [
- ['oklch(0.84 0.19 80.46)', [255, 186, 0]],
- ['oklch(84% 0.19 80.46)', [255, 186, 0]],
- ['oklch(1 0 0)', [255, 255, 255]],
- ['oklch(0 0 0)', [0, 0, 0]],
- // Chroma far outside the sRGB gamut must clamp, never produce NaN.
- ['oklch(0.62 0.4 30)', [255, 0, 0]],
- ['color(srgb 0.1 0.11 0.12)', [26, 28, 31]],
- // Chrome's serialization of a color-mix in srgb routinely lands outside
- // 0..1 on one or more channels.
- ['color(srgb 1.04084 0.728032 -0.213551)', [255, 186, 0]],
- ['color(srgb-linear 0.5 0.5 0.5)', [188, 188, 188]],
- ['color(display-p3 0.9 0.8 0.2)', [235, 203, 0]],
- ['color(display-p3 1 0 0)', [255, 0, 0]],
- ['lch(20 5 60)', [54, 47, 42]],
- ['lab(50 40 -30)', [165, 91, 171]],
- ['lab(100 0 0)', [255, 255, 255]],
- ['lab(0 0 0)', [0, 0, 0]],
- ];
- for (const [input, [r, g, b]] of cases) {
- test(`${input} -> rgb(${r}, ${g}, ${b})`, () => {
- const c = parseAnyColor(input);
- expect(c).not.toBeNull();
- expect(Math.abs(c.r - r)).toBeLessThanOrEqual(1);
- expect(Math.abs(c.g - g)).toBeLessThanOrEqual(1);
- expect(Math.abs(c.b - b)).toBeLessThanOrEqual(1);
- expect(c.a).toBe(1);
- });
- }
-
- test('carries the alpha channel through color() and lch()', () => {
- expect(parseAnyColor('color(srgb 0.1 0.11 0.12 / 0.4)').a).toBeCloseTo(0.4, 3);
- expect(parseAnyColor('lch(20 5 60 / 25%)').a).toBeCloseTo(0.25, 3);
- });
-
- test('returns null for color spaces it does not model, so callers abstain', () => {
- expect(parseAnyColor('color(rec2020 0.5 0.2 0.1)')).toBeNull();
- expect(parseAnyColor('color(--custom-profile 0.2 0.3 0.4)')).toBeNull();
- });
- });
-
- test('parseGradientColors reads stops written in modern color syntax', () => {
- const stops = parseGradientColors('linear-gradient(oklch(0.07 0.006 95), oklch(0.11 0.008 95))');
- expect(stops).toHaveLength(2);
- expect(stops[0].r).toBeLessThan(10);
- expect(stops[1].r).toBeLessThan(20);
- });
-
- test('parseGradientColors ignores the interpolation-space hint', () => {
- const stops = parseGradientColors('linear-gradient(in oklab, rgb(0, 0, 0), rgb(255, 255, 255))');
- expect(stops).toHaveLength(2);
- });
-
- test('parseGradientColors resolves color-mix stops without leaking nested hex', () => {
- const stops = parseGradientColors('linear-gradient(135deg, color-mix(in srgb, #2d5a4a 92%, #000), color-mix(in srgb, #1a3d32 90%, #000))');
- expect(stops).toHaveLength(2);
- expect(stops[0]).toEqual({ r: 41, g: 83, b: 68, a: 1 });
- expect(stops[1]).toEqual({ r: 23, g: 55, b: 45, a: 1 });
- });
-
- test('parseGradientColors does not leak nested hex when color-mix has var()', () => {
- const stops = parseGradientColors('linear-gradient(135deg, color-mix(in srgb, var(--brand) 92%, #000), color-mix(in srgb, var(--brand-deep) 90%, #000))');
- expect(stops).toEqual([]);
- });
-
- test('parseGradientColors still collects sibling bare hex stops beside color-mix', () => {
- const stops = parseGradientColors('linear-gradient(color-mix(in srgb, #2d5a4a 92%, #000), #ffffff)');
- expect(stops).toHaveLength(2);
- expect(stops[0]).toEqual({ r: 41, g: 83, b: 68, a: 1 });
- expect(stops[1]).toEqual({ r: 255, g: 255, b: 255, a: 1 });
- });
-
- test('parseGradientColors still reads bare hex gradient stops', () => {
- const stops = parseGradientColors('linear-gradient(#2d5a4a, #000)');
- expect(stops).toHaveLength(2);
- expect(stops[0]).toEqual({ r: 45, g: 90, b: 74, a: 1 });
- expect(stops[1]).toEqual({ r: 0, g: 0, b: 0, a: 1 });
- });
-
- test('checkHoverContrast flags a failing hover pair on a styled control', () => {
- const f = checkHoverContrast({
- tag: 'a',
- textColor: { r: 239, g: 236, b: 233, a: 1 },
- bg: { r: 215, g: 56, b: 23, a: 1 },
- ownBgAlpha: 1,
- fontSize: 13.6,
- fontWeight: 500,
- hasDirectText: true,
- isEmojiOnly: false,
- });
- expect(f).toHaveLength(1);
- expect(f[0].id).toBe('low-contrast');
- expect(f[0].snippet).toContain(':hover');
- });
-
- test('checkHoverContrast skips plain links without their own background', () => {
- const f = checkHoverContrast({
- tag: 'a',
- textColor: { r: 120, g: 120, b: 120, a: 1 },
- bg: { r: 128, g: 128, b: 128, a: 1 },
- ownBgAlpha: null,
- fontSize: 16,
- fontWeight: 400,
- hasDirectText: true,
- isEmojiOnly: false,
- });
- expect(f).toHaveLength(0);
- });
-
- test('checkHoverContrast passes a compliant hover pair', () => {
- const f = checkHoverContrast({
- tag: 'a',
- textColor: { r: 255, g: 255, b: 255, a: 1 },
- bg: { r: 20, g: 20, b: 20, a: 1 },
- ownBgAlpha: 1,
- fontSize: 14,
- fontWeight: 500,
- hasDirectText: true,
- isEmojiOnly: false,
- });
- expect(f).toHaveLength(0);
- });
-});
-
-// ---------------------------------------------------------------------------
-// Auto-scrolling marquee
-// ---------------------------------------------------------------------------
-
-describe('marquee', () => {
- test('flags infinite percent-travel X loop (implicit start)', () => {
- const css = `
- .ticker-track { display: flex; width: max-content; animation: ticker 25s linear infinite; }
- @keyframes ticker { to { transform: translateX(-50%); } }
- `;
- const f = scanCssTextForMarquee(css);
- expect(f).toHaveLength(1);
- expect(f[0].id).toBe('marquee');
- expect(f[0].snippet).toContain('.ticker-track');
- });
-
- test('flags