feat(sync): geschlossene Issues haken ihre Aufgabe auch ohne Webhook ab
CI / pruefen (push) Canceled after 0s
CI / pruefen (push) Canceled after 0s
Behebt florian.wilde/nebula#1. Der Rueckweg "Issue geschlossen -> Aufgabe erledigt" gab es seit v1.7.0 schon, aber nur ueber den Webhook. Ist keiner eingerichtet - und am Repository war keiner eingetragen -, blieb er ganz aus. Damit hing die Funktion an einer Voraussetzung, die man ihr von aussen nicht ansieht. Der Abgleich fragt jetzt den Zustand verknuepfter Issues nach und hakt ab, was in Git geschlossen ist. Beides ergaenzt sich: der Webhook wirkt sofort, der Abgleich verlaesslich. Gespart wird, wo es geht: Nummern, die der gerade geholte Snapshot als offen fuehrt, werden nicht einzeln nachgefragt. Umgekehrt gilt das nicht - die Liste ist gedeckelt, aus dem Fehlen einer Nummer folgt nichts. Deshalb ein Deckel von 25 Nachfragen je Durchgang. **Nur abhaken, nie zuruecknehmen.** Der Abgleich sieht Zustaende, keine Ereignisse. Ein offenes Issue kann auch bedeuten, dass das Schliessen von Nebula aus fehlgeschlagen ist; ein symmetrischer Abgleich wuerde dann eine bewusst erledigte Aufgabe wieder aufmachen. Das Zuruecknehmen bleibt dem Webhook vorbehalten, der ein echtes "reopened" sieht. Ein geloeschtes Issue ist ebenfalls kein erledigtes. Nebenbei eine Unstimmigkeit aus v1.7.0 geradegezogen: der Webhook setzte nur den Status und liess aus, was die Oberflaeche tut - naechste Fassung einer wiederkehrenden Aufgabe, Eintrag im Verlauf. Beide Wege gehen jetzt durch dieselbe Funktion. Geprueft: 232 Unit-Tests, 31 E2E-Tests. Der neue E2E-Test laeuft als einziger wirklich nach draussen - er haengt eine Aufgabe an ein seit Jahren geschlossenes Issue und prueft, dass der Abgleich sie abhakt. Ist GitHub nicht erreichbar, ueberspringt er sich statt rot zu werden. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,13 @@
|
||||
import { expect, test, type APIRequestContext } from "@playwright/test";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
/**
|
||||
* Dieselbe Datenbank wie global-setup.ts. Gebraucht fuer den einen Fall, in
|
||||
* dem ein Zustand herzustellen ist, den die API bewusst nicht anbietet.
|
||||
*/
|
||||
const DATABASE_URL =
|
||||
process.env.E2E_DATABASE_URL ??
|
||||
"postgresql://nebula:nebula@localhost:5432/nebula_e2e_test?schema=public";
|
||||
|
||||
/**
|
||||
* Tests ueber HTTP gegen die laufende Anwendung – also durch Middleware,
|
||||
@@ -325,6 +334,79 @@ test("Die Anmeldung verraet nicht, welche Konten es gibt", async ({ request, pla
|
||||
await request.delete(`/api/admin/users/${id}`);
|
||||
});
|
||||
|
||||
test("Abgleich hakt Aufgaben ab, deren Issue geschlossen wurde", async ({ request }) => {
|
||||
await login(request, ADMIN);
|
||||
|
||||
// Ein oeffentliches Repository mit sicher geschlossenen Issues. Der
|
||||
// Abgleich laeuft anonym gegen die GitHub-API – kein Token noetig.
|
||||
const projekt = (
|
||||
await (
|
||||
await request.post("/api/projects", {
|
||||
data: { name: "Rueckweg", repoUrl: "https://github.com/vercel/swr" },
|
||||
})
|
||||
).json()
|
||||
).project;
|
||||
const repoId = projekt.repositories[0].id;
|
||||
|
||||
const angelegt = await request.post(`/api/projects/${projekt.id}/tasks`, {
|
||||
data: { title: "Haengt an einem geschlossenen Issue" },
|
||||
});
|
||||
const taskId = (await angelegt.json()).task.id;
|
||||
|
||||
/**
|
||||
* Die Verknuepfung direkt in der Datenbank setzen.
|
||||
*
|
||||
* Ueber die API gibt es dafuer keinen Weg, und das ist richtig so: "als
|
||||
* Issue anlegen" wuerde ein neues Issue in einem fremden Repository
|
||||
* erzeugen. Ein Endpunkt nur fuer Tests waere eine Hintertuer im
|
||||
* Produktionscode – dafuer greift der Test lieber selbst zur Datenbank.
|
||||
*
|
||||
* Issue 1 von vercel/swr ist seit Jahren geschlossen.
|
||||
*/
|
||||
const prisma = new PrismaClient({
|
||||
datasources: { db: { url: DATABASE_URL } },
|
||||
});
|
||||
try {
|
||||
await prisma.task.update({
|
||||
where: { id: taskId },
|
||||
data: { sourceRepositoryId: repoId, sourceIssue: 1 },
|
||||
});
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
const vorher = await (await request.get(`/api/projects/${projekt.id}`)).json();
|
||||
const alsAngelegt = vorher.project.tasks.find(
|
||||
(t: { id: string }) => t.id === taskId,
|
||||
);
|
||||
expect(alsAngelegt.sourceIssue).toBe(1);
|
||||
expect(alsAngelegt.status).toBe("TODO");
|
||||
|
||||
// Abgleich anstossen – er fragt den Zustand des Issues nach.
|
||||
const sync = await request.post(`/api/repositories/${repoId}/sync`);
|
||||
const syncBody = await sync.json();
|
||||
|
||||
/**
|
||||
* Dieser Test greift als einziger wirklich nach draussen. Ist GitHub nicht
|
||||
* erreichbar oder das anonyme Rate-Limit ausgeschoepft, sagt er das statt
|
||||
* rot zu werden – ein fremder Dienst darf die Testreihe nicht bestimmen.
|
||||
*/
|
||||
if (!sync.ok()) {
|
||||
test.skip(true, `GitHub nicht erreichbar: ${syncBody.message ?? sync.status()}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const nachher = await (await request.get(`/api/projects/${projekt.id}`)).json();
|
||||
const abgehakt = nachher.project.tasks.find((t: { id: string }) => t.id === taskId);
|
||||
expect(abgehakt.status).toBe("DONE");
|
||||
expect(abgehakt.doneAt).not.toBeNull();
|
||||
|
||||
// Und ein zweiter Lauf aendert nichts mehr – abgehakt bleibt abgehakt,
|
||||
// ohne dass erneut nachgefragt wird.
|
||||
const nochmal = await request.post(`/api/repositories/${repoId}/sync`);
|
||||
expect((await nochmal.json()).message ?? "").not.toMatch(/abgehakt/);
|
||||
});
|
||||
|
||||
test("Webhook: Signatur entscheidet, sonst passiert nichts", async ({ request, playwright }) => {
|
||||
await login(request, ADMIN);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user