From 58b704977ae4b99dc6b5cd8a4a615827fc555ae7 Mon Sep 17 00:00:00 2001 From: haelyra <49814733+haelyra@users.noreply.github.com> Date: Sun, 11 Oct 2026 03:51:36 -0400 Subject: [PATCH] fix: correct late search credit and security test gates --- scripts/lib/gateguard-search-filters.js | 25 ++++++++++++++----- ...serve-secret-redaction-environment.test.js | 25 +++++++++++++++++++ tests/hooks/observe-secret-redaction.test.js | 10 +++++--- tests/lib/control-pane-proximity.test.js | 6 +++-- tests/lib/gateguard-search-evidence.test.js | 2 ++ tests/lib/gateguard-search-filters.test.js | 17 +++++++++++++ 6 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 tests/hooks/observe-secret-redaction-environment.test.js diff --git a/scripts/lib/gateguard-search-filters.js b/scripts/lib/gateguard-search-filters.js index 4d0cf4881..8a7eb5b5b 100644 --- a/scripts/lib/gateguard-search-filters.js +++ b/scripts/lib/gateguard-search-filters.js @@ -377,10 +377,10 @@ function grepToolFilters(glob, type) { const filters = { positives: [], includes: [], exclusions: [], opaque: false }; if (typeof type === 'string' && type) { const globs = rgTypeGlobs(type); - if (globs) filters.includes.push(...globs); + if (globs) filters.typeIncludes = globs; else filters.opaque = true; } - if (typeof glob !== 'string' || !glob) return filters; + if (typeof glob !== 'string' || !glob) return resolveTypeFilters(filters); for (const part of splitTopLevelCommas(glob)) { if (part.startsWith('!')) filters.exclusions.push(part.slice(1)); else { @@ -388,7 +388,18 @@ function grepToolFilters(glob, type) { filters.includes.push(part); } } - return filters; + return resolveTypeFilters(filters); +} + +function resolveTypeFilters({ typeIncludes = [], typeExclusions = [], ...filters }) { + // rg's explicit positive globs override type selection, not union with it. + // Keep type alternatives separate until every explicit glob is known. + const hasIncludes = filters.includes.length > 0; + return { + ...filters, + includes: hasIncludes ? filters.includes : typeIncludes, + exclusions: filters.exclusions.concat(hasIncludes ? [] : typeExclusions) + }; } function shellSearchFilters(kind, args) { @@ -401,7 +412,7 @@ function shellSearchFilters(kind, args) { const consumed = applyShellFilterArg(kind, args, i, filters); i += consumed; } - return filters; + return resolveTypeFilters(filters); } function applyShellFilterArg(kind, args, i, filters) { @@ -418,8 +429,10 @@ function applyShellFilterArg(kind, args, i, filters) { const type = (name, indexes, excluded) => { const globs = rgTypeGlobs(name); if (!globs) filters.opaque = true; - else if (excluded) globs.forEach(glob => exclude(glob, indexes)); - else globs.forEach(include); + else if (excluded) { + filters.typeExclusions = (filters.typeExclusions || []).concat(globs); + indexes.forEach(index => filters.dropped.add(index)); + } else filters.typeIncludes = (filters.typeIncludes || []).concat(globs); }; const extension = value => include(value ? `*.${String(value).replace(/^\./, '')}` : ''); if (kind.startsWith('git ') && GIT_EXCLUDE_PATHSPEC.test(arg)) { diff --git a/tests/hooks/observe-secret-redaction-environment.test.js b/tests/hooks/observe-secret-redaction-environment.test.js new file mode 100644 index 000000000..5610bc4aa --- /dev/null +++ b/tests/hooks/observe-secret-redaction-environment.test.js @@ -0,0 +1,25 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +if (process.platform === 'win32') { + console.log('Skipping non-Windows Python availability control.'); +} else { + const emptyPath = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-missing-python-')); + try { + const result = spawnSync(process.execPath, [path.join(__dirname, 'observe-secret-redaction.test.js')], { + env: { ...process.env, PATH: emptyPath }, encoding: 'utf8', timeout: 15000 + }); + assert.equal(result.status, 1, result.stderr); + assert.match(result.stdout, /FAIL python3 is required/); + assert.match(result.stdout, /Passed: 0\nFailed: 1/); + assert.doesNotMatch(result.stdout, /Skipping shell fixtures/); + console.log('PASS missing Python fails the actual non-Windows redaction gate'); + } finally { + fs.rmSync(emptyPath, { recursive: true, force: true }); + } +} diff --git a/tests/hooks/observe-secret-redaction.test.js b/tests/hooks/observe-secret-redaction.test.js index fbebca8ad..1ab6dcff0 100644 --- a/tests/hooks/observe-secret-redaction.test.js +++ b/tests/hooks/observe-secret-redaction.test.js @@ -41,7 +41,13 @@ function capture(phase, input) { } console.log('=== Actual registered observation secret redaction ==='); -if (process.platform !== 'win32' && spawnSync('python3', ['--version']).status === 0) { +if (process.platform === 'win32') { + console.log('Skipping shell fixtures: native Windows runner requires platform CI.'); +} else if (spawnSync('python3', ['--version']).status !== 0) { + console.log(' FAIL python3 is required for observation redaction tests'); + failed += 1; +} else { + // Synthetic canary data proves that captured credential-shaped text is masked. for (const [label, content] of [ ['double-quoted source', 'const apiKey = "CANARY_TOKEN_abcdefghijk";'], ['single-quoted source', "const password = 'CANARY_TOKEN_abcdefghijk';"], @@ -61,8 +67,6 @@ if (process.platform !== 'win32' && spawnSync('python3', ['--version']).status = assert.ok(records.includes('[REDACTED]')); }); } -} else { - console.log('Skipping shell fixtures: native Windows runner requires platform CI.'); } console.log(`Passed: ${passed}`); console.log(`Failed: ${failed}`); diff --git a/tests/lib/control-pane-proximity.test.js b/tests/lib/control-pane-proximity.test.js index 1104214bc..7c69e1629 100644 --- a/tests/lib/control-pane-proximity.test.js +++ b/tests/lib/control-pane-proximity.test.js @@ -163,8 +163,9 @@ test('quoted staged paths and untracked paths share identity, and untracked fail assert.ok(snapshot.agents.every(agent => agent.files.includes(file))); for (const code of ['EACCES', 'ETIMEDOUT', 'ENOBUFS']) { childProcess.execFileSync = (command, args, options) => { + assert.strictEqual(command, 'git', 'fixture only forwards Git probes'); if (args.includes('ls-files')) throw Object.assign(new Error('untracked probe unavailable'), { code }); - return originalExec(command, args, options); + return originalExec('git', args, { ...options, shell: false }); }; delete require.cache[modulePath]; const fresh = require(modulePath); @@ -172,8 +173,9 @@ test('quoted staged paths and untracked paths share identity, and untracked fail assert.ok(tracked, `${code}: tracked changes must survive`); assert.deepStrictEqual(tracked.files.map(entry => entry.path).sort(), ['README', file].sort()); childProcess.execFileSync = (command, args, options) => { + assert.strictEqual(command, 'git', 'fixture only forwards Git probes'); if (args.includes('--name-only')) throw Object.assign(new Error('tracked names unavailable'), { code }); - return originalExec(command, args, options); + return originalExec('git', args, { ...options, shell: false }); }; delete require.cache[modulePath]; const partial = require(modulePath).sessionsToAgents([worktrees[0]]); diff --git a/tests/lib/gateguard-search-evidence.test.js b/tests/lib/gateguard-search-evidence.test.js index 1f3e87f7d..a39e56e6f 100644 --- a/tests/lib/gateguard-search-evidence.test.js +++ b/tests/lib/gateguard-search-evidence.test.js @@ -209,6 +209,8 @@ test('a search confined to the target file name is a lookup, not a search', () = ['Grep', { pattern: 'tokenizer', glob: '**/tokenizer.js' }], ['Bash', { command: 'find src -name tokenizer.js' }], ['Bash', { command: 'rg -g tokenizer.js tokenizer src' }], + ['Bash', { command: 'rg -t js -g tokenizer.js tokenizer src' }], + ['Grep', { pattern: 'tokenizer', type: 'js', glob: 'tokenizer.js' }], ['Bash', { command: 'rg --files -g tokenizer.js src' }], ['Bash', { command: 'rg tokenizer src/**/tokenizer.js' }] ]; diff --git a/tests/lib/gateguard-search-filters.test.js b/tests/lib/gateguard-search-filters.test.js index 14709817c..7e60ea0e1 100644 --- a/tests/lib/gateguard-search-filters.test.js +++ b/tests/lib/gateguard-search-filters.test.js @@ -105,6 +105,23 @@ test('rg -T/--type-not excludes the type globs', () => { } }); +test('explicit rg globs take precedence over type alternatives in either flag order', () => { + for (const args of [['-t', 'js', '-g', '*.py'], ['-g', '*.py', '--type=js']]) { + const item = rgItem([...args, 'tokenizer', 'src']); + assert.strictEqual(admitted(item, 'src/tokenizer.js'), false, args.join(' ')); + assert.strictEqual(admitted(item, 'src/tokenizer.py'), true, args.join(' ')); + } + assert.strictEqual(admitted(rgItem(['-t', 'py', '-g', '*.js']), 'src/tokenizer.js'), true); + assert.strictEqual(admitted(rgItem(['-T', 'js', '-g', '*.js']), 'src/tokenizer.js'), true); + assert.strictEqual(admitted(rgItem(['-t', 'js', '-g', '!*.js']), 'src/tokenizer.js'), false); +}); + +test('Grep tool explicit globs do not gain an alternative from its type', () => { + const item = { ...grepToolFilters('*.py', 'js'), scopes: [['.']], pathIncludes: true }; + assert.strictEqual(admitted(item, 'src/tokenizer.js'), false); + assert.strictEqual(admitted(item, 'src/tokenizer.py'), true); +}); + test('unknown or redefined rg types are unreadable filters', () => { for (const args of [['-t', 'nosuchtype'], ['-T', 'nosuchtype'], ['--type-add', 'web:*.js', '-t', 'web'], ['--type-clear', 'js']]) { assert.strictEqual(admitted(rgItem([...args, 'x']), 'src/tokenizer.js'), false, args.join(' '));