Files
pbakaus_impeccable/tests/context-signals.test.mjs
T
Paul BakausandClaude Code ea098ceb96 Accept remote refs as diff bases; honor non-origin upstreams
Both review bots found real gaps in the first pass: candidates were
verified as local branch names only, so an origin/HEAD target with no
local checkout fell through, and stripOrigin() dropped upstreams on any
remote not named origin (fork workflows tracking upstream/release).

Candidates now carry a display name plus the revs to try in order: the
upstream's remote rev wins outright (it tracks the actual merge target,
so it beats a possibly stale local branch of the same name), origin/HEAD
tries the local branch then the remote-tracking ref, and the
conventional names each try local then origin/<name>. git.base keeps
reporting the friendly branch name while the diff runs against whichever
rev resolved. Two new failing-first tests: remote-only default branch,
and an upstream on a remote named upstream with no local base branch.

Prepared with AI assistance (Claude Code), directed by @pbakaus.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-07-25 20:17:55 -07:00

348 lines
15 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(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('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', '<template><div/></template>\n');
write('.claude/skills/impeccable/scripts/detector.js', 'export const x = 1;\n');
git('add', '.');
git('commit', '-qm', 'init');
write('.vitepress/theme/Layout.vue', '<template><span/></template>\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('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`);
}
});
});