Files
gstack/test/slop-diff-cli.test.ts
T
Garry TanandClaude Fable 5 1055561cae test: coverage fill — 95 tests for six zero-coverage surfaces
- eval CLI family (eval-list/compare/summary + eval-select smoke): the
  primary interface to eval results had no tests; isolation via a fake
  gstack-slug under a mkdtemp HOME (the scripts' real resolution path —
  they do NOT honor GSTACK_EVAL_DIR; only EvalCollector does). Pinned
  current behavior: eval-list does NOT exclude _partial runs (documented
  improvement candidate)
- slop-diff (runs on every /review + quality-gate): fixture git repo +
  first-on-PATH npx stub (never downloads real slop-scan); no-diff
  early exit, missing-scanner fallback, fingerprint line-insensitivity,
  merge-base worktree scan
- bin/gstack-code-intelligence CLI arg surface (lib was covered, the
  284-line CLI wasn't): select/consent/suggest/index/search gating;
  pinned: --help routes to usage failure exit 1 (no handler)
- browse media-extract: the page.evaluate callback exercised in-process
  against a mock DOM (no exports added) — lazy-src fallback chain,
  HLS/DASH detection, bg-image url() parsing, 500-element cap
- browse session-cookie-store: factory contract (cookieName/ttlMs/
  maxSessions eviction, cross-store isolation, mint→validate
  round-trip); store is in-memory — no fs cases exist
- lib/version-source direct unit tests (gstack-version-bump.test.ts
  spawns the bin, never imports the lib): parse/format/cmp/bump
  coercion, npm 4→3 translation, #2501 mangled-JSON regression class

All hermetic (mkdtemp homes, runBin child isolation); windows curation
correctly partitions the six.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 05:28:51 +00:00

163 lines
6.7 KiB
TypeScript

/**
* scripts/slop-diff.ts — new-findings-only slop report, run on every /review
* and quality gate.
*
* Isolation: every git call in the script inherits the child's cwd (no
* explicit cwd is passed to spawnSync), so pointing the CLI at a tiny fixture
* repo is just `cwd: fixtureRepo`. The `npx slop-scan` dependency is stubbed
* with a PATH-prepended fake so no test ever downloads or runs the real
* scanner — the stub also makes the "scanner missing", "invalid JSON", and
* "real findings" paths deterministic.
*/
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { runBin } from './helpers/run-bin';
const ROOT = path.resolve(import.meta.dir, '..');
const SLOP_DIFF = path.join(ROOT, 'scripts', 'slop-diff.ts');
let repo: string;
let stubDir: string;
function git(...args: string[]): void {
const result = runBin('git', args, { cwd: repo });
if (result.status !== 0) {
throw new Error(`git ${args.join(' ')} failed: ${result.stderr}`);
}
}
// POSIX-only on purpose: the npx stub is a shebang script, and Windows
// CreateProcess cannot exec shebangs (a PATH `npx` without .cmd would fall
// through to the REAL npx and try to download slop-scan). The quoted
// '/bin/bash' below is what the Windows-fragile content scanner in
// scripts/test-free-shards.ts keys on to exclude this file from the
// windows-safe subset.
const BASH = '/bin/bash';
/** Install a fake `npx` first on PATH. Body is a bash script fragment. */
function stubNpx(body: string): void {
fs.writeFileSync(path.join(stubDir, 'npx'), `#!${BASH}\n${body}\n`, { mode: 0o755 });
}
function runSlopDiff(...args: string[]) {
return runBin('bun', [SLOP_DIFF, ...args], {
cwd: repo,
env: { PATH: `${stubDir}:${process.env.PATH}` },
// Two scans + a worktree add/remove; generous but bounded.
timeoutMs: 90_000,
});
}
beforeEach(() => {
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'slop-diff-repo-'));
stubDir = fs.mkdtempSync(path.join(os.tmpdir(), 'slop-diff-npx-'));
git('-c', 'init.defaultBranch=main', 'init', '-q');
git('config', 'user.email', 'fixture@example.com');
git('config', 'user.name', 'Fixture');
fs.writeFileSync(path.join(repo, 'README.md'), '# fixture\n');
git('add', 'README.md');
git('commit', '-q', '-m', 'initial');
// A default stub so no test path can ever reach a real npx/network.
stubNpx('exit 1');
});
afterEach(() => {
fs.rmSync(repo, { recursive: true, force: true });
fs.rmSync(stubDir, { recursive: true, force: true });
});
/** Commit a changed file on a feature branch so `main...HEAD` is non-empty. */
function commitFeatureChange(): void {
git('checkout', '-q', '-b', 'feature');
fs.mkdirSync(path.join(repo, 'src'), { recursive: true });
fs.writeFileSync(path.join(repo, 'src', 'app.ts'), 'export const x = 1;\n');
git('add', 'src/app.ts');
git('commit', '-q', '-m', 'feature change');
}
describe('slop:diff CLI (scripts/slop-diff.ts)', () => {
test('no changes vs the base branch: exits 0 without ever invoking the scanner', () => {
// HEAD == main → empty diff → early exit before any npx call. The stub
// exits 1, so if the scanner were invoked the output would differ.
const result = runSlopDiff();
expect(result.status).toBe(0);
expect(result.stdout).toContain('No files changed vs main');
expect(result.stdout).toContain('nothing to check');
});
test('missing slop-scan (npx produces no output): graceful message, exit 0', () => {
commitFeatureChange();
// Default stub: exit 1, no stdout → the script's fallback path.
const result = runSlopDiff();
expect(result.status).toBe(0);
expect(result.stdout).toContain('slop-scan not available');
expect(result.stdout).toContain('npm i -g slop-scan');
});
test('scanner emitting invalid JSON: graceful message, exit 0', () => {
commitFeatureChange();
stubNpx('echo "this is not json"');
const result = runSlopDiff();
expect(result.status).toBe(0);
expect(result.stdout).toContain('slop-scan returned invalid JSON');
});
test('reports only NEW findings in changed files, diffed against the merge-base scan', () => {
commitFeatureChange();
// The stub is invoked twice: `npx slop-scan scan . --json` for HEAD and
// `npx slop-scan scan <tmp-worktree> --json` for the merge-base. Branch on
// the scan target ($3): HEAD gets one finding in the changed file plus one
// in an UNCHANGED file (which must be filtered out); the base gets none.
stubNpx([
'if [ "$3" = "." ]; then',
` echo '{"findings":[`
+ `{"ruleId":"empty-catch","path":"src/app.ts","evidence":["line 3: empty catch, boundary=none"]},`
+ `{"ruleId":"empty-catch","path":"README.md","evidence":["line 1: empty catch, boundary=none"]}`
+ `]}'`,
'else',
' echo \'{"findings":[]}\'',
'fi',
].join('\n'));
const result = runSlopDiff();
expect(result.status).toBe(0);
expect(result.stdout).toContain('1 new findings');
expect(result.stdout).toContain('src/app.ts');
expect(result.stdout).toContain('empty-catch');
expect(result.stdout).toContain('line 3: empty catch, boundary=none');
// README.md was not part of the branch diff — its finding is not "new".
expect(result.stdout).not.toContain('README.md');
expect(result.stdout).toContain('Net: +1 new, -0 removed');
});
test('a finding present at the merge-base is not new, even when line numbers shift', () => {
commitFeatureChange();
// Same (rule, file, evidence-modulo-line-number) on both sides: HEAD says
// line 42, base says line 3 — the line-number-insensitive fingerprint must
// treat them as the same finding.
stubNpx([
'if [ "$3" = "." ]; then',
' echo \'{"findings":[{"ruleId":"empty-catch","path":"src/app.ts","evidence":["line 42: empty catch, boundary=none"]}]}\'',
'else',
// The base scan sees worktree-absolute paths; the script remaps them by
// stripping the worktree prefix, so emit the path under the scan target.
' echo "{\\"findings\\":[{\\"ruleId\\":\\"empty-catch\\",\\"path\\":\\"$3/src/app.ts\\",\\"evidence\\":[\\"line 3: empty catch, boundary=none\\"]}]}"',
'fi',
].join('\n'));
const result = runSlopDiff();
expect(result.status).toBe(0);
expect(result.stdout).toContain('no new findings');
});
test('an explicit base argument overrides main', () => {
// Diff feature...feature is empty even though feature differs from main.
commitFeatureChange();
const result = runSlopDiff('feature');
expect(result.status).toBe(0);
expect(result.stdout).toContain('No files changed vs feature');
});
});