Files
gstack/test/empty-find-fallthrough.test.ts
Garry Tan 6b6e68f7af test: fix Windows CI fixtures and credential scan
Rebase captured JSON values and filesystem evidence using the appropriate
path convention. Compile native fake CLIs on Windows and synchronize pipe
holder readiness, with cleanup retained when assertions fail.

Assemble synthetic credential fixtures at runtime so the added-line scan
keeps enforcing the same gate without flagging its own rejection controls.

Discover generated skills directly for the empty-find regression check,
avoiding a recursive scan through saved evaluation artifacts and dependencies.
2026-09-11 17:37:16 +00:00

115 lines
5.0 KiB
TypeScript

/**
* Empty `find | xargs ls -t` must not fall through to cwd (#2483).
*
* GNU xargs runs `ls -t` once even on EMPTY input, and `ls -t` with no
* operands lists the CURRENT DIRECTORY — so on a fresh install (no
* ceo-plans / checkpoints / plans yet) a random cwd .md becomes "the plan"
* or "the latest checkpoint". `xargs -r` pins the BSD skip-on-empty
* behavior on both GNU and BSD (same shape as the landed
* bin/gstack-codex-session-import fix, #2482).
*
* Re-derived from community PR #2483 by @tranthanhnhatkhoa.
*/
import { describe, test, expect } from 'bun:test';
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { HOST_PATHS } from '../scripts/resolvers/types';
import type { TemplateContext } from '../scripts/resolvers/types';
import { generateContextRecovery } from '../scripts/resolvers/preamble/generate-context-recovery';
import { discoverSkillFiles } from '../scripts/discover-skills';
import { getExternalHosts } from '../hosts';
const ROOT = path.join(import.meta.dir, '..');
function generatedSkillFiles(root: string): string[] {
// Match the generator's output locations before scanning files. Workspace
// evidence, dependencies, and local Claude installs can be arbitrarily large.
const skillRoots = [root, ...getExternalHosts().map(host => path.join(root, host.hostSubdir, 'skills')),
path.join(root, 'openclaw', 'skills')];
return skillRoots.filter(dir => fs.existsSync(dir)).flatMap(dir =>
discoverSkillFiles(dir).map(file => path.join(dir, file)));
}
function unguardedSkillFiles(root: string): string[] {
return generatedSkillFiles(root).filter(file => fs.readFileSync(file, 'utf-8').includes('xargs ls -t'));
}
function makeCtx(): TemplateContext {
return {
skillName: 'test-skill',
tmplPath: 'test.tmpl',
host: 'claude',
paths: HOST_PATHS.claude,
preambleTier: 2,
};
}
describe('empty find must not fall through to cwd (#2483)', () => {
test('no resolver emits a bare `xargs ls -t` (must be `xargs -r ls -t`)', () => {
const out = execSync(
`grep -rn "xargs ls -t" "${path.join(ROOT, 'scripts')}" "${path.join(ROOT, 'bin')}" || true`,
{ encoding: 'utf-8', timeout: 30_000 },
);
expect(out.trim()).toBe('');
});
test('rendered Context Recovery uses the guarded form at both find sites', () => {
const rendered = generateContextRecovery(makeCtx());
const bareSites = rendered.split('xargs ls -t').length - 1;
expect(bareSites).toBe(0);
const guardedSites = rendered.split('xargs -r ls -t').length - 1;
expect(guardedSites).toBe(2);
});
test('live block: empty checkpoints dir yields NO checkpoint, not a cwd file', () => {
const rendered = generateContextRecovery(makeCtx());
const m = rendered.match(/_LATEST_CP=\$\(.*\)/);
expect(m).not.toBeNull();
const line = m![0];
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-home-'));
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-cwd-'));
try {
// Fresh install shape: the checkpoints dir exists but is EMPTY,
// and the cwd holds a decoy markdown file.
const proj = path.join(home, 'projects', 'unknown');
fs.mkdirSync(path.join(proj, 'checkpoints'), { recursive: true });
fs.writeFileSync(path.join(cwd, 'DECOY.md'), '# not a checkpoint\n');
const script = `_PROJ="${proj}"\n${line.replace(/\$\{_PROJ\}|"\$_PROJ"/g, '"$_PROJ"')}\necho "LATEST_CP=[$_LATEST_CP]"`;
const out = execSync(`bash -c '${script.replace(/'/g, `'\\''`)}'`, {
cwd,
encoding: 'utf-8',
timeout: 30_000,
});
expect(out).toContain('LATEST_CP=[]');
expect(out).not.toContain('DECOY.md');
} finally {
fs.rmSync(home, { recursive: true, force: true });
fs.rmSync(cwd, { recursive: true, force: true });
}
});
test('no generated SKILL.md carries the unguarded form', () => {
expect(generatedSkillFiles(ROOT).length).toBeGreaterThan(0);
expect(unguardedSkillFiles(ROOT)).toEqual([]);
});
test('generated skill discovery checks every host and excludes non-generated trees', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-skill-scan-'));
const unsafe = ['SKILL.md', path.join('review', 'SKILL.md'),
...getExternalHosts().map(host => path.join(host.hostSubdir, 'skills', 'gstack-review', 'SKILL.md')),
path.join('openclaw', 'skills', 'gstack-openclaw-review', 'SKILL.md')];
const decoys = ['.context', 'node_modules', '.claude', 'dist'].map(dir => path.join(dir, 'nested', 'SKILL.md'));
try {
for (const file of [...unsafe, ...decoys, path.join('safe', 'SKILL.md')]) {
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
fs.writeFileSync(path.join(root, file), file === path.join('safe', 'SKILL.md') ? 'xargs -r ls -t' : 'xargs ls -t');
}
expect(unguardedSkillFiles(root).sort()).toEqual(unsafe.map(file => path.join(root, file)).sort());
} finally { fs.rmSync(root, { recursive: true, force: true }); }
});
});