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,
|
CADDY_ADMIN_URL,
|
||||||
SERVICES_FILE,
|
SERVICES_FILE,
|
||||||
SERVICES_DIR,
|
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,
|
CONFIG_FILE,
|
||||||
DNS_CREDENTIALS_FILE,
|
DNS_CREDENTIALS_FILE,
|
||||||
TAILSCALE_CONFIG_FILE,
|
TAILSCALE_CONFIG_FILE,
|
||||||
|
|||||||
@@ -14,8 +14,19 @@ const path = require('path');
|
|||||||
module.exports = function nestingGuard() {
|
module.exports = function nestingGuard() {
|
||||||
try {
|
try {
|
||||||
const paths = require('../config/paths');
|
const paths = require('../config/paths');
|
||||||
const dataDir = paths.dataDir;
|
const dataDir = paths && paths.dataDir;
|
||||||
const dataDataPath = path.join(dataDir, 'data');
|
// 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 data/data exists, it's a recursive duplicate — remove it
|
||||||
if (fs.existsSync(dataDataPath)) {
|
if (fs.existsSync(dataDataPath)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user