mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Rework context-signals' detect target after review: a URL meant a costly Puppeteer render (and a probed port might not even be this project), and the index.html-or-bail fallback failed most real apps (no root index.html). New priority: (1) the scannable markup/style files in the dirty git tree (what the user is working on, small and local); (2) a local source dir (src / app / components / pages / public — the detector walks these and skips node_modules / dist / build); (3) a root index.html, else the project root as a last resort when there's code. Emits `scan.targets` (a list) + `scan.via`. Never a URL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
156 lines
6.0 KiB
JavaScript
156 lines
6.0 KiB
JavaScript
/**
|
|
* 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(s.setup.register, null);
|
|
assert.equal(s.setup.hasCode, false);
|
|
assert.equal(s.critique.latest, null);
|
|
});
|
|
|
|
it('detects PRODUCT.md, register, and code presence', async () => {
|
|
write('PRODUCT.md', '# Product\n\n## Register\n\nbrand\n');
|
|
write('package.json', '{"name":"x"}');
|
|
const s = await gatherSignals(scratch);
|
|
assert.equal(s.setup.hasProduct, true);
|
|
assert.equal(s.setup.register, 'brand');
|
|
assert.equal(s.setup.hasCode, true);
|
|
});
|
|
|
|
it('flags missing DESIGN.md when code exists', async () => {
|
|
write('PRODUCT.md', '# Product\n\n## Register\n\nproduct\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.register, 'product');
|
|
});
|
|
|
|
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('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('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`);
|
|
}
|
|
});
|
|
});
|