← back to Butlr

test/orphan-recordings.test.js

88 lines

#!/usr/bin/env node
// Unit test for lib/orphan-recordings.scanOrphans.
//
// Builds a temp recordings dir with 3 known MP3s + 2 orphans + 1 stray
// .pcm (should be ignored), passes a fake knownIds set, asserts the
// returned list has exactly the 2 expected orphans in mtime-desc order.
//
// Pure-function test — no Express, no real data/calls.json, no network.
// Run with: node test/orphan-recordings.test.js

const fs = require('fs');
const path = require('path');
const os = require('os');
const assert = require('assert');

const { scanOrphans } = require('../lib/orphan-recordings');

function ok(name, fn) {
  try { fn(); console.log(`  ✓ ${name}`); return 1; }
  catch (e) { console.error(`  ✗ ${name}`); console.error('     ', e.message); return 0; }
}

let pass = 0, total = 0;

// ── setup temp recordings dir ───────────────────────────────────────
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'butlr-orphan-test-'));
console.log(`tmp: ${tmp}`);
const known = ['AAAAAAAA', 'BBBBBBBB', 'CCCCCCCC'];
const orphans = ['ORPHAN01', 'ORPHAN02'];
const now = Date.now();
for (const id of [...known, ...orphans]) {
  fs.writeFileSync(path.join(tmp, `${id}.mp3`), 'fakemp3');
}
fs.writeFileSync(path.join(tmp, 'STRAYPCM.pcm'), 'fakepcm'); // should be ignored
// Touch orphans so their mtime ordering is deterministic
fs.utimesSync(path.join(tmp, 'ORPHAN01.mp3'), now / 1000 - 60, now / 1000 - 60);
fs.utimesSync(path.join(tmp, 'ORPHAN02.mp3'), now / 1000, now / 1000);

// ── run ─────────────────────────────────────────────────────────────
const knownSet = new Set(known);
const result = scanOrphans({ dir: tmp, knownIds: knownSet });

console.log('\nresult:');
for (const o of result) console.log(`  ${o.id}  ${o.size_kb} KB  ${o.mtime}`);
console.log();

// ── assertions ──────────────────────────────────────────────────────
total++; pass += ok('returns exactly 2 orphans', () => assert.strictEqual(result.length, 2));
total++; pass += ok('orphans are ORPHAN01 + ORPHAN02', () => {
  const ids = result.map(o => o.id).sort();
  assert.deepStrictEqual(ids, ['ORPHAN01', 'ORPHAN02']);
});
total++; pass += ok('ORPHAN02 sorted before ORPHAN01 (newer first)', () => {
  assert.strictEqual(result[0].id, 'ORPHAN02');
  assert.strictEqual(result[1].id, 'ORPHAN01');
});
total++; pass += ok('does NOT include known IDs', () => {
  for (const o of result) assert.ok(!knownSet.has(o.id), `${o.id} should not be returned`);
});
total++; pass += ok('does NOT include .pcm stray', () => {
  for (const o of result) assert.ok(!o.filename.endsWith('.pcm'), 'pcm leaked');
});
total++; pass += ok('each orphan has filename/size_bytes/size_kb/mtime', () => {
  for (const o of result) {
    assert.ok(o.filename); assert.ok(typeof o.size_bytes === 'number');
    assert.ok(o.size_kb); assert.ok(o.mtime);
  }
});

// ── empty case ──────────────────────────────────────────────────────
total++; pass += ok('empty dir returns []', () => {
  const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'butlr-empty-'));
  const r = scanOrphans({ dir: empty, knownIds: new Set() });
  assert.deepStrictEqual(r, []);
  fs.rmSync(empty, { recursive: true });
});

// ── nonexistent dir ─────────────────────────────────────────────────
total++; pass += ok('nonexistent dir returns []', () => {
  const r = scanOrphans({ dir: '/no/such/path/nowhere', knownIds: new Set() });
  assert.deepStrictEqual(r, []);
});

// ── cleanup + report ────────────────────────────────────────────────
fs.rmSync(tmp, { recursive: true });
console.log(`\n${pass}/${total} passed`);
process.exit(pass === total ? 0 : 1);