Files
dashcaddy/dashcaddy-api/src/utilities/nesting-guard.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

50 lines
2.0 KiB
JavaScript

/**
* Recursive data nesting guard.
*
* In past versions, a buggy update/restore path created data/data/data/...
* directories — each containing a full recursive copy of the parent.
* This module runs at startup, detects and removes nested duplicates.
*
* Add to app.js: require('./utilities/nesting-guard')();
*/
const fs = require('fs');
const path = require('path');
module.exports = function nestingGuard() {
try {
const paths = require('../config/paths');
const dataDir = paths && paths.dataDir;
// Defensive: if paths.dataDir is undefined (older callers or a future
// export-shape drift), fall back to platformPaths.dataDir directly so the
// guard can still execute. Pre-fix this branch was swallowed silently by
// the outer try/catch, leaving the entire nesting-guard a no-op (DC-077).
const effectiveDataDir = typeof dataDir === 'string' && dataDir
? dataDir
: require('../../platform-paths').dataDir;
if (typeof effectiveDataDir !== 'string' || !effectiveDataDir) {
console.warn('[nesting-guard] Skipped: dataDir unavailable from src/config/paths and platform-paths');
return;
}
const dataDataPath = path.join(effectiveDataDir, 'data');
// If data/data exists, it's a recursive duplicate — remove it
if (fs.existsSync(dataDataPath)) {
const stat = fs.statSync(dataDataPath);
if (stat.isDirectory()) {
// Verify it's truly a duplicate (contains config.json like the parent)
const markerFile = path.join(dataDataPath, 'config.json');
const parentMarker = path.join(dataDir, 'config.json');
if (fs.existsSync(markerFile) && fs.existsSync(parentMarker)) {
console.log('[nesting-guard] Removing recursive data nesting: ' + dataDataPath);
fs.rmSync(dataDataPath, { recursive: true, force: true });
console.log('[nesting-guard] Recursive nesting removed');
}
}
}
} catch (e) {
// Non-fatal — don't crash startup over cleanup
console.warn('[nesting-guard] Skipped: ' + e.message);
}
};