/** * Regression tests for getLocalVersion() — DC-033. * * The SelfUpdater's getLocalVersion() reads package.json + VERSION from the * filesystem relative to its own __dirname. server.js loads it via * `./src/docker/self-updater`, so __dirname inside the container is * `/app/src/docker` — which has no package.json. The function's outer * try/catch silently swallowed the ENOENT and returned the * `{ version: '0.0.0', commit: null }` fallback, making every DashCaddy * host running v1.14.x (≤ v1.14.8) appear to be at "version 0.0.0" in the * dashboard and "always outdated" to checkForUpdate(). * * DC-033 fixed it by walking a candidate list (api root first, __dirname * second). DC-035 is the regression test: if anyone re-introduces the * __dirname antipattern — or accidentally deletes the api-root package.json * — this suite will fail loudly. * * Loading pattern matters: this test loads `./src/docker/self-updater` to * match what server.js does at runtime. The legacy `./self-updater` path * (from /app) was deleted by DC-036, so the only require() that exists * now is the docker copy. */ const path = require('path'); describe('SelfUpdater.getLocalVersion() — DC-033 regression', () => { // Resolve from a known cwd so require('./src/docker/self-updater') lands // on the api-root copy, not some other relative-resolution target. const API_ROOT = path.join(__dirname, '..'); let SelfUpdater; beforeAll(() => { // Sanity check: the file must exist at the expected path. const target = path.join(API_ROOT, 'src', 'docker', 'self-updater.js'); expect(() => require.resolve(target)).not.toThrow(); const mod = require(path.join(API_ROOT, 'src', 'docker', 'self-updater.js')); SelfUpdater = mod.SelfUpdater || mod.default || mod; }); test('module loads and exports a SelfUpdater class', () => { expect(typeof SelfUpdater).toBe('function'); expect(SelfUpdater.name).toBe('SelfUpdater'); }); describe('getLocalVersion() returns real version + commit', () => { let result; beforeAll(() => { // Empty options — DEFAULTS will be used; getLocalVersion doesn't // need config to read sibling files. const instance = new SelfUpdater({}); result = instance.getLocalVersion(); }); test('result is an object with version + commit', () => { expect(result).toEqual(expect.objectContaining({ version: expect.any(String), commit: expect.any(String), })); }); test('version is NOT the 0.0.0 fallback (the DC-033 bug)', () => { // If this fails, someone re-introduced the __dirname antipattern. expect(result.version).not.toBe('0.0.0'); }); test('version is a valid semver string', () => { // Anchored semver: MAJOR.MINOR.PATCH with optional pre-release/build. // Reject '0.0.0' explicitly and anything without 3 numeric components. expect(result.version).toMatch(/^\d+\.\d+\.\d+/); const parts = result.version.split('.'); expect(parts.length).toBeGreaterThanOrEqual(3); for (const part of parts) { // Allow pre-release suffixes (e.g. "1-rc1") but the first 3 must be numeric. const numeric = part.split('-')[0].split('+')[0]; expect(numeric).toMatch(/^\d+$/); } }); test('commit is a git SHA (7-40 hex chars), not null', () => { expect(result.commit).not.toBeNull(); expect(result.commit).toMatch(/^[0-9a-f]{7,40}$/); }); }); describe('candidate-path resolution survives missing sibling files', () => { // If we shadow __dirname by requiring the module through a different // require() chain, the function should still find package.json via its // candidate-list fallback. This catches the case where someone refactors // the file to a deeper subdirectory and forgets to update the candidates. test('getLocalVersion works regardless of how the module is required', () => { const mod = require(path.join(API_ROOT, 'src', 'docker', 'self-updater.js')); const Cls = mod.SelfUpdater || mod.default || mod; const result = new Cls({}).getLocalVersion(); expect(result.version).not.toBe('0.0.0'); expect(result.commit).toMatch(/^[0-9a-f]{7,40}$/); }); }); });