mirror of
https://github.com/garrytan/gstack.git
synced 2026-08-31 18:30:39 +02:00
- 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>
163 lines
6.7 KiB
TypeScript
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');
|
|
});
|
|
});
|