mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
* extension: kinpaku UI for the popup, DevTools panel, sidebar and overlays The extension still wore magenta (#d6336c on the toolbar badge, oklch(48% 0.25 350) through the DevTools panel and the Elements sidebar) and a cream popup left over from the old dark system. It now wears the brand the site ships in impeccable-site PR #34: paper and instruments, one light theme. - extension/shared/kinpaku.css is the one token layer the three pages link. Values are copied from site/styles/kinpaku-tokens.css, and the header states the four rules that shape the rest: paper and ink, gold as jewelry, patina carries colored text, a dark surface is a control. - Severity has one language. An AI tell gets the detector's gold tag and a lit gold dot; a quality issue gets patina; a scan that did not run gets vermilion. Selectors and details are patina-deep, which clears 4.5:1 on paper, and gold never carries text anywhere. - Popup: neutral paper, the mark as a carved ink tile, an ink primary button and a paper cap for the secondary, and the count in ink over a rule that lights gold when there is something to report. - Panel and sidebar: paper in both DevTools themes, since the brand has no dark theme any more. The panel handles the seam instead of inverting, with a hard top edge under .theme-dark. Segmented controls and the switch are paper hardware: a recessed track, a raised cap, a lit gold dot. - Overlays in the page: a gold hairline plus a soft outer glow instead of a 2px outline, and the label chip is now the tag, ink on gold in the mono face, matching what live mode draws. - The toolbar badge is gold with dark ink text (about 11.8:1); Chrome's default white badge text does not clear 4.5:1 on gold. Behavior is untouched: every id, class hook and message the popup, panel, sidebar and content script depend on is unchanged, and the one markup change beyond the stylesheet links is a severity class on the sidebar's kind label. The extension smoke suite passes on all nine fixtures with no service worker or offscreen errors, `bun run test` is green with a local engine build, and `web-ext lint` reports the same two Firefox-only errors and the same fifteen warnings as origin/main. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * extension: guard setBadgeTextColor, and make the dark seam paint-only Two review findings from #729. setBadgeTextColor arrived in Chrome 110 and does not exist on Firefox's action API. `chrome.action.setBadgeTextColor?.({...}).catch(...)` still evaluates `.catch` on the undefined the optional call returns, so on a browser without the method updateBadge raised a TypeError that escaped into whatever asked for the badge update. Both the method and its return value are checked now, and tests/extension-build.test.mjs pins the shape: no `?.(...).catch`, an existence check around the call, a promise check before `.catch`, and the gold and ink values with the magenta one gone. The dark-DevTools seam is a fixed 2px line at the top of the sheet, above the sticky toolbar because that is what keeps it in place while the panel scrolls under it. It now sets pointer-events: none, so it is paint and nothing else and the toolbar's top row of pixels stays clickable. extension/shared/ joins the detector suite's trigger list, since the shared stylesheet is part of extension packaging. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY * overlay: the page banner is a paper bar, not a gold fill The last place gold was still upholstery. A full-width gold bar across someone else's page is a fill, and the system holds gold to a mark, a line, a lit dot, or a tag. The banner is now paper with ink text, a gold hairline along its bottom edge, and the detector's lit gold dot at its head, the same indicator the panel puts on a section. Each page-level finding wears the tag the panel gives it: gold with ink for an AI tell, a quiet gray chip for a quality issue, with the detail beside it in ink rather than folded into the chip. The bar carries a shadow of its own so it still separates from a dark host page, and the standalone controls take muted ink now that they sit on paper instead of gold. crates/live/assets/detect-antipatterns-browser.js is the regenerated tracked artifact, so live mode and the site's detector get the same banner. Extension smoke: all nine fixtures pass, no service worker or offscreen errors. bun run test green with a local engine build. web-ext lint reports the same two Firefox-only errors and fifteen warnings as origin/main. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY --------- Co-authored-by: Claude Code <noreply@anthropic.com>
386 lines
13 KiB
JavaScript
386 lines
13 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
export const DEFAULT_SUITES = ['core', 'oracle', '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$/,
|
|
/^scripts\/lib\/(live-server-processes|process-group|test-orphan-reaper)\.mjs$/,
|
|
/^tests\/lib\/live-servers\.mjs$/,
|
|
/^scripts\/lib\/(live-server-processes|process-group|test-orphan-reaper)\.mjs$/,
|
|
/^tests\/lib\/live-servers\.mjs$/,
|
|
/^\.github\/workflows\/ci\.yml$/,
|
|
];
|
|
|
|
export const SUITES = {
|
|
core: {
|
|
description: 'Build, provider transforms, hook manifests, plugin validators, and prose gates.',
|
|
triggers: [
|
|
...COMMON_INFRA_PATTERNS,
|
|
/^scripts\/(?!build-extension)/,
|
|
/^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/)/,
|
|
/^ENGINE_VERSION$/,
|
|
/^README(\.npm)?\.md$/,
|
|
/^cli\/bin\//,
|
|
],
|
|
commands: [
|
|
{
|
|
runner: 'bun',
|
|
files: [
|
|
'tests/build.test.js',
|
|
'tests/lib/provider-blocks.test.js',
|
|
'tests/lib/transformers/provider-blocks.test.js',
|
|
'tests/lib/utils.test.js',
|
|
'tests/lib/transformers/factory.test.js',
|
|
'tests/lib/transformers/opencode-commands.test.js',
|
|
'tests/lib/transformers/providers.test.js',
|
|
'tests/root-commands-sync.test.js',
|
|
'tests/validate-plugin-versions.test.js',
|
|
'tests/validate-plugin-manifest.test.js',
|
|
'tests/plugin-paths.test.js',
|
|
],
|
|
},
|
|
{
|
|
runner: 'node',
|
|
// A finite per-test cap so an async hang is cancelled and reported
|
|
// rather than left running with `--test-timeout` unset (Infinity).
|
|
// Note: this timer lives in the event loop, so it cannot interrupt a
|
|
// test blocked in a synchronous spawnSync; the runner's wall-clock
|
|
// group-kill covers that case. The slowest core test is ~11s, so 180s
|
|
// is safe.
|
|
timeoutMs: 180000,
|
|
files: [
|
|
'tests/ci-test-plan.test.mjs',
|
|
'tests/cli-shim.test.mjs',
|
|
'tests/publish-platform-packages.test.mjs',
|
|
'tests/github-sheriff.test.mjs',
|
|
'tests/hook-build.test.mjs',
|
|
'tests/openai-plugin.test.mjs',
|
|
'tests/process-group.test.mjs',
|
|
'tests/release.test.mjs',
|
|
'tests/skill-reference.test.mjs',
|
|
'tests/readme-gitignore.test.mjs',
|
|
'tests/test-suites.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: 'Extension packaging checks (the rule logic itself is covered by the crate tests and the oracle).',
|
|
triggers: [
|
|
...COMMON_INFRA_PATTERNS,
|
|
/^extension\/(background|content|detector|devtools|offscreen|popup|shared|manifest\.json)/,
|
|
/^scripts\/build-extension\.js$/,
|
|
/^browser-bundle\//,
|
|
// Everything `cargo xtask bundle` reads: the rules and the registry
|
|
// rows (core, foundation), the wasm module (wasm), the assembly and the
|
|
// registry serialization (bundle), and the task itself (xtask). Leaving
|
|
// one out means a PR that changes what the bundle emits never rebuilds
|
|
// it, and the tracked-output check in ci.yml then compares a committed
|
|
// artifact against an untouched tree and passes on stale bytes.
|
|
/^crates\/(bundle|core|foundation|wasm|xtask)\//,
|
|
// The tracked artifacts themselves, so a hand-edit is regenerated over.
|
|
/^crates\/live\/assets\//,
|
|
],
|
|
commands: [
|
|
{
|
|
runner: 'node',
|
|
files: ['tests/extension-build.test.mjs'],
|
|
},
|
|
],
|
|
},
|
|
live: {
|
|
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,
|
|
/^skill\/(reference\/live\.md|scripts\/live-browser)/,
|
|
/^tests\/live-e2e\//,
|
|
/^tests\/lib\/engine-bin\.mjs$/,
|
|
],
|
|
commands: [
|
|
{
|
|
runner: 'node',
|
|
files: [
|
|
'tests/live-reference.test.mjs',
|
|
'tests/live-browser-ignores.test.mjs',
|
|
'tests/live-browser-source.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-server-leak.test.mjs',
|
|
],
|
|
},
|
|
],
|
|
},
|
|
framework: {
|
|
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$/,
|
|
/^tests\/lib\/engine-bin\.mjs$/,
|
|
],
|
|
commands: [
|
|
{
|
|
runner: 'node',
|
|
files: ['tests/framework-fixtures.test.mjs'],
|
|
},
|
|
],
|
|
},
|
|
// `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 (moved to the engine repo; no tests here).',
|
|
optIn: true,
|
|
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.',
|
|
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-browser/,
|
|
/^ENGINE_VERSION$/,
|
|
/^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,
|
|
/^ENGINE_VERSION$/,
|
|
/^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,
|
|
/^ENGINE_VERSION$/,
|
|
/^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$/,
|
|
/^ENGINE_VERSION$/,
|
|
/^tests\/skill-behavior\//,
|
|
],
|
|
commands: [
|
|
{
|
|
runner: 'node',
|
|
// 300000 was too low to measure what these scenarios assert. The
|
|
// workflow-contract turns run 20+ steps against a frontier model, and
|
|
// the *correct* path is the slow one: a run that stops to put the
|
|
// concept to the user before building was measured at 579s, while the
|
|
// runs that skipped that checkpoint and failed the assertion finished
|
|
// in 130-200s. At a 300s cap the thorough path is killed and the hasty
|
|
// path is graded, so the cap was selecting for the behavior the suite
|
|
// exists to forbid.
|
|
timeoutMs: 900000,
|
|
// Overall wall-clock safety cap for the whole sweep: if a provider
|
|
// call wedges past every inner guard (the harness's 840s per-turn
|
|
// AbortSignal and the 900s per-test timeout), the runner SIGKILLs the
|
|
// process group so the sweep still ends with a per-provider tally
|
|
// instead of hanging overnight. Sized well above a healthy two-provider
|
|
// sweep; override with IMPECCABLE_TEST_WALL_CLOCK_MS to scope it down.
|
|
wallClockMs: 3_600_000,
|
|
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,
|
|
/^ENGINE_VERSION$/,
|
|
/^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'],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
function escapeRegExp(value) {
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
// Every suite must select itself when one of its own test files changes.
|
|
// Generated from the files lists so the hand-written trigger patterns above
|
|
// only carry source paths and fixture directories; before this, four test
|
|
// files were registered in a suite that change-based CI could never select
|
|
// by editing them (serve-question, ci-test-plan, both validate-plugin-*),
|
|
// and tests/lib/detector-bundle.test.js triggered core while running in
|
|
// detector. The meta-test in tests/test-suites.test.mjs pins this invariant.
|
|
for (const suite of Object.values(SUITES)) {
|
|
const ownFiles = suite.commands.flatMap((command) => command.files);
|
|
suite.triggers = [
|
|
...(suite.triggers ?? []),
|
|
...ownFiles.map((file) => new RegExp(`^${escapeRegExp(file)}$`)),
|
|
];
|
|
}
|
|
|
|
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)));
|
|
}
|