Merge feature/dc-064-discover-adopt-fetcht: DC-077 nesting-guard silent no-op fix [glm-grade=B]
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
@@ -31,6 +31,17 @@ module.exports = {
|
||||
CADDY_ADMIN_URL,
|
||||
SERVICES_FILE,
|
||||
SERVICES_DIR,
|
||||
// Re-export the resolved data directory so other modules (notably
|
||||
// src/utilities/nesting-guard.js) can locate `/app/data` without having to
|
||||
// also require('../../platform-paths') — keeps a single source of truth for
|
||||
// the data dir on the src/config/paths surface. Without this, `dataDir`
|
||||
// resolves to `undefined`, and `path.join(undefined, 'data')` throws
|
||||
// `TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type
|
||||
// string. Received undefined` at startup (DC-077 fingerprint). Fall back to
|
||||
// platformPaths.dataDir if SERVICES_DIR is somehow not a string (defensive —
|
||||
// SERVICES_DIR is computed from a path.dirname() of a string so it always
|
||||
// is, but the cost of guarding is one branch).
|
||||
dataDir: typeof SERVICES_DIR === 'string' && SERVICES_DIR ? SERVICES_DIR : platformPaths.dataDir,
|
||||
CONFIG_FILE,
|
||||
DNS_CREDENTIALS_FILE,
|
||||
TAILSCALE_CONFIG_FILE,
|
||||
|
||||
@@ -14,8 +14,19 @@ const path = require('path');
|
||||
module.exports = function nestingGuard() {
|
||||
try {
|
||||
const paths = require('../config/paths');
|
||||
const dataDir = paths.dataDir;
|
||||
const dataDataPath = path.join(dataDir, 'data');
|
||||
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)) {
|
||||
|
||||
Reference in New Issue
Block a user