// watch-cron-scope.test.mjs — `watch-cron.mjs` must scan each watched project // as that project's own working tree (v8.1.3, PLAN § v8.1.3 punkt 5). // // Since v8.1.0 a project's .llm-security-ignore and .llm-security/policy.json // are honored only when the target is the caller's own working tree (at or // below cwd, same git root). watch-cron.mjs started the orchestrator with // `cwd: PLUGIN_ROOT`, so every watched project was outside cwd and its own // suppressions were dropped: the user was shown findings they had already // suppressed. v8.1.3 runs the orchestrator with the project dir as cwd. // // (a) a watched project WITH a `**` ignore file reports the same counts as // the orchestrator run from inside that project (its config honored); // (b) a sibling WITHOUT the ignore file reports findings (known-positive: // keeps (a) from passing on a fixture that produces nothing). // Fixtures under $HOME: outside os.tmpdir() (always foreign) and this repo. // watch-cron writes PLUGIN_ROOT/reports/watch/latest.json; the test restores // whatever was there before. import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; import { resolve, dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { spawnSync } from 'node:child_process'; import { mkdirSync, writeFileSync, readFileSync, rmSync, existsSync } from 'node:fs'; import crypto from 'node:crypto'; import { mkOwnTreeDir } from '../helpers/own-tree.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const PLUGIN_ROOT = resolve(__dirname, '../..'); const WATCH_CRON = join(PLUGIN_ROOT, 'scanners', 'watch-cron.mjs'); const ORCHESTRATOR = join(PLUGIN_ROOT, 'scanners', 'scan-orchestrator.mjs'); const LATEST = join(PLUGIN_ROOT, 'reports', 'watch', 'latest.json'); // Not a real credential — a known-positive blob for the entropy scanner only. // Fixed, not random: a random blob starts with `/` one time in 64, and the // entropy scanner skips a string that starts with `/` as a path. One run of // this test with a random blob reported zero findings for both projects; that // cause is the probable one, not a proven one (the blob was not logged). const HIGH_ENTROPY_BLOB = Buffer.concat([ crypto.createHash('sha512').update('watch-cron-scope-1').digest(), crypto.createHash('sha512').update('watch-cron-scope-2').digest(), ]).subarray(0, 72).toString('base64'); function writeProject(dir, { withIgnore }) { mkdirSync(dir, { recursive: true }); const r = spawnSync('git', ['init', '-q', dir], { encoding: 'utf8' }); assert.equal(r.status, 0, `git init failed: ${r.stderr}`); writeFileSync(join(dir, 'config.js'), `const payload = "${HIGH_ENTROPY_BLOB}";\nmodule.exports = { payload };\n`); if (withIgnore) writeFileSync(join(dir, '.llm-security-ignore'), '**\n'); } const total = (counts) => Object.values(counts || {}).reduce((a, b) => a + b, 0); describe('watch-cron: a watched project is scanned as its own working tree', () => { let root; let savedLatest = null; let watched; let direct; before(() => { root = mkOwnTreeDir('watch-cron-'); writeProject(join(root, 'suppressed'), { withIgnore: true }); writeProject(join(root, 'plain'), { withIgnore: false }); const config = join(root, 'watch-config.json'); writeFileSync(config, JSON.stringify({ targets: [ { path: join(root, 'suppressed'), label: 'suppressed' }, { path: join(root, 'plain'), label: 'plain' }, ], options: { baseline: false, saveBaseline: false }, })); if (existsSync(LATEST)) savedLatest = readFileSync(LATEST); const run = spawnSync(process.execPath, [WATCH_CRON, '--config', config], { cwd: root, encoding: 'utf8', timeout: 600_000, }); assert.ok(run.status !== null, `watch-cron did not finish: ${run.error?.message}`); watched = JSON.parse(readFileSync(LATEST, 'utf8')); // Reference: the orchestrator run from inside the project (own tree). const ref = spawnSync(process.execPath, [ORCHESTRATOR, '.'], { cwd: join(root, 'suppressed'), encoding: 'utf8', timeout: 300_000, maxBuffer: 64 * 1024 * 1024, }); direct = JSON.parse(ref.stdout).aggregate; }); after(() => { if (savedLatest !== null) writeFileSync(LATEST, savedLatest); else rmSync(LATEST, { force: true }); rmSync(root, { recursive: true, force: true }); }); it('(b) the project without an ignore file reports findings (known-positive)', () => { const plain = watched.targets.find(t => t.label === 'plain'); assert.equal(plain.error, null, `plain target errored: ${plain.error}`); assert.ok(total(plain.counts) > 0, `expected findings, got ${JSON.stringify(plain.counts)}`); }); it('(a) the project\'s own ignore file is honored (same counts as a run from inside it)', () => { const suppressed = watched.targets.find(t => t.label === 'suppressed'); assert.equal(suppressed.error, null, `suppressed target errored: ${suppressed.error}`); assert.deepEqual(suppressed.counts, direct.counts); const plain = watched.targets.find(t => t.label === 'plain'); assert.ok(total(suppressed.counts) < total(plain.counts), `ignore file had no effect: ${JSON.stringify(suppressed.counts)} vs ${JSON.stringify(plain.counts)}`); }); });