import { describe, it, expect } from 'vitest'; import { tokenizeBash, isMutatingSegment } from './bash-tokenizer.mjs'; describe('tokenizeBash — basics', () => { it('tokenizes a simple command', () => { const r = tokenizeBash('ls -la /tmp'); expect(r.ok).toBe(true); expect(r.segments).toHaveLength(1); expect(r.segments[0].tokens).toEqual(['ls', '-la', '/tmp']); expect(r.hasSubshell).toBe(false); }); it('returns ok:false on empty input', () => { expect(tokenizeBash('').ok).toBe(false); expect(tokenizeBash(' ').ok).toBe(false); expect(tokenizeBash(null).ok).toBe(false); }); }); describe('tokenizeBash — segments & operators', () => { it('splits on && and records the operator', () => { const r = tokenizeBash('ls && git commit'); expect(r.segments.map((s) => s.tokens[0])).toEqual(['ls', 'git']); expect(r.segments[0].op).toBe('&&'); expect(r.segments[1].op).toBe(null); }); it('splits on pipe', () => { const r = tokenizeBash('cat a | grep x'); expect(r.segments).toHaveLength(2); expect(r.segments[0].op).toBe('|'); }); }); describe('tokenizeBash — sub-shell detection', () => { it.each([ ['echo `ls`', 'backtick'], ['echo $(ls)', 'cmd-subst'], ['diff <(ls a) <(ls b)', 'process-subst-in'], ['cat < { const r = tokenizeBash(cmd); expect(r.ok).toBe(true); expect(r.hasSubshell).toBe(true); expect(r.subshellKinds).toContain(kind); }); it('does not flag plain command', () => { expect(tokenizeBash('ls -la').hasSubshell).toBe(false); }); }); describe('tokenizeBash — parse errors', () => { it('returns ok:false on unbalanced quotes', () => { expect(tokenizeBash('echo "unterminated').ok).toBe(false); expect(tokenizeBash("echo 'open").ok).toBe(false); }); }); describe('isMutatingSegment', () => { it.each([ [['rm', '-rf', 'x'], true], [['git', 'commit', '-m', 'x'], true], [['git', 'status'], false], [['composer', 'install'], true], [['composer', 'show'], false], [['cat', 'x', '>', 'y'], true], [['grep', 'x', 'file'], false], ])('%j → %s', (tokens, expected) => { expect(isMutatingSegment(tokens)).toBe(expected); }); });