- _isNewer: same-version releases are never 'newer' (commit labels are opaque stamps) — kills the same-version auto-apply regression loop - _autoCheckAndApply: identical version@sha256 never re-applied - dashcaddy-update.sh: truthful rollback verdicts (failed rebuild/health = exit 1 + failure result), exact frontend snapshot/restore (incl. update-introduced owned subtrees), contents-copy cp fallbacks with manifest reconciliation, JSON-encoded results/meta/stamp, prune on every exit path - self-updater: frontend-only Linux releases fail loudly (no more silent no-op success stuck in 'applying') - start.sh: DASHCADDY_UPDATE_ENABLED=true (Sami 2026-09-13), json-file log caps 10M x3, source->webroot sync via update-stamp contract - tests: _isNewer regression suite, functional cycle + json/escape suites (scripts/test-frontend-cycle.sh, scripts/test-json-escape.sh) 15 judge rounds: C,C,D,D,C,C,D,C,C,C,D,C,A
110 lines
3.8 KiB
JavaScript
110 lines
3.8 KiB
JavaScript
/**
|
|
* DC-122 regression tests — SelfUpdater._isNewer() must never treat a
|
|
* same-version/different-commit release as "newer".
|
|
*
|
|
* Loader works in BOTH layouts:
|
|
* - repo layout: requires ../src/docker/self-updater.js directly;
|
|
* - flattened judge worktree (deps missing): extracts the _isNewer +
|
|
* _compareVersions method sources from the implementation file and
|
|
* evaluates just those two pure functions — the test then exercises
|
|
* the exact shipped logic without needing platform-paths/logging.
|
|
*/
|
|
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
function findImplementationFile() {
|
|
const candidates = [
|
|
path.join(__dirname, '..', 'src', 'docker', 'self-updater.js'),
|
|
path.join(__dirname, 'self-updater.js'),
|
|
path.join(__dirname, '0_self-updater.js'),
|
|
];
|
|
for (const c of candidates) if (fs.existsSync(c)) return c;
|
|
throw new Error('self-updater.js not found relative to test file');
|
|
}
|
|
|
|
// Pull a single method out of the class source text by brace matching.
|
|
function extractMethod(src, name, argNames) {
|
|
const marker = `${name}(${argNames}) {`;
|
|
const at = src.indexOf(marker);
|
|
if (at === -1) throw new Error(`method ${name}(${argNames}) not found in source`);
|
|
const bodyStart = at + marker.length;
|
|
let depth = 1;
|
|
let i = bodyStart;
|
|
while (depth > 0 && i < src.length) {
|
|
const ch = src[i++];
|
|
if (ch === '{') depth++;
|
|
else if (ch === '}') depth--;
|
|
}
|
|
const body = src.slice(bodyStart, i - 1);
|
|
const args = argNames.split(',').map((s) => s.trim());
|
|
return new Function(...args, body);
|
|
}
|
|
|
|
function loadIsNewer() {
|
|
const implPath = findImplementationFile();
|
|
const src = fs.readFileSync(implPath, 'utf8');
|
|
// DC-122 (judge rev11): never construct a real SelfUpdater here — its
|
|
// constructor writes instance-id / notify-secret files to production
|
|
// default paths, making a unit test stateful. Always evaluate the two
|
|
// pure methods from source; this is the exact shipped logic either way.
|
|
const impl = {
|
|
_compareVersions: extractMethod(src, '_compareVersions', 'a, b'),
|
|
_isNewer: extractMethod(src, '_isNewer', 'local, remote'),
|
|
};
|
|
return impl._isNewer.bind(impl);
|
|
}
|
|
|
|
describe('SelfUpdater._isNewer() — DC-122 same-version auto-apply regression', () => {
|
|
let isNewer;
|
|
|
|
beforeAll(() => {
|
|
isNewer = loadIsNewer();
|
|
});
|
|
|
|
test('same version + different commit labels ⇒ NOT newer (the DC-122 bug)', () => {
|
|
expect(isNewer(
|
|
{ version: '1.16.0', commit: '20260722-065235-cookie-only-session-653478a' },
|
|
{ version: '1.16.0', commit: '321334c' }
|
|
)).toBe(false);
|
|
});
|
|
|
|
test('same version + reversed commit labels ⇒ NOT newer', () => {
|
|
expect(isNewer(
|
|
{ version: '1.16.0', commit: '321334c' },
|
|
{ version: '1.16.0', commit: '20260722-065235-cookie-only-session-653478a' }
|
|
)).toBe(false);
|
|
});
|
|
|
|
test('identical version+commit ⇒ NOT newer', () => {
|
|
expect(isNewer(
|
|
{ version: '1.16.0', commit: 'abc1234' },
|
|
{ version: '1.16.0', commit: 'abc1234' }
|
|
)).toBe(false);
|
|
});
|
|
|
|
test('higher remote semver ⇒ newer', () => {
|
|
expect(isNewer(
|
|
{ version: '1.15.0', commit: 'abc1234' },
|
|
{ version: '1.16.0', commit: 'def5678' }
|
|
)).toBe(true);
|
|
});
|
|
|
|
test('lower remote semver ⇒ NOT newer (downgrade refused)', () => {
|
|
expect(isNewer(
|
|
{ version: '1.16.0', commit: 'abc1234' },
|
|
{ version: '1.15.0', commit: 'def5678' }
|
|
)).toBe(false);
|
|
});
|
|
|
|
test('remote without version ⇒ NOT newer', () => {
|
|
expect(isNewer({ version: '1.16.0', commit: 'abc1234' }, {})).toBe(false);
|
|
expect(isNewer({ version: '1.16.0' }, null)).toBe(false);
|
|
});
|
|
|
|
test('multi-component semver compares numerically (10.0.0 > 9.5.1)', () => {
|
|
expect(isNewer({ version: '9.5.1' }, { version: '10.0.0' })).toBe(true);
|
|
expect(isNewer({ version: '10.0.0' }, { version: '9.5.1' })).toBe(false);
|
|
});
|
|
});
|