Files
dashcaddy/dashcaddy-api/__tests__/nesting-guard.test.js
T
DashCaddy Polish 18ffd2e519 fix(nesting-guard): export dataDir from src/config/paths; harden fallback to platform-paths (DC-077) [glm-grade=B]
Pre-fix, every dashcaddy-api container startup logged:
  [nesting-guard] Skipped: The "path" argument must be of type string. Received undefined
because src/utilities/nesting-guard.js does require('../config/paths') and
calls paths.dataDir — but src/config/paths.js imported platformPaths and
only re-exported its specific files (SERVICES_FILE, CONFIG_FILE, etc);
dataDir was never re-exported, so paths.dataDir was undefined.

Result: path.join(undefined, 'data') threw TypeError, the outer try/catch
swallowed it, and the entire nesting-guard became a silent no-op. The
cleanup that prevents recursive data/data/data/... directory duplicates
never ran on any startup. Bug class is 'silent functional no-op' (same
family as DC-056 AggregateError visibility).

(1) src/config/paths.js (+11): re-export dataDir as
SERVICES_DIR-derived (with platformPaths.dataDir fallback). dataDir is
the dirname of SERVICES_FILE in container (env override wins), which
equals /app/data — same value platform-paths.dataDir computes for the
default config. Either path is fine; SERVICES_DIR is preferred because it
respects env-override.

(2) src/utilities/nesting-guard.js (+13/-2): defensive fallback to
require('../../platform-paths').dataDir if paths.dataDir is missing
(any future export-shape drift or older caller). Explicit skip-warn
instead of silent catch when both paths fail.

(3) __tests__/nesting-guard.test.js (NEW, 112 lines, 4/4 passing):
isolates module cache per test, exercises (a) cleanup when nested
data/data exists, (b) no-op when clean, (c) dataDir export contract,
(d) dataDir === dirname(SERVICES_FILE) under env override. No jest.doMock
leaks across tests (verified via 4-call probe sequence).

Verified: 4/4 tests passing. Full repo suite: 100/104 suites / 2335/2335
tests passing (4 pre-existing failures in __tests__/billing/* are
unrelated module-resolution issues in src/billing/invoice.js, confirmed
unaffected by this change via stash+rerun).

GLM-5.3 round 1: B (ship, one polish nit — trailing newline on test
file, folded in same commit per multi-round-fix-first protocol).

Deploy plan: container rebuild + atomic swap via /opt/dashcaddy/start.sh
on DNS2; live-verify status.sami=200, dashcaddy-api=Up+healthy, and
absence of [nesting-guard] Skipped log line in container logs.
2026-08-18 17:07:13 -07:00

113 lines
4.0 KiB
JavaScript

/**
* Nesting-guard tests — DC-077 (data/data recursive duplicate cleanup)
*
* The guard runs at app startup. Pre-fix, `src/config/paths.js` did NOT
* re-export `dataDir`, so `paths.dataDir` resolved to `undefined`. The
* outer try/catch swallowed the resulting `TypeError [ERR_INVALID_ARG_TYPE]`
* and the entire guard became a silent no-op — every startup logged
* `[nesting-guard] Skipped: The "path" argument must be of type string.
* Received undefined`. Post-fix, paths.js exports `dataDir` and the guard
* falls back to platform-paths directly if `paths.dataDir` is missing.
*
* Tests use jest.isolateModules() for clean module-cache isolation.
* jest.doMock is intentionally avoided — it persists across tests in a
* describe and is the root cause of subtle flakes.
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
describe('nesting-guard (DC-077)', () => {
const originalEnv = { ...process.env };
beforeEach(() => {
jest.restoreAllMocks();
});
afterEach(() => {
process.env = { ...originalEnv };
jest.restoreAllMocks();
});
function makeTmpTree() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'nest-guard-'));
}
function writeJson(p, obj) {
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, JSON.stringify(obj));
}
it('removes a recursive data/data duplicate when present', () => {
const tmp = makeTmpTree();
writeJson(path.join(tmp, 'config.json'), { x: 1 });
writeJson(path.join(tmp, 'data', 'config.json'), { x: 1 });
writeJson(path.join(tmp, 'data', 'services.json'), []);
process.env.SERVICES_FILE = path.join(tmp, 'services.json');
process.env.CONFIG_FILE = path.join(tmp, 'config.json');
let cleanupLog = '';
let warnLog = '';
jest.isolateModules(() => {
const guard = require('../src/utilities/nesting-guard');
jest.spyOn(console, 'log').mockImplementation((m) => { cleanupLog += String(m) + '\n'; });
jest.spyOn(console, 'warn').mockImplementation((m) => { warnLog += String(m) + '\n'; });
guard();
});
expect(fs.existsSync(path.join(tmp, 'data'))).toBe(false);
expect(fs.existsSync(path.join(tmp, 'config.json'))).toBe(true);
expect(cleanupLog).toMatch(/Removing recursive data nesting|Recursive nesting removed/);
expect(warnLog).not.toMatch(/Skipped/);
});
it('does nothing when no nested data/data directory exists', () => {
const tmp = makeTmpTree();
writeJson(path.join(tmp, 'config.json'), { x: 1 });
process.env.SERVICES_FILE = path.join(tmp, 'services.json');
process.env.CONFIG_FILE = path.join(tmp, 'config.json');
let cleanupLog = '';
let warnLog = '';
jest.isolateModules(() => {
const guard = require('../src/utilities/nesting-guard');
jest.spyOn(console, 'log').mockImplementation((m) => { cleanupLog += String(m) + '\n'; });
jest.spyOn(console, 'warn').mockImplementation((m) => { warnLog += String(m) + '\n'; });
guard();
});
expect(fs.existsSync(path.join(tmp, 'config.json'))).toBe(true);
expect(warnLog).not.toMatch(/Skipped/);
expect(cleanupLog).not.toMatch(/Removing recursive data nesting/);
});
it('src/config/paths exports dataDir as a non-empty string', () => {
let dataDir;
jest.isolateModules(() => {
const paths = require('../src/config/paths');
dataDir = paths.dataDir;
});
expect(typeof dataDir).toBe('string');
expect(dataDir.length).toBeGreaterThan(0);
});
it('src/config/paths.dataDir equals dirname(SERVICES_FILE) when SERVICES_FILE env is set', () => {
const tmp = makeTmpTree();
process.env.SERVICES_FILE = path.join(tmp, 'services.json');
process.env.CONFIG_FILE = path.join(tmp, 'config.json');
let servicesFile, dataDir;
jest.isolateModules(() => {
const paths = require('../src/config/paths');
servicesFile = paths.SERVICES_FILE;
dataDir = paths.dataDir;
});
expect(dataDir).toBe(path.dirname(servicesFile));
expect(dataDir).toBe(tmp);
});
});