feat(tasks): Aufgabe als Git-Issue anlegen, Zustand wandert mit

Die Zuordnung stand im Schema laengst – sourceIssue, sourceUrl und
sourceRepositoryId samt Eindeutigkeit je Repository, gefuellt von der
Uebernahme aus Issues seit v0.9.0. Es fehlte die Gegenrichtung.

Ein Knopf im Aufgabendialog legt das Issue an; Nummer und Adresse landen in
denselben Feldern, der Dublettenschutz gilt also fuer beide Wege. In den
Rumpf kommt ein Rueckverweis auf das Projekt.

Ausdruecklich per Knopf und nie nebenbei: was in ein fremdes System
geschrieben wird, soll jemand gewollt haben. Ist die Aufgabe erst
verknuepft, wandert der Zustand allerdings mit – erledigt schliesst das
Issue, ein Zurueckziehen oeffnet es wieder. Fehlertolerant: ein nicht
erreichbares Git-System darf das Abhaken nicht verhindern.

Die Zustaende passen nicht aufeinander, und das bleibt so. Nebula kennt
vier, ein Issue zwei; nach aussen geht nur die Grenze um ERLEDIGT. Ein
Rundlauf verliert damit IN ARBEIT und BLOCKIERT. Labels waeren die
Alternative, wuerden aber den Issue-Tracker mit Zustaenden fuellen, die
dort niemand pflegt.

Kein Widerspruch zu "keine eigenen Git-Operationen": Issues laufen ueber
die HTTP-API der Provider. Dafuer gibt es jetzt sendJson neben getJson –
durch dieselbe SSRF-Schleuse, aber ohne Umleitungen zu folgen. Ein 307 auf
eine interne Adresse naehme den Rumpf und den Token im Header mit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-15 00:35:11 +02:00
co-authored by Claude Opus 5
parent 6a7365946b
commit e1d435f10a
10 changed files with 553 additions and 3 deletions
+43
View File
@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import { issueShouldBeOpen, statusForIssueState } from "./issues";
describe("issueShouldBeOpen", () => {
it("haelt das Issue offen, solange die Aufgabe nicht erledigt ist", () => {
expect(issueShouldBeOpen("TODO")).toBe(true);
expect(issueShouldBeOpen("DOING")).toBe(true);
expect(issueShouldBeOpen("BLOCKED")).toBe(true);
});
it("schliesst es erst bei ERLEDIGT", () => {
expect(issueShouldBeOpen("DONE")).toBe(false);
});
});
describe("statusForIssueState", () => {
it("macht aus einem geschlossenen Issue eine erledigte Aufgabe", () => {
expect(statusForIssueState(false, "TODO")).toBe("DONE");
expect(statusForIssueState(false, "DOING")).toBe("DONE");
});
it("holt eine erledigte Aufgabe zurueck nach OFFEN", () => {
expect(statusForIssueState(true, "DONE")).toBe("TODO");
});
it("laesst den feineren Zustand stehen, wenn das Issue ohnehin offen ist", () => {
// Ein "opened"-Ereignis zu einer Aufgabe, die schon in Arbeit ist, darf
// sie nicht auf OFFEN zuruecksetzen – IN ARBEIT ist mehr Information.
expect(statusForIssueState(true, "DOING")).toBe("DOING");
expect(statusForIssueState(true, "BLOCKED")).toBe("BLOCKED");
expect(statusForIssueState(true, "TODO")).toBe("TODO");
});
it("bildet den Rundlauf ab, so verlustbehaftet wie er ist", () => {
// IN ARBEIT geht nach draussen als "offen". Kommt es zurueck, bleibt es
// IN ARBEIT – der Zustand ueberlebt also, solange niemand das Issue
// schliesst. Nach dem Schliessen und Wiederoeffnen ist er weg, und das
// ist die bewusst hingenommene Grenze.
const hin = issueShouldBeOpen("DOING");
expect(statusForIssueState(hin, "DOING")).toBe("DOING");
expect(statusForIssueState(true, statusForIssueState(false, "DOING"))).toBe("TODO");
});
});