[grade=A urn:ump:seyasvbntxsjibq5jiaqmfowc6xgchwe4jvcc54jd6355ujgm75q] DC-122: self-updater hardening + auto-update on + docker disk discipline (v1.16.0)
- _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
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user