mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
* test: guard the plugin loader contract that PR #494 exposed The agents manifest key shipped for months and silently loaded zero of the four subagents; no validator looked at the generated plugin manifest's shape and claude plugin validate never checks it. Three layers now do: - scripts/lib/validate-plugin-manifest.js pins the verified loader contract (KNOWN_LOADER_KEYS allowlist, no agents key, trailing-slash skills path from issue #86, every skill/agents/*.md shipped in plugin/agents/), unit-tested in tests/validate-plugin-manifest.test.js including a check of the real committed subtree. - The same check gates bun run build next to the version-drift guard. - tests/plugin-e2e.test.mjs installs the committed ./plugin subtree into a real Claude Code (sandboxed via CLAUDE_CONFIG_DIR in a temp dir) and asserts the component inventory: skill parses, all agents visible, hooks discovered. In the default suite; runs in about a second and skips cleanly when the claude CLI is absent, so CI is unaffected. All three failed against the pre-#494 tree for the shipped reason (Agents 0 of 4) and pass against current main. AI-assisted via Claude Code under maintainer direction. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix: address PR review bot findings - Copilot: guard collectPluginManifestFindings against valid JSON that is not an object (null, string, number, array) so a broken manifest is a finding instead of a build crash; unit test added - Copilot: update the plugin-e2e header comment, the suite is in the default lineup rather than opt-in AI-assisted via Claude Code under maintainer direction. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix: harden plugin E2E sandbox isolation Bugbot: create the sandbox CLAUDE_CONFIG_DIR up front and redirect HOME and USERPROFILE into the temp workDir too, so a CLI code path that derives config or cache locations from the home directory instead of CLAUDE_CONFIG_DIR still cannot touch the developer's real Claude config when the default suite runs. AI-assisted via Claude Code under maintainer direction. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix: agent parity check mirrors the build's emit rules Bugbot: the shipped filename is `${claude-name || name}.md` and a providers: list may exclude claude-code, so comparing raw source basenames could fail the build on a renamed or provider-scoped agent with a build:release hint that cannot fix it. The validator now derives expected filenames the same way the transformer factory does (shared parseFrontmatter, same providers gate) with unit coverage for renames, name overrides, and provider-scoped agents. AI-assisted via Claude Code under maintainer direction. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix: run the plugin E2E through a shell on Windows Bugbot: the claude CLI is a .cmd shim on Windows and Node refuses to spawn those via execFile without a shell, so the availability probe always failed and the suite silently skipped there. Windows now invokes through a shell with every argument double-quoted (temp paths routinely contain spaces); the POSIX path is unchanged. AI-assisted via Claude Code under maintainer direction. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com>
393 lines
13 KiB
JavaScript
393 lines
13 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
export const DEFAULT_SUITES = ['core', 'detector', 'live', 'framework', 'plugin-e2e'];
|
|
export const OPT_IN_SUITES = [
|
|
'cli-remote-e2e',
|
|
'live-e2e',
|
|
'live-e2e-accept-cleanup',
|
|
'new-work-e2e',
|
|
'skill-behavior',
|
|
'live-svelte-adapter-deepseek',
|
|
];
|
|
|
|
const COMMON_INFRA_PATTERNS = [
|
|
/^package\.json$/,
|
|
/^bun\.lock$/,
|
|
/^scripts\/run-tests\.mjs$/,
|
|
/^scripts\/test-suites\.mjs$/,
|
|
/^scripts\/ci-test-plan\.mjs$/,
|
|
/^\.github\/workflows\/ci\.yml$/,
|
|
];
|
|
|
|
export const SUITES = {
|
|
core: {
|
|
description: 'Build, provider transforms, CLI helpers, context, and storage unit tests.',
|
|
triggers: [
|
|
...COMMON_INFRA_PATTERNS,
|
|
/^scripts\/(?!benchmark-detector|build-browser-detector|build-extension)/,
|
|
/^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|concept-seed|context|context-signals|critique-storage|design-parser|doctor|hook|impeccable-paths|is-generated|lib\/(artifact-schema|composition-catalog|concept-catalog|provider|staleness|staleness-deep|staleness-notice|surface-briefs|target-slug|template-extensions)|pin|surface-brief))/,
|
|
/^README(\.npm)?\.md$/,
|
|
/^cli\/bin\//,
|
|
/^tests\/(build|cleanup-deprecated|cli-args|cli-ignores|concept-seed|context|context-signals|critique-storage|design-parser|doctor|github-sheriff|hook|hook-build|impeccable-paths|openai-plugin|pin|skills-cli|staleness|surface-brief|target-args|template-extensions|test-suites|windows-path-fix|zip)\.test\.(js|mjs)$/,
|
|
/^tests\/lib\//,
|
|
],
|
|
commands: [
|
|
{
|
|
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',
|
|
],
|
|
},
|
|
{
|
|
runner: 'node',
|
|
files: [
|
|
'tests/ci-test-plan.test.mjs',
|
|
'tests/cli-args.test.mjs',
|
|
'tests/concept-seed.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/hook-build.test.mjs',
|
|
'tests/hook.test.mjs',
|
|
'tests/impeccable-paths.test.mjs',
|
|
'tests/openai-plugin.test.mjs',
|
|
'tests/pin.test.mjs',
|
|
'tests/doctor.test.mjs',
|
|
'tests/staleness.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',
|
|
],
|
|
},
|
|
],
|
|
},
|
|
detector: {
|
|
description: 'Anti-pattern detector tests across text, jsdom fixtures, and Puppeteer browser paths.',
|
|
needsPuppeteer: true,
|
|
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\/design-system\.test\.mjs$/,
|
|
/^tests\/(detect-antipatterns|detect-cli-design-contamination|detect-url-launch|inline-ignores|extension-build|fixtures\/antipatterns)/,
|
|
],
|
|
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',
|
|
],
|
|
},
|
|
],
|
|
},
|
|
live: {
|
|
description: 'Fast live-mode unit and local-server integration tests, excluding full browser fixture sweeps.',
|
|
triggers: [
|
|
...COMMON_INFRA_PATTERNS,
|
|
/^skill\/(reference\/live\.md|scripts\/(detect-csp|lib\/is-generated|lib\/template-extensions|live\/|live|live-|modern-screenshot|pin|palette))/,
|
|
/^tests\/live-/,
|
|
/^tests\/live-e2e\/(agent|agents\/llm-agent|cli-options|preactions|session|steer|ui)\.mjs$/,
|
|
/^tests\/live-e2e\/agent-insert\.test\.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-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-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-tanstack-adapter.test.mjs',
|
|
'tests/live-target-context.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.',
|
|
triggers: [
|
|
...COMMON_INFRA_PATTERNS,
|
|
/^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\//,
|
|
],
|
|
commands: [
|
|
{
|
|
runner: 'node',
|
|
files: ['tests/framework-fixtures.test.mjs'],
|
|
},
|
|
],
|
|
},
|
|
'cli-e2e': {
|
|
description: 'Deterministic CLI install/update tests against a local universal bundle.',
|
|
commands: [
|
|
{
|
|
runner: 'bun',
|
|
files: ['tests/skills-cli.test.js'],
|
|
},
|
|
],
|
|
},
|
|
'cli-remote-e2e': {
|
|
description: 'Remote CLI install/update smoke tests against impeccable.style.',
|
|
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'],
|
|
},
|
|
],
|
|
},
|
|
'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.',
|
|
triggers: [
|
|
...COMMON_INFRA_PATTERNS,
|
|
/^plugin\//,
|
|
/^skill\/agents\//,
|
|
/^scripts\/build\.js$/,
|
|
/^scripts\/lib\/validate-plugin-manifest\.js$/,
|
|
/^tests\/plugin-e2e\.test\.mjs$/,
|
|
],
|
|
commands: [
|
|
{
|
|
runner: 'node',
|
|
timeoutMs: 300000,
|
|
forceExit: true,
|
|
files: ['tests/plugin-e2e.test.mjs'],
|
|
},
|
|
],
|
|
},
|
|
'live-e2e': {
|
|
description: 'Full Playwright live-mode click-to-accept sweep across runtime framework fixtures.',
|
|
optIn: true,
|
|
needsPlaywright: true,
|
|
triggers: [
|
|
...COMMON_INFRA_PATTERNS,
|
|
/^skill\/scripts\/live/,
|
|
/^tests\/framework-fixtures/,
|
|
/^tests\/live-e2e(\.test\.mjs|\/)/,
|
|
],
|
|
commands: [
|
|
{
|
|
runner: 'node',
|
|
timeoutMs: 600000,
|
|
forceExit: true,
|
|
files: ['tests/live-e2e.test.mjs'],
|
|
},
|
|
],
|
|
},
|
|
'new-work-e2e': {
|
|
description: 'Playwright smoke sweep of the new-work concept/serve-question decision page plus the offline fake image generator.',
|
|
optIn: true,
|
|
needsPlaywright: true,
|
|
triggers: [
|
|
...COMMON_INFRA_PATTERNS,
|
|
/^skill\/scripts\/(serve-question|generate-image|concept-seed)\.mjs$/,
|
|
/^tests\/new-work-e2e(\.test\.mjs|\/)/,
|
|
],
|
|
commands: [
|
|
{
|
|
runner: 'node',
|
|
timeoutMs: 600000,
|
|
forceExit: true,
|
|
files: ['tests/new-work-e2e.test.mjs'],
|
|
},
|
|
],
|
|
},
|
|
'live-e2e-accept-cleanup': {
|
|
description: 'Provider-backed post-accept cleanup regression.',
|
|
optIn: true,
|
|
needsPlaywright: true,
|
|
triggers: [
|
|
...COMMON_INFRA_PATTERNS,
|
|
/^skill\/scripts\/(live-accept|live-browser|live-server|live-wrap)\.mjs$/,
|
|
/^skill\/scripts\/live\/sveltekit-adapter\.mjs$/,
|
|
/^tests\/live-e2e-accept-cleanup-regression\.test\.mjs$/,
|
|
/^tests\/live-e2e\//,
|
|
],
|
|
commands: [
|
|
{
|
|
runner: 'node',
|
|
timeoutMs: 600000,
|
|
files: ['tests/live-e2e-accept-cleanup-regression.test.mjs'],
|
|
},
|
|
],
|
|
},
|
|
'live-e2e-agent': {
|
|
description: 'Focused insert-mode fake-agent helper tests.',
|
|
commands: [
|
|
{
|
|
runner: 'node',
|
|
files: ['tests/live-e2e/agent-insert.test.mjs'],
|
|
},
|
|
],
|
|
},
|
|
'skill-behavior': {
|
|
description: 'LLM-backed skill setup behavior scenarios.',
|
|
optIn: true,
|
|
triggers: [
|
|
...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$/,
|
|
/^tests\/skill-behavior\//,
|
|
],
|
|
commands: [
|
|
{
|
|
runner: 'node',
|
|
timeoutMs: 300000,
|
|
files: [
|
|
'tests/skill-behavior/scenarios.test.mjs',
|
|
'tests/skill-behavior/workflow-contract.test.mjs',
|
|
],
|
|
},
|
|
],
|
|
},
|
|
'live-svelte-adapter-deepseek': {
|
|
description: 'DeepSeek-backed Svelte adapter browser sweep.',
|
|
optIn: true,
|
|
needsPlaywright: true,
|
|
triggers: [
|
|
...COMMON_INFRA_PATTERNS,
|
|
/^skill\/scripts\/(live-server|live-wrap)\.mjs$/,
|
|
/^skill\/scripts\/live\/(sveltekit-adapter|svelte-component)\.mjs$/,
|
|
/^tests\/framework-fixtures\/vite8-sveltekit-stateful\//,
|
|
/^tests\/live-svelte-adapter-deepseek\.test\.mjs$/,
|
|
],
|
|
commands: [
|
|
{
|
|
runner: 'node',
|
|
timeoutMs: 1200000,
|
|
files: ['tests/live-svelte-adapter-deepseek.test.mjs'],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
export function expandSuites(requested) {
|
|
const names = requested.length === 0 ? ['default'] : requested;
|
|
const expanded = [];
|
|
for (const name of names) {
|
|
if (name === 'default' || name === 'all-local') {
|
|
expanded.push(...DEFAULT_SUITES);
|
|
} else if (name === 'all') {
|
|
expanded.push(...DEFAULT_SUITES, ...OPT_IN_SUITES);
|
|
} else if (SUITES[name]) {
|
|
expanded.push(name);
|
|
} else {
|
|
throw new Error(`Unknown test suite "${name}". Run: node scripts/run-tests.mjs --list`);
|
|
}
|
|
}
|
|
return [...new Set(expanded)];
|
|
}
|
|
|
|
export function suiteFiles(suiteNames) {
|
|
const files = [];
|
|
for (const name of suiteNames) {
|
|
const suite = SUITES[name];
|
|
if (!suite) throw new Error(`Unknown test suite "${name}"`);
|
|
for (const command of suite.commands) {
|
|
files.push(...command.files);
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
export function findTestFiles(root = process.cwd()) {
|
|
const out = [];
|
|
const stack = [path.join(root, 'tests')];
|
|
while (stack.length) {
|
|
const dir = stack.pop();
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const abs = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
stack.push(abs);
|
|
} else if (/\.test\.(js|mjs)$/.test(entry.name)) {
|
|
out.push(path.relative(root, abs).split(path.sep).join('/'));
|
|
}
|
|
}
|
|
}
|
|
return out.sort();
|
|
}
|
|
|
|
export function matchesSuiteTriggers(suiteName, changedFiles) {
|
|
const suite = SUITES[suiteName];
|
|
if (!suite) throw new Error(`Unknown test suite "${suiteName}"`);
|
|
return changedFiles.some((file) => suite.triggers?.some((pattern) => pattern.test(file)));
|
|
}
|