Analyse: Abgleich schaltet neu aktivierte Analysen nicht ab (Sperre + Nachlesen), weitere Abgleich-Tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-01 18:50:36 +00:00
1 parent 78170567b4
commit 495c3a2c40
2 files changed
+76 -9

No files matched your search

+14 -9
View File
@@ -566,16 +566,21 @@ export async function runAnalyseReconcile({ now = new Date() } = {}) {
if (!doc.activatedMailAt) await sendK18(doc, project)
})
} else if ((doc.state === 'off' || doc.state === 'disabled') && SITE_KEY_RE.test(doc.siteKey || '')) {
await analyseDeps.client.stopSite(doc.siteKey)
stats.stopped.push(doc.$id)
const marker = offMarker(doc)
if (!doc.removalTicketId && marker >= config.analyse.avvVersion) {
const project = await projectOf(doc.projectId)
if (project && (await customerOf(doc.customerId))) {
await ensureRemovalTicket(doc, project)
stats.tickets.push(doc.$id)
// Gleiche Sperre wie Ein-/Ausschalten, Dokument nachlesen: eine inzwischen neu aktivierte Analyse bleibt an
await withLocks([`analyse:${doc.projectId}`], async () => {
const fresh = await listDocuments(config.collections.portalFeatures, [Query.equal('featureKey', FEATURE_KEY), Query.equal('projectId', doc.projectId), Query.limit(1)])
const cur = fresh[0]
if (!cur || cur.state === 'active' || !SITE_KEY_RE.test(cur.siteKey || '')) return
await analyseDeps.client.stopSite(cur.siteKey)
stats.stopped.push(cur.$id)
if (!cur.removalTicketId && offMarker(cur) >= config.analyse.avvVersion) {
const project = await projectOf(cur.projectId)
if (project && (await customerOf(cur.customerId))) {
await ensureRemovalTicket(cur, project)
stats.tickets.push(cur.$id)
}
}
}
})
}
} catch (err) {
console.error(`[analyse] Abgleich für Projekt ${doc.projectId} fehlgeschlagen:`, err.message)
+62
View File
@@ -703,3 +703,65 @@ test('deleteCustomerWithPortalAccess: Analyse aus (kein Ticket, keine Mail), Lö
assert.equal((await doc('kd2', 'kd2p')).state, 'off')
assert.ok(stops.includes(b.siteKey))
})
// ---- Fix round 1 ----
test('Abgleich off-Zweig: wird das Dokument nach dem Lesen wieder aktiv, bleibt der Messdienst an (Sperre + Nachlesen)', async () => {
const { withLocks } = await import('../server/services/keyedLock.js')
aw.seed('websiteProjects', { $id: 'f1p', customerId: 'c1', projectName: 'F1', subdomain: 'f1', isPublic: true })
const d = await seedFeature('c1', 'f1p', 'off', { offSince: '2026-10-04T09:00:00.000Z', removalTicketId: 'x' })
let run
await withLocks(['analyse:f1p'], async () => {
run = an.runAnalyseReconcile({ now: NOW })
await new Promise((r) => setTimeout(r, 150)) // Abgleich hat gelesen und wartet auf die Sperre
await updateDocument(config.collections.portalFeatures, d.$id, { ...sf.stateFields('active') })
})
await run
assert.ok(!stops.includes(d.siteKey), 'frisch aktivierte Analyse nicht abgeschaltet')
assert.equal((await doc('c1', 'f1p')).state, 'active')
})
test('Abgleich: Appwrite-Lesefehler (nicht 404) beendet nichts', async () => {
aw.seed('websiteProjects', { $id: 'f2p', customerId: 'c1', projectName: 'F2', subdomain: 'f2', isPublic: true })
const d = await seedFeature('c1', 'f2p', 'active')
const real = globalThis.fetch
globalThis.fetch = async (url, opts) => {
if (String(url).includes('/collections/websiteProjects/documents/f2p')) return new Response(JSON.stringify({ message: 'kaputt' }), { status: 500, headers: { 'content-type': 'application/json' } })
return real(url, opts)
}
try {
await an.runAnalyseReconcile({ now: NOW })
} finally {
globalThis.fetch = real
}
assert.ok(!stops.includes(d.siteKey))
assert.equal((await doc('c1', 'f2p')).state, 'active')
})
test('Abgleich: nicht öffentliches Projekt / ohne Subdomain => beendet mit Entfernen-Ticket, ohne K19', async () => {
aw.seed('websiteProjects', { $id: 'f3p', customerId: 'c1', projectName: 'F3', subdomain: 'f3', isPublic: false, repoFullName: 'WEBklar/f3' })
aw.seed('websiteProjects', { $id: 'f4p', customerId: 'c1', projectName: 'F4', isPublic: true })
const a = await seedFeature('c1', 'f3p', 'active')
const b = await seedFeature('c1', 'f4p', 'active')
await an.runAnalyseReconcile({ now: NOW })
for (const [p, name, key] of [['f3p', 'F3', a.siteKey], ['f4p', 'F4', b.siteKey]]) {
const d = await doc('c1', p)
assert.equal(d.state, 'off')
assert.ok(stops.includes(key))
assert.equal(removalTopics(name).length, 1)
assert.ok(d.removalTicketId)
}
assert.equal(mails.sent.filter((m) => /beendet/.test(m.subject)).length, 0)
})
test('Abgleich: stopSite wirft => Dokument bleibt unverändert aktiv', async () => {
aw.seed('websiteProjects', { $id: 'f5p', customerId: 'c1', projectName: 'F5', subdomain: 'f5', isPublic: false })
const d = await seedFeature('c1', 'f5p', 'active')
const real = an.analyseDeps.client.stopSite
an.analyseDeps.client.stopSite = async () => { throw new Error('Messdienst weg') }
await an.runAnalyseReconcile({ now: NOW })
an.analyseDeps.client.stopSite = real
const after = await doc('c1', 'f5p')
assert.equal(after.state, 'active')
assert.ok(!after.offSince)
assert.equal(removalTopics('F5').length, 0)
})