Zuweisen: Warnungen statt Abbruch, Zuweisung unter der Projekt-Sperre
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a960701d48
commit
5ce8aa6fbb
2 files changed
+211
-38
No files matched your search
@@ -43,6 +43,22 @@ function pushEntry({ sha, at, user, msg }) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wartet, bis `aw.calls` mindestens `count` Eintraege hat - deterministisch, kein fester Timeout.
|
||||
* `calls.push` passiert server-seitig, SOBALD eine Anfrage beim Fake-Appwrite ankommt - also
|
||||
* strikt BEVOR die Antwort beim Aufrufer eintrifft und dieser (falls ueberhaupt) die naechste
|
||||
* Anfrage lostritt. Damit laesst sich `failNext` gezielt auf einen bestimmten von mehreren gleich
|
||||
* aussehenden Aufrufen ansetzen (z. B. das zweite GET desselben Dokuments), ohne zu raten.
|
||||
*/
|
||||
function waitForAppwriteCalls(count) {
|
||||
return new Promise((resolve) => {
|
||||
;(function check() {
|
||||
if (aw.calls.length >= count) return resolve()
|
||||
setImmediate(check)
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
gitea = await startFakeGitea({
|
||||
repos: [
|
||||
@@ -50,9 +66,15 @@ before(async () => {
|
||||
{ full_name: 'JUSN/leer' },
|
||||
{ full_name: 'JUSN/nur-kunde' },
|
||||
{ full_name: 'JUSN/doppelt' },
|
||||
{ full_name: 'WEBklar/ticket-fail' },
|
||||
{ full_name: 'WEBklar/worksheet-fail' },
|
||||
{ full_name: 'JUSN/reread-fail' },
|
||||
{ full_name: 'WEBklar/inlock-fail' },
|
||||
],
|
||||
feeds: {
|
||||
// Newest first, wie der echte Gitea-Feed.
|
||||
'WEBklar/ticket-fail': [pushEntry({ sha: 'tf1', user: 'JUSN', at: '2026-09-27T10:00:00Z', msg: 'Ticket-Fail-Push' })],
|
||||
'WEBklar/worksheet-fail': [pushEntry({ sha: 'wf1', user: 'JUSN', at: '2026-09-27T10:00:00Z', msg: 'Worksheet-Fail-Push' })],
|
||||
'JUSN/D-ner-Nation': [
|
||||
pushEntry({ sha: 's1', user: 'webklar-system', at: '2026-09-27T14:50:00Z', msg: 'System-Push' }),
|
||||
pushEntry({ sha: 'k1', user: 'knso', at: '2026-09-27T14:48:00Z', msg: 'Update preview config from ticket system' }),
|
||||
@@ -73,6 +95,10 @@ before(async () => {
|
||||
{ $id: 'p2', repoFullName: 'JUSN/leer', projectName: 'leer', customerId: '', ticketId: '' },
|
||||
{ $id: 'p3', repoFullName: 'JUSN/nur-kunde', projectName: 'nur-kunde', customerId: '', ticketId: '' },
|
||||
{ $id: 'p4', repoFullName: 'JUSN/doppelt', projectName: 'doppelt', customerId: '', ticketId: '' },
|
||||
{ $id: 'p5', repoFullName: 'WEBklar/ticket-fail', projectName: 'ticket-fail', customerId: '', ticketId: '' },
|
||||
{ $id: 'p6', repoFullName: 'WEBklar/worksheet-fail', projectName: 'worksheet-fail', customerId: '', ticketId: '' },
|
||||
{ $id: 'p7', repoFullName: 'JUSN/reread-fail', projectName: 'reread-fail', customerId: '', ticketId: '' },
|
||||
{ $id: 'p8', repoFullName: 'WEBklar/inlock-fail', projectName: 'inlock-fail', customerId: '', ticketId: '' },
|
||||
],
|
||||
workorders: [{ $id: 't1', woid: '100115' }],
|
||||
customers: [{ $id: 'c1', name: 'Döner Nation' }],
|
||||
@@ -123,9 +149,34 @@ test('nur Kunde ohne Pushes: kein Ticket', async () => {
|
||||
assert.equal(aw.all('workorders').length, before)
|
||||
})
|
||||
test('nur Kunde mit Pushes: Webpage-Ticket entsteht', async () => {
|
||||
const r = await assignProject('p3', { customerId: 'c1' }, { now })
|
||||
const t = aw.all('workorders').find((w) => w.$id === aw.get('websiteProjects', 'p3').ticketId)
|
||||
// ticketAdmin loggt die automatische Ticket-Anlage (console.log) - hier abfangen, damit die
|
||||
// Testausgabe sauber bleibt (Ruling, Fix-Runde 1).
|
||||
const originalLog = console.log
|
||||
console.log = () => {}
|
||||
let r
|
||||
try {
|
||||
r = await assignProject('p3', { customerId: 'c1' }, { now })
|
||||
} finally {
|
||||
console.log = originalLog
|
||||
}
|
||||
const stored = aw.get('websiteProjects', 'p3')
|
||||
const t = aw.all('workorders').find((w) => w.$id === stored.ticketId)
|
||||
assert.equal(t.type, 'Webpage'); assert.equal(r.backfilled, 1)
|
||||
// I2: die Antwort muss die gerade erst automatisch angelegte ticketId zeigen, nicht den Stand
|
||||
// von vor dem Anlegen.
|
||||
assert.equal(r.project.ticketId, stored.ticketId)
|
||||
})
|
||||
test('Kunde erneut zuweisen ohne Ticket: kein zweites Ticket, kein doppeltes Arbeitsblatt', async () => {
|
||||
// p3 hat aus dem vorigen Test bereits ein automatisch angelegtes Ticket - ein verzoegerter
|
||||
// zweiter Klick (gleicher Kunde, keine ticketId) darf es nicht durch ein leeres ueberschreiben
|
||||
// (Review I3). "Verzoegert" reicht hier als einfaches Abwarten des ersten Aufrufs.
|
||||
const before = { workorders: aw.all('workorders').length, worksheets: aw.all('worksheets').length }
|
||||
const ticketIdBefore = aw.get('websiteProjects', 'p3').ticketId
|
||||
const r = await assignProject('p3', { customerId: 'c1' }, { now })
|
||||
assert.equal(aw.all('workorders').length, before.workorders)
|
||||
assert.equal(aw.all('worksheets').length, before.worksheets)
|
||||
assert.equal(aw.get('websiteProjects', 'p3').ticketId, ticketIdBefore)
|
||||
assert.equal(r.project.ticketId, ticketIdBefore)
|
||||
})
|
||||
test('Doppelklick: ein Umzug, keine doppelten Arbeitsblätter', async () => {
|
||||
await Promise.all([assignProject('p4', { customerId: 'c1', ticketId: 't1' }, { now }), assignProject('p4', { customerId: 'c1', ticketId: 't1' }, { now })])
|
||||
@@ -155,3 +206,48 @@ test('Gitea-Ausfall: Zuweisung bleibt', async () => {
|
||||
test('unbekanntes Projekt', async () => {
|
||||
await assert.rejects(assignProject('nope', { ticketId: 't1' }, { now }), (e) => e.status === 404)
|
||||
})
|
||||
test('Appwrite-Ausfall beim Ticket-Anlegen: Zuweisung bleibt, Warnung statt Abbruch', async () => {
|
||||
aw.failNext({ method: 'POST', pathIncludes: 'workorders', status: 500 })
|
||||
const r = await assignProject('p5', { customerId: 'c1' }, { now })
|
||||
assert.equal(r.ok, true)
|
||||
assert.equal(aw.get('websiteProjects', 'p5').customerId, 'c1')
|
||||
assert.match(r.warnings[0], /^Pushes konnten nicht nachgetragen werden \(/)
|
||||
})
|
||||
test('Appwrite-Ausfall beim Arbeitsblatt-Anlegen: Zuweisung bleibt, Warnung statt Abbruch', async () => {
|
||||
aw.failNext({ method: 'POST', pathIncludes: 'worksheets', status: 500 })
|
||||
const r = await assignProject('p6', { customerId: 'c1', ticketId: 't1' }, { now })
|
||||
assert.equal(r.ok, true)
|
||||
assert.equal(aw.get('websiteProjects', 'p6').ticketId, 't1')
|
||||
assert.match(r.warnings[0], /^Pushes konnten nicht nachgetragen werden \(/)
|
||||
})
|
||||
test('Nachlesen nach Umzug schlaegt fehl: Antwort zeigt trotzdem den neuen Namen', async () => {
|
||||
// p7 zieht hier tatsaechlich um (JUSN -> WEBklar); das GET danach (Controller-Ruling R13/R14)
|
||||
// soll scheitern. Gezielt getimt: warten, bis das GET VOR dem Umzug und das PATCH AUS dem
|
||||
// Umzug (`updateProjectDocument` in repoMove.js) durch sind, dann erst failNext scharf machen -
|
||||
// so trifft es sicher das GET NACH dem Umzug und keines der beiden vorherigen.
|
||||
const target = aw.calls.length + 2
|
||||
const p = reconcileProject('p7', { now })
|
||||
await waitForAppwriteCalls(target)
|
||||
aw.failNext({ method: 'GET', pathIncludes: 'p7', status: 500 })
|
||||
const r = await p
|
||||
assert.deepEqual(r.moved, { from: 'JUSN/reread-fail', to: 'WEBklar/reread-fail' })
|
||||
assert.deepEqual(r.warnings, [])
|
||||
assert.equal(r.project.repoFullName, 'WEBklar/reread-fail')
|
||||
assert.equal(r.project.giteaRepoUrl, `${gitea.baseUrl}/WEBklar/reread-fail`)
|
||||
assert.equal(r.project.giteaRepoName, 'reread-fail')
|
||||
})
|
||||
test('Appwrite-Fehler beim Nachlesen unter der Sperre in assignProject: Zuweisung bleibt, Warnung statt Abbruch', async () => {
|
||||
// Gezielt getimt wie oben: warten, bis die Existenzpruefung und das Speichern der Zuweisung
|
||||
// (beide in `assignProject`) durch sind, dann erst failNext scharf machen - so trifft es sicher
|
||||
// das GET, das der Reconcile-Kern (`reconcileProjectLocked`) direkt danach selbst macht.
|
||||
const target = aw.calls.length + 2
|
||||
const p = assignProject('p8', { customerId: 'c1' }, { now })
|
||||
await waitForAppwriteCalls(target)
|
||||
aw.failNext({ method: 'GET', pathIncludes: 'p8', status: 500 })
|
||||
const r = await p
|
||||
assert.equal(r.ok, true)
|
||||
assert.equal(r.moved, null)
|
||||
assert.equal(r.backfilled, 0)
|
||||
assert.equal(aw.get('websiteProjects', 'p8').customerId, 'c1')
|
||||
assert.match(r.warnings[0], /^Zuweisung gespeichert, aber Umzug und Nachtragen sind fehlgeschlagen \(/)
|
||||
})
|
||||
Reference in new issue
Block a user