fix(cms): stable bottom bar and self-save no longer trips the 409 lock
CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Blocked by required conditions
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Blocked by required conditions
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Waiting to run
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Blocked by required conditions

MITHOME-68: the optimistic-lock fingerprint was frozen at page load, so the
user's own second save 409'd. /save now returns the hash of the written
content and the client refreshes CONTENT_HASH on success — 409 only fires
for genuine external changes (deploy, other tab, restore). Also: successful
logins no longer consume the auth failure budget (only failed attempts do).

MITHOME-69: bottom bar items no longer shift while saving/publishing — the
status message occupies a constant flex slot (visibility instead of
display), the publish button locks its width while running and restores
its env-specific label, auto margins removed. Layout guard test added.

Test markers are now run-unique so a crashed run can never poison the
next one's expectations.
This commit is contained in:
Do Siki
2026-08-19 14:04:49 +02:00
parent 41a2259ede
commit 88383049d5
6 changed files with 126 additions and 20 deletions
+59 -2
View File
@@ -67,17 +67,74 @@ async function call(method, pathName, { body, headers } = {}) {
async function main() {
await waitForServer();
const contentFile = path.join(ROOT, 'proto', 'src', 'content', 'pages', 'contact.json');
const original = fs.readFileSync(contentFile, 'utf8');
const login = await fetch(`${BASE}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user: 'conflict-test-user', pass: 'conflict-test-pass' }),
});
const cookie = (login.headers.get('set-cookie') || '').split(';')[0];
const csrfPage = await (await fetch(`${BASE}/?file=contact`, { headers: { Cookie: cookie } })).text();
const csrf = csrfPage.match(/CSRF_TOKEN = "([a-f0-9]+)"/)[1];
// Baseline: read the page and capture the served fingerprint
const page = await (await call('GET', '/?file=contact')).text();
const servedHash = page.match(/const CONTENT_HASH = "([a-f0-9]+)"/)[1];
const servedHash = page.match(/let CONTENT_HASH = "([a-f0-9]+)"/)[1];
const diskBefore = fs.readFileSync(contentFile, 'utf8');
assert.equal(servedHash, hashOf(diskBefore), 'served fingerprint matches the file on disk');
const payload = diskBefore; // unchanged content is still a valid save payload
const jsonHeaders = { 'Content-Type': 'application/json' };
// 1. correct hash → 200
// MITHOME-68: the save response returns the new content hash; a tab that
// adopts it can save again — only a genuinely external change may 409.
const v3 = JSON.parse(original);
v3.hero.subtitle = 'Hash frissítés teszt ' + Date.now();
const first = await fetch(`${BASE}/save?file=contact`, {
method: 'POST',
headers: {
Cookie: cookie,
'Content-Type': 'application/json',
'X-CSRF-Token': csrf,
'X-Content-Hash': hashOf(fs.readFileSync(contentFile, 'utf8')),
},
body: JSON.stringify(v3, null, 2),
});
assert.equal(first.status, 200);
const firstBody = await first.json();
assert.match(firstBody.contentHash, /^[a-f0-9]{64}$/, 'save must return the new content hash');
// same tab continues with the returned hash → 200
const again = await fetch(`${BASE}/save?file=contact`, {
method: 'POST',
headers: {
Cookie: cookie,
'Content-Type': 'application/json',
'X-CSRF-Token': csrf,
'X-Content-Hash': firstBody.contentHash,
},
body: JSON.stringify(v3, null, 2),
});
assert.equal(again.status, 200, 'save with the refreshed hash must succeed');
// a stale (pre-save) hash still 409s
const stale = await fetch(`${BASE}/save?file=contact`, {
method: 'POST',
headers: {
Cookie: cookie,
'Content-Type': 'application/json',
'X-CSRF-Token': csrf,
'X-Content-Hash': hashOf(original),
},
body: JSON.stringify(v3, null, 2),
});
assert.equal(stale.status, 409, 'stale hash must still be rejected');
// leave the disk as the following sections expect it
fs.writeFileSync(contentFile, original);
// 1. correct hash → 200
const ok = await call('POST', '/save?file=contact', {
headers: { ...jsonHeaders, 'X-Content-Hash': servedHash },
body: payload,