Features: - 30+ Custom Skills (cognitive, development, UI/UX, autonomous agents) - RalphLoop autonomous agent integration - Multi-AI consultation (Qwen) - Agent management system with sync capabilities - Custom hooks for session management - MCP servers integration - Plugin marketplace setup - Comprehensive installation script Components: - Skills: always-use-superpowers, ralph, brainstorming, ui-ux-pro-max, etc. - Agents: 100+ agents across engineering, marketing, product, etc. - Hooks: session-start-superpowers, qwen-consult, ralph-auto-trigger - Commands: /brainstorm, /write-plan, /execute-plan - MCP Servers: zai-mcp-server, web-search-prime, web-reader, zread - Binaries: ralphloop wrapper Installation: ./supercharge.sh
584 lines
20 KiB
TypeScript
584 lines
20 KiB
TypeScript
/**
|
|
* Targeted unit tests for helper parsers in the safety net.
|
|
*
|
|
* These focus on option-scanning branches that are hard to hit via end-to-end
|
|
* command strings, improving confidence (and coverage) of the parsing logic.
|
|
*/
|
|
|
|
import { describe, expect, test } from 'bun:test';
|
|
import { dangerousInText } from '../src/core/analyze/dangerous-text.ts';
|
|
import { extractDashCArg } from '../src/core/analyze/shell-wrappers.ts';
|
|
import {
|
|
_extractParallelChildCommand,
|
|
_extractXargsChildCommand,
|
|
_findHasDelete,
|
|
_hasRecursiveForceFlags,
|
|
} from '../src/core/analyze.ts';
|
|
import { _extractGitSubcommandAndRest, _getCheckoutPositionalArgs } from '../src/core/rules-git.ts';
|
|
import { extractShortOpts, splitShellCommands, stripWrappersWithInfo } from '../src/core/shell.ts';
|
|
import { MAX_STRIP_ITERATIONS } from '../src/types.ts';
|
|
|
|
describe('shell parsing helpers', () => {
|
|
describe('extractDashCArg', () => {
|
|
test('returns null for empty tokens', () => {
|
|
expect(extractDashCArg([])).toBeNull();
|
|
});
|
|
|
|
test('returns null for single token', () => {
|
|
expect(extractDashCArg(['bash'])).toBeNull();
|
|
});
|
|
|
|
test('extracts arg after standalone -c', () => {
|
|
expect(extractDashCArg(['bash', '-c', 'echo ok'])).toBe('echo ok');
|
|
});
|
|
|
|
test('extracts arg after bundled -lc', () => {
|
|
expect(extractDashCArg(['bash', '-lc', 'echo ok'])).toBe('echo ok');
|
|
});
|
|
|
|
test('extracts arg after bundled -xc', () => {
|
|
expect(extractDashCArg(['sh', '-xc', 'rm -rf /'])).toBe('rm -rf /');
|
|
});
|
|
|
|
test('returns null when -c has no following arg', () => {
|
|
expect(extractDashCArg(['bash', '-c'])).toBeNull();
|
|
});
|
|
|
|
test('returns null when bundled option has no following arg', () => {
|
|
expect(extractDashCArg(['bash', '-lc'])).toBeNull();
|
|
});
|
|
|
|
test('handles -- separator before -c (implementation scans past it)', () => {
|
|
expect(extractDashCArg(['bash', '--', '-c', 'echo'])).toBe('echo');
|
|
});
|
|
|
|
test('ignores long options starting with --', () => {
|
|
expect(extractDashCArg(['bash', '--rcfile', 'script'])).toBeNull();
|
|
});
|
|
|
|
test('returns null when next token starts with dash', () => {
|
|
expect(extractDashCArg(['bash', '-lc', '-x'])).toBeNull();
|
|
});
|
|
|
|
test('handles -c appearing later in tokens', () => {
|
|
expect(extractDashCArg(['bash', '-l', '-c', 'echo ok'])).toBe('echo ok');
|
|
});
|
|
});
|
|
|
|
describe('extractShortOpts', () => {
|
|
test('stops at double dash', () => {
|
|
// given: tokens with -Ap after -- (a filename, not options)
|
|
// when: extracting short options
|
|
// then: A and p should NOT be in the result
|
|
expect(extractShortOpts(['git', 'add', '--', '-Ap'])).toEqual(new Set());
|
|
expect(extractShortOpts(['rm', '-r', '--', '-f'])).toEqual(new Set(['-r']));
|
|
});
|
|
|
|
test('extracts before double dash', () => {
|
|
// given: tokens with options before --
|
|
// when: extracting short options
|
|
// then: only options before -- are extracted
|
|
expect(extractShortOpts(['git', '-v', 'add', '-n', '--', '-x'])).toEqual(
|
|
new Set(['-v', '-n']),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('splitShellCommands', () => {
|
|
test('returns whole command when quotes are unclosed', () => {
|
|
expect(splitShellCommands('echo "unterminated')).toEqual([['echo "unterminated']]);
|
|
});
|
|
|
|
test('extracts arithmetic substitution segments (nested parens)', () => {
|
|
expect(splitShellCommands('echo $((1+2))')).toEqual([['echo'], ['1+2']]);
|
|
});
|
|
|
|
test('extracts backtick substitution segments', () => {
|
|
expect(splitShellCommands('echo `date`')).toEqual([['date'], ['echo', '`date`']]);
|
|
});
|
|
|
|
test('extracts $() substitution segments split on operators', () => {
|
|
expect(splitShellCommands('echo $(rm -rf /tmp/x && echo ok)')).toEqual([
|
|
['echo'],
|
|
['rm', '-rf', '/tmp/x'],
|
|
['echo', 'ok'],
|
|
]);
|
|
});
|
|
|
|
test('extracts multiple backtick substitutions from one token', () => {
|
|
expect(splitShellCommands('echo `a`:`b`')).toEqual([['a'], ['b'], ['echo', '`a`:`b`']]);
|
|
});
|
|
|
|
test('handles nested $(...) with operators', () => {
|
|
const result = splitShellCommands('echo $(echo $(rm -rf /tmp/x))');
|
|
expect(result.length).toBeGreaterThan(1);
|
|
const flat = result.flat();
|
|
expect(flat).toContain('rm');
|
|
expect(flat).toContain('-rf');
|
|
});
|
|
|
|
test('handles deeply nested $(...) substitutions', () => {
|
|
const result = splitShellCommands('echo $(a $(b $(c)))');
|
|
expect(result.length).toBeGreaterThan(1);
|
|
});
|
|
|
|
test('handles $(...) with semicolon operators', () => {
|
|
expect(splitShellCommands('echo $(cd /tmp; rm -rf .)')).toEqual([
|
|
['echo'],
|
|
['cd', '/tmp'],
|
|
['rm', '-rf', '.'],
|
|
]);
|
|
});
|
|
|
|
test('handles $(...) with pipe operators', () => {
|
|
expect(splitShellCommands('echo $(cat file | rm -rf /)')).toEqual([
|
|
['echo'],
|
|
['cat', 'file'],
|
|
['rm', '-rf', '/'],
|
|
]);
|
|
});
|
|
|
|
test('handles unterminated $() substitution (no hang, still extracts tokens)', () => {
|
|
expect(splitShellCommands('echo $(rm -rf /tmp/x')).toEqual([
|
|
['echo'],
|
|
['rm', '-rf', '/tmp/x'],
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('stripWrappersWithInfo', () => {
|
|
test('strips sudo options that consume a value', () => {
|
|
const result = stripWrappersWithInfo(['sudo', '-u', 'root', 'rm', '-rf', '/tmp/a']);
|
|
expect(result.tokens).toEqual(['rm', '-rf', '/tmp/a']);
|
|
});
|
|
|
|
test('strips env -C=...', () => {
|
|
const result = stripWrappersWithInfo(['env', '-C=/tmp', 'rm', '-rf']);
|
|
expect(result.tokens).toEqual(['rm', '-rf']);
|
|
});
|
|
|
|
test('strips command -pv and -- separator', () => {
|
|
const result = stripWrappersWithInfo(['command', '-pv', '--', 'git', 'status']);
|
|
expect(result.tokens).toEqual(['git', 'status']);
|
|
});
|
|
|
|
test('captures env assignments after hitting max strip iterations', () => {
|
|
const tokens = Array.from({ length: MAX_STRIP_ITERATIONS }, () => 'sudo');
|
|
tokens.push('FOO=bar', 'rm', '-rf');
|
|
|
|
const result = stripWrappersWithInfo(tokens);
|
|
expect(result.tokens).toEqual(['rm', '-rf']);
|
|
expect(result.envAssignments.get('FOO')).toBe('bar');
|
|
});
|
|
|
|
test('strips nested wrappers across iterations and preserves env assignments', () => {
|
|
const result = stripWrappersWithInfo([
|
|
'sudo',
|
|
'env',
|
|
'FOO=1',
|
|
'sudo',
|
|
'command',
|
|
'--',
|
|
'rm',
|
|
'-rf',
|
|
'/tmp/a',
|
|
]);
|
|
expect(result.tokens).toEqual(['rm', '-rf', '/tmp/a']);
|
|
expect(result.envAssignments.get('FOO')).toBe('1');
|
|
});
|
|
|
|
test("drops leading tokens containing '=' that are not NAME=value assignments", () => {
|
|
// Intentionally conservative: only strict NAME=value is treated as an env assignment.
|
|
// Shell-legal forms like NAME+=value are dropped to reach the real command head.
|
|
const result = stripWrappersWithInfo(['FOO+=bar', 'rm', '-rf']);
|
|
expect(result.tokens).toEqual(['rm', '-rf']);
|
|
expect(result.envAssignments.get('FOO')).toBeUndefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('rm parsing helpers', () => {
|
|
describe('hasRecursiveForceFlags', () => {
|
|
test('empty tokens returns false', () => {
|
|
expect(_hasRecursiveForceFlags([])).toBe(false);
|
|
});
|
|
|
|
test('stops at double dash', () => {
|
|
// -f after `--` is a positional arg, not an option.
|
|
expect(_hasRecursiveForceFlags(['rm', '-r', '--', '-f'])).toBe(false);
|
|
});
|
|
|
|
test('detects -rf combined', () => {
|
|
expect(_hasRecursiveForceFlags(['rm', '-rf', 'foo'])).toBe(true);
|
|
});
|
|
|
|
test('detects -r -f separate', () => {
|
|
expect(_hasRecursiveForceFlags(['rm', '-r', '-f', 'foo'])).toBe(true);
|
|
});
|
|
|
|
test('detects --recursive --force', () => {
|
|
expect(_hasRecursiveForceFlags(['rm', '--recursive', '--force', 'foo'])).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('find parsing helpers', () => {
|
|
describe('findHasDelete', () => {
|
|
test('exec without terminator ignored', () => {
|
|
// Un-terminated -exec should not cause a false positive on -delete.
|
|
expect(_findHasDelete(['-exec', 'echo', '-delete'])).toBe(false);
|
|
});
|
|
|
|
test('skips undefined tokens', () => {
|
|
// biome-ignore lint/suspicious/noExplicitAny: intentionally testing malformed input
|
|
expect(_findHasDelete([undefined as any, '-delete'] as any)).toBe(true);
|
|
});
|
|
|
|
test('delete outside exec detected', () => {
|
|
expect(_findHasDelete(['-name', '*.txt', '-delete'])).toBe(true);
|
|
});
|
|
|
|
test('delete inside exec not detected', () => {
|
|
expect(_findHasDelete(['-exec', 'rm', '-delete', ';', '-print'])).toBe(false);
|
|
});
|
|
|
|
test('options that consume a value treat -delete as an argument', () => {
|
|
const consumingValue = [
|
|
'-name',
|
|
'-iname',
|
|
'-path',
|
|
'-ipath',
|
|
'-regex',
|
|
'-iregex',
|
|
'-type',
|
|
'-user',
|
|
'-group',
|
|
'-perm',
|
|
'-size',
|
|
'-mtime',
|
|
'-ctime',
|
|
'-atime',
|
|
'-newer',
|
|
'-printf',
|
|
'-fprint',
|
|
'-fprintf',
|
|
] as const;
|
|
|
|
for (const opt of consumingValue) {
|
|
expect(_findHasDelete([opt, '-delete'])).toBe(false);
|
|
expect(_findHasDelete([opt, '-delete', '-delete'])).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('dangerousInText', () => {
|
|
test('detects rm -rf variants', () => {
|
|
expect(dangerousInText('rm -rf /tmp/x')).toBe('rm -rf');
|
|
expect(dangerousInText('rm -R -f /tmp/x')).toBe('rm -rf');
|
|
expect(dangerousInText('rm -fr /tmp/x')).toBe('rm -rf');
|
|
expect(dangerousInText('rm -f -r /tmp/x')).toBe('rm -rf');
|
|
});
|
|
|
|
test('detects with leading whitespace (trimStart)', () => {
|
|
expect(dangerousInText(' rm -rf /tmp/x')).toBe('rm -rf');
|
|
});
|
|
|
|
test('detects key git patterns', () => {
|
|
expect(dangerousInText('git reset --hard')).toBe('git reset --hard');
|
|
expect(dangerousInText('git clean -f')).toBe('git clean -f');
|
|
});
|
|
|
|
test('skips find -delete when text starts with echo/rg', () => {
|
|
expect(dangerousInText('echo "find . -delete')).toBeNull();
|
|
expect(dangerousInText('rg "find . -delete')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('xargs parsing helpers', () => {
|
|
describe('extractXargsChildCommand', () => {
|
|
test('none when child unspecified', () => {
|
|
expect(_extractXargsChildCommand(['xargs'])).toEqual([]);
|
|
});
|
|
|
|
test('double dash starts child', () => {
|
|
expect(_extractXargsChildCommand(['xargs', '--', 'rm', '-rf'])).toEqual(['rm', '-rf']);
|
|
});
|
|
|
|
test('long option consumes value', () => {
|
|
expect(_extractXargsChildCommand(['xargs', '--max-args', '5', 'rm', '-rf'])).toEqual([
|
|
'rm',
|
|
'-rf',
|
|
]);
|
|
});
|
|
|
|
test('long option equals form', () => {
|
|
expect(_extractXargsChildCommand(['xargs', '--max-args=5', 'rm'])).toEqual(['rm']);
|
|
});
|
|
|
|
test('short option attached form', () => {
|
|
expect(_extractXargsChildCommand(['xargs', '-n1', 'rm'])).toEqual(['rm']);
|
|
});
|
|
|
|
test('dash i does not consume child', () => {
|
|
expect(_extractXargsChildCommand(['xargs', '-i', 'rm', '-rf'])).toEqual(['rm', '-rf']);
|
|
});
|
|
|
|
test('more attached forms', () => {
|
|
const cases: Array<[string[], string[]]> = [
|
|
[['xargs', '-P4', 'rm'], ['rm']],
|
|
[['xargs', '-L2', 'rm'], ['rm']],
|
|
[['xargs', '-n1', 'rm'], ['rm']],
|
|
];
|
|
for (const [tokens, expected] of cases) {
|
|
expect(_extractXargsChildCommand(tokens)).toEqual(expected);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('parallel parsing helpers', () => {
|
|
describe('extractParallelChildCommand', () => {
|
|
test('returns empty when ::: is first token after parallel', () => {
|
|
// When ::: is the first token after parallel (and options),
|
|
// it returns empty because args follow :::
|
|
expect(_extractParallelChildCommand(['parallel', ':::'])).toEqual([]);
|
|
});
|
|
|
|
test('extracts command with -- separator', () => {
|
|
expect(_extractParallelChildCommand(['parallel', '--', 'rm', '-rf'])).toEqual(['rm', '-rf']);
|
|
});
|
|
|
|
test('returns command and all following tokens', () => {
|
|
// The function returns all tokens starting from the first non-option
|
|
expect(_extractParallelChildCommand(['parallel', 'rm', '-rf'])).toEqual(['rm', '-rf']);
|
|
});
|
|
|
|
test('returns command including ::: marker when command comes first', () => {
|
|
// If command tokens appear before :::, all of them are returned
|
|
expect(_extractParallelChildCommand(['parallel', 'rm', '-rf', ':::', '/'])).toEqual([
|
|
'rm',
|
|
'-rf',
|
|
':::',
|
|
'/',
|
|
]);
|
|
});
|
|
|
|
test('consumes options', () => {
|
|
expect(_extractParallelChildCommand(['parallel', '-j4', '--', 'rm', '-rf'])).toEqual([
|
|
'rm',
|
|
'-rf',
|
|
]);
|
|
});
|
|
|
|
test('consumes --option=value', () => {
|
|
expect(_extractParallelChildCommand(['parallel', '--foo=bar', 'rm', '-rf'])).toEqual([
|
|
'rm',
|
|
'-rf',
|
|
]);
|
|
});
|
|
|
|
test('consumes options that take a value', () => {
|
|
expect(_extractParallelChildCommand(['parallel', '-S', 'sshlogin', 'rm', '-rf'])).toEqual([
|
|
'rm',
|
|
'-rf',
|
|
]);
|
|
});
|
|
|
|
test('consumes -j value form', () => {
|
|
expect(_extractParallelChildCommand(['parallel', '-j', '4', 'rm', '-rf'])).toEqual([
|
|
'rm',
|
|
'-rf',
|
|
]);
|
|
});
|
|
|
|
test('skips unknown short option', () => {
|
|
expect(_extractParallelChildCommand(['parallel', '-X', 'rm', '-rf'])).toEqual(['rm', '-rf']);
|
|
});
|
|
|
|
test('empty for just parallel', () => {
|
|
expect(_extractParallelChildCommand(['parallel'])).toEqual([]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('git rules helpers', () => {
|
|
describe('extractGitSubcommandAndRest', () => {
|
|
test('git only returns null subcommand', () => {
|
|
const result = _extractGitSubcommandAndRest(['git']);
|
|
expect(result.subcommand).toBeNull();
|
|
expect(result.rest).toEqual([]);
|
|
});
|
|
|
|
test('non git returns null subcommand', () => {
|
|
const result = _extractGitSubcommandAndRest(['echo', 'ok']);
|
|
expect(result.subcommand).toBeNull();
|
|
expect(result.rest).toEqual([]);
|
|
});
|
|
|
|
test('unknown short option skipped', () => {
|
|
const result = _extractGitSubcommandAndRest(['git', '-x', 'reset', '--hard']);
|
|
expect(result.subcommand).toBe('reset');
|
|
expect(result.rest).toEqual(['--hard']);
|
|
});
|
|
|
|
test('unknown long option equals skipped', () => {
|
|
const result = _extractGitSubcommandAndRest(['git', '--unknown=1', 'reset', '--hard']);
|
|
expect(result.subcommand).toBe('reset');
|
|
expect(result.rest).toEqual(['--hard']);
|
|
});
|
|
|
|
test('opts with value separate consumed', () => {
|
|
const result = _extractGitSubcommandAndRest(['git', '-c', 'foo=bar', 'reset']);
|
|
expect(result.subcommand).toBe('reset');
|
|
expect(result.rest).toEqual([]);
|
|
});
|
|
|
|
test('double dash can introduce subcommand', () => {
|
|
const result = _extractGitSubcommandAndRest(['git', '--', 'reset', '--hard']);
|
|
expect(result.subcommand).toBe('reset');
|
|
expect(result.rest).toEqual(['--hard']);
|
|
});
|
|
|
|
test('double dash without a subcommand yields null', () => {
|
|
const result = _extractGitSubcommandAndRest(['git', '--', '--help']);
|
|
expect(result.subcommand).toBeNull();
|
|
expect(result.rest).toEqual(['--help']);
|
|
});
|
|
|
|
test('attached -C consumes itself', () => {
|
|
const result = _extractGitSubcommandAndRest(['git', '-C/tmp', 'reset', '--hard']);
|
|
expect(result.subcommand).toBe('reset');
|
|
expect(result.rest).toEqual(['--hard']);
|
|
});
|
|
});
|
|
|
|
describe('getCheckoutPositionalArgs', () => {
|
|
test('attached short opts ignored', () => {
|
|
expect(_getCheckoutPositionalArgs(['-bnew', 'main', 'file.txt'])).toEqual([
|
|
'main',
|
|
'file.txt',
|
|
]);
|
|
expect(_getCheckoutPositionalArgs(['-U3', 'main'])).toEqual(['main']);
|
|
});
|
|
|
|
test('long equals ignored', () => {
|
|
expect(_getCheckoutPositionalArgs(['--pathspec-from-file=paths.txt', 'main'])).toEqual([
|
|
'main',
|
|
]);
|
|
});
|
|
|
|
test('double dash breaks', () => {
|
|
expect(_getCheckoutPositionalArgs(['--', 'file.txt'])).toEqual([]);
|
|
});
|
|
|
|
test('options with value consumed', () => {
|
|
expect(_getCheckoutPositionalArgs(['-b', 'new', 'main'])).toEqual(['main']);
|
|
});
|
|
|
|
test('unknown long option consumes value', () => {
|
|
expect(_getCheckoutPositionalArgs(['--unknown', 'main', 'file.txt'])).toEqual(['file.txt']);
|
|
});
|
|
|
|
test('unknown short option skipped', () => {
|
|
expect(_getCheckoutPositionalArgs(['-x', 'main'])).toEqual(['main']);
|
|
});
|
|
|
|
test('optional value options recurse-submodules', () => {
|
|
expect(_getCheckoutPositionalArgs(['--recurse-submodules', 'main'])).toEqual(['main']);
|
|
expect(_getCheckoutPositionalArgs(['--recurse-submodules=on-demand', 'main'])).toEqual([
|
|
'main',
|
|
]);
|
|
});
|
|
|
|
test('optional value options track', () => {
|
|
expect(_getCheckoutPositionalArgs(['--track', 'main'])).toEqual(['main']);
|
|
expect(_getCheckoutPositionalArgs(['--track=direct', 'main'])).toEqual(['main']);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('cwd tracking helpers', () => {
|
|
const { _segmentChangesCwd } = require('../src/core/analyze.ts');
|
|
|
|
test('cd returns true', () => {
|
|
expect(_segmentChangesCwd(['cd', '..'])).toBe(true);
|
|
});
|
|
|
|
test('pushd returns true', () => {
|
|
expect(_segmentChangesCwd(['pushd', '/tmp'])).toBe(true);
|
|
});
|
|
|
|
test('popd returns true', () => {
|
|
expect(_segmentChangesCwd(['popd'])).toBe(true);
|
|
});
|
|
|
|
test('builtin cd returns true', () => {
|
|
expect(_segmentChangesCwd(['builtin', 'cd', '..'])).toBe(true);
|
|
});
|
|
|
|
test('builtin only returns false', () => {
|
|
expect(_segmentChangesCwd(['builtin'])).toBe(false);
|
|
});
|
|
|
|
test('grouped cd returns true', () => {
|
|
expect(_segmentChangesCwd(['{', 'cd', '..', ';', '}'])).toBe(true);
|
|
});
|
|
|
|
test('subshell cd returns true', () => {
|
|
expect(_segmentChangesCwd(['(', 'cd', '..', ')'])).toBe(true);
|
|
});
|
|
|
|
test('command substitution cd returns true', () => {
|
|
expect(_segmentChangesCwd(['$(', 'cd', '..', ')'])).toBe(true);
|
|
});
|
|
|
|
test('regex fallback on unparseable', () => {
|
|
expect(_segmentChangesCwd(['cd', "'unterminated"])).toBe(true);
|
|
});
|
|
|
|
test('non-cd command returns false', () => {
|
|
expect(_segmentChangesCwd(['ls', '-la'])).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('xargs parsing helpers', () => {
|
|
const { _extractXargsChildCommandWithInfo } = require('../src/core/analyze.ts');
|
|
|
|
test('replacement token from -I option', () => {
|
|
const result = _extractXargsChildCommandWithInfo(['xargs', '-I', '{}', 'rm', '-rf', '{}']);
|
|
expect(result.replacementToken).toBe('{}');
|
|
});
|
|
|
|
test('replacement token from -I attached', () => {
|
|
const result = _extractXargsChildCommandWithInfo(['xargs', '-I%', 'rm', '-rf', '%']);
|
|
expect(result.replacementToken).toBe('%');
|
|
});
|
|
|
|
test('replacement token from --replace defaults to braces', () => {
|
|
const result = _extractXargsChildCommandWithInfo(['xargs', '--replace', 'rm', '-rf', '{}']);
|
|
expect(result.replacementToken).toBe('{}');
|
|
});
|
|
|
|
test('replacement token from --replace= empty defaults to braces', () => {
|
|
const result = _extractXargsChildCommandWithInfo(['xargs', '--replace=', 'rm', '-rf', '{}']);
|
|
expect(result.replacementToken).toBe('{}');
|
|
});
|
|
|
|
test('replacement token from --replace=CUSTOM', () => {
|
|
const result = _extractXargsChildCommandWithInfo([
|
|
'xargs',
|
|
'--replace=FOO',
|
|
'rm',
|
|
'-rf',
|
|
'FOO',
|
|
]);
|
|
expect(result.replacementToken).toBe('FOO');
|
|
});
|
|
|
|
test('no replacement token when not specified', () => {
|
|
const result = _extractXargsChildCommandWithInfo(['xargs', 'rm', '-rf']);
|
|
expect(result.replacementToken).toBeNull();
|
|
});
|
|
});
|