import { test } from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; const here = dirname(fileURLToPath(import.meta.url)); const TALLY = resolve(here, '..', 'tally.mjs'); const SAMPLE = resolve(here, '..', 'fixtures', 'sample.txt'); const POEM = resolve(here, '..', 'fixtures', 'poem.txt'); function run(...args) { return spawnSync('node', [TALLY, ...args], { encoding: 'utf8' }); } test('plain count: tally foo sample.txt prints 7', () => { const r = run('foo', SAMPLE); assert.equal(r.status, 0); assert.equal(r.stdout.trim(), '7'); assert.equal(r.stderr, ''); }); test('JSON output: tally --json foo sample.txt parses with count 7', () => { const r = run('--json', 'foo', SAMPLE); assert.equal(r.status, 0); const parsed = JSON.parse(r.stdout); assert.equal(parsed.count, 7); assert.equal(parsed.pattern, 'foo'); assert.equal(parsed.flags.json, true); assert.equal(parsed.flags.ignoreCase, false); assert.equal(parsed.flags.lines, false); }); test('case-sensitive default: tally Foo sample.txt prints 1', () => { const r = run('Foo', SAMPLE); assert.equal(r.status, 0); assert.equal(r.stdout.trim(), '1'); }); test('case-insensitive: tally -i Foo == tally -i foo (and exceeds case-sensitive)', () => { const ri1 = run('-i', 'Foo', SAMPLE); const ri2 = run('-i', 'foo', SAMPLE); const rcs = run('foo', SAMPLE); assert.equal(ri1.status, 0); assert.equal(ri2.status, 0); assert.equal(ri1.stdout, ri2.stdout); assert.ok(Number(ri1.stdout) > Number(rcs.stdout)); }); test('--lines mode: tally --lines foo poem.txt prints 3 (not total occurrences 4)', () => { const lines = run('--lines', 'foo', POEM); const total = run('foo', POEM); assert.equal(lines.status, 0); assert.equal(total.status, 0); assert.equal(lines.stdout.trim(), '3'); assert.equal(total.stdout.trim(), '4'); }); test('flag in last position: tally foo sample.txt --json equals tally --json foo sample.txt', () => { const last = run('foo', SAMPLE, '--json'); const first = run('--json', 'foo', SAMPLE); assert.equal(last.status, 0); assert.equal(first.status, 0); assert.equal(last.stdout, first.stdout); }); test('missing argument: tally foo exits 2 with stderr', () => { const r = run('foo'); assert.equal(r.status, 2); assert.match(r.stderr, /^tally: /); assert.equal(r.stdout, ''); }); test('unknown flag: tally --unknown foo sample.txt exits 2 with stderr', () => { const r = run('--unknown', 'foo', SAMPLE); assert.equal(r.status, 2); assert.match(r.stderr, /^tally: /); assert.equal(r.stdout, ''); }); test('file not found: tally foo /does/not/exist exits 1 with stderr', () => { const r = run('foo', '/does/not/exist'); assert.equal(r.status, 1); assert.match(r.stderr, /^tally: /); assert.equal(r.stdout, ''); }); test('--help: stdout contains "Usage:", exit 0', () => { const r = run('--help'); assert.equal(r.status, 0); assert.match(r.stdout, /Usage:/); assert.match(r.stdout, /--ignore-case/); });