ENG-11: workflow-app gebuendelter diensteinstiegspunkt #231
No files matched your search
@@ -0,0 +1,175 @@
|
||||
// Command workflow-app ist der gebuendelte Diensteinstiegspunkt fuer das
|
||||
// NEXARCH-Workflow-Modul (ENG-11): bindet alle bereits fertigen
|
||||
// HTTP-API-Pakete an einen einzigen net/http.ServeMux, analog
|
||||
// connect/cmd/connect-app. Reines Wiring, keine neue Fachlogik.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/approval"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/approvalapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/backchannel"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/backchannelapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/caseapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/caseobj"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/caseoverview"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/caseoverviewapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/escalation"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/escalationnotify"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/moduleadapter"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/notifyclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/reminder"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/routingrule"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/routingruleapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/task"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/tenantregistryclient"
|
||||
)
|
||||
|
||||
func requireEnv(name string) string {
|
||||
v := os.Getenv(name)
|
||||
if v == "" {
|
||||
log.Fatalf("workflow-app: %s muss gesetzt sein", name)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func main() {
|
||||
dsn := requireEnv("NEXARCH_WORKFLOW_APP_TENANT_DSN")
|
||||
serviceToken := requireEnv("NEXARCH_WORKFLOW_APP_SERVICE_TOKEN")
|
||||
tenantSlug := requireEnv("NEXARCH_WORKFLOW_APP_TENANT_SLUG")
|
||||
tenantRegistryURL := requireEnv("NEXARCH_TENANT_REGISTRY_URL")
|
||||
tenantRegistryToken := os.Getenv("NEXARCH_TENANT_REGISTRY_SERVICE_TOKEN")
|
||||
|
||||
addr := os.Getenv("NEXARCH_WORKFLOW_APP_LISTEN_ADDR")
|
||||
if addr == "" {
|
||||
addr = "127.0.0.1:9600"
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
pool, err := pgxpool.New(ctx, dsn)
|
||||
if err != nil {
|
||||
log.Fatalf("workflow-app: datenbankverbindung: %v", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
taskStore := task.NewStore(pool)
|
||||
mux, err := buildMux(pool, taskStore, serviceToken, tenantSlug, tenantRegistryURL, tenantRegistryToken)
|
||||
if err != nil {
|
||||
log.Fatalf("workflow-app: %v", err)
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("workflow-app: listening on %s", addr)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("workflow-app: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
stopEscalation := startEscalationDispatch(ctx, pool, taskStore, tenantSlug)
|
||||
defer stopEscalation()
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-stop
|
||||
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
_ = srv.Shutdown(shutdownCtx)
|
||||
}
|
||||
|
||||
// buildMux verdrahtet alle bestehenden Workflow-API-Pakete auf einen
|
||||
// gemeinsamen ServeMux (Akzeptanzkriterium 1) -- ausgelagert aus main(),
|
||||
// damit ein Test denselben Aufbau real gegen eine Datenbank pruefen kann,
|
||||
// ohne main() zu duplizieren.
|
||||
func buildMux(pool *pgxpool.Pool, taskStore *task.Store, serviceToken, tenantSlug, tenantRegistryURL, tenantRegistryToken string) (*http.ServeMux, error) {
|
||||
engine, err := caseobj.NewEngineWithCase(pool, approval.ProcessDefinition)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prozess-engine aufbauen: %w", err)
|
||||
}
|
||||
caseSvc := caseobj.NewService(pool, engine)
|
||||
approvalSvc := approval.NewService(pool, engine)
|
||||
reminderStore := reminder.NewStore(pool, taskStore)
|
||||
caseoverviewSvc := caseoverview.NewService(caseSvc, reminderStore)
|
||||
tenantVerifier := tenantregistryclient.New(tenantRegistryURL, tenantRegistryToken)
|
||||
backchannelSvc := backchannel.NewService(caseSvc, reminderStore, tenantSlug, tenantVerifier)
|
||||
routingruleStore := routingrule.NewStore(pool)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
|
||||
caseapi.NewServer(caseSvc).RegisterRoutes(mux)
|
||||
approvalapi.NewServer(approvalSvc).RegisterRoutes(mux)
|
||||
caseoverviewapi.NewServer(caseoverviewSvc).RegisterRoutes(mux)
|
||||
backchannelapi.NewServer(backchannelSvc).RegisterRoutes(mux)
|
||||
routingruleapi.NewServer(routingruleStore).RegisterRoutes(mux)
|
||||
moduleadapter.NewServer(pool, serviceToken, tenantSlug).RegisterRoutes(mux)
|
||||
return mux, nil
|
||||
}
|
||||
|
||||
// startEscalationDispatch startet den periodischen TSK-05-Versand
|
||||
// (escalationnotify.RunAt) als Ticker-Goroutine, da escalation.ProcessDue
|
||||
// sonst nie in Produktion aufgerufen wird -- es gibt hier anders als bei
|
||||
// RET-07/BAK-08 keinen systemd-Timer als externen Aufrufer. Die Dispatch-
|
||||
// Konfiguration ist bewusst optional: fehlt sie, startet der Dienst
|
||||
// trotzdem (Fristen/Eskalationen sind kein Kernbetriebsrisiko wie eine
|
||||
// fehlende DSN), nur ohne Benachrichtigungsversand.
|
||||
func startEscalationDispatch(ctx context.Context, pool *pgxpool.Pool, taskStore *task.Store, tenantSlug string) func() {
|
||||
notifyBaseURL := os.Getenv("NEXARCH_WORKFLOW_APP_NOTIFY_BASE_URL")
|
||||
if notifyBaseURL == "" {
|
||||
log.Print("workflow-app: NEXARCH_WORKFLOW_APP_NOTIFY_BASE_URL nicht gesetzt, Eskalations-Benachrichtigung (TSK-05) laeuft nicht")
|
||||
return func() {}
|
||||
}
|
||||
notifyServiceToken := requireEnv("NEXARCH_WORKFLOW_APP_NOTIFY_SERVICE_TOKEN")
|
||||
interval := 5 * time.Minute
|
||||
if raw := os.Getenv("NEXARCH_WORKFLOW_APP_ESCALATION_INTERVAL"); raw != "" {
|
||||
parsed, err := time.ParseDuration(raw)
|
||||
if err != nil {
|
||||
log.Fatalf("workflow-app: NEXARCH_WORKFLOW_APP_ESCALATION_INTERVAL: %v", err)
|
||||
}
|
||||
interval = parsed
|
||||
}
|
||||
|
||||
escalationStore := escalation.NewStore(pool)
|
||||
notifyClient := notifyclient.New(notifyBaseURL, notifyServiceToken)
|
||||
|
||||
loopCtx, cancel := context.WithCancel(ctx)
|
||||
ticker := time.NewTicker(interval)
|
||||
go func() {
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-loopCtx.Done():
|
||||
return
|
||||
case now := <-ticker.C:
|
||||
dispatched, err := escalationnotify.RunAt(loopCtx, escalationStore, taskStore, notifyClient, tenantSlug, nil, now)
|
||||
if err != nil {
|
||||
log.Printf("workflow-app: eskalations-durchlauf fehlgeschlagen: %v", err)
|
||||
continue
|
||||
}
|
||||
if dispatched > 0 {
|
||||
log.Printf("workflow-app: %d eskalation(en) benachrichtigt", dispatched)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
log.Printf("workflow-app: eskalations-benachrichtigung aktiv, intervall %s", interval)
|
||||
return cancel
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/task"
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/testdb"
|
||||
)
|
||||
|
||||
func findModuleRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("getwd: %v", err)
|
||||
}
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
||||
return dir
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
t.Fatal("go.mod nicht gefunden")
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
func testPool(t *testing.T) *pgxpool.Pool {
|
||||
t.Helper()
|
||||
dsn := os.Getenv("TEST_TENANT_DSN")
|
||||
if dsn == "" {
|
||||
t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest uebersprungen")
|
||||
}
|
||||
ctx := context.Background()
|
||||
pool := testdb.Pool(t, dsn)
|
||||
|
||||
migrations, err := migrate.Load(filepath.Join(findModuleRoot(t), "migrations", "tenant"))
|
||||
if err != nil {
|
||||
t.Fatalf("migrationen laden: %v", err)
|
||||
}
|
||||
if _, err := migrate.Up(ctx, pool, migrations); err != nil {
|
||||
t.Fatalf("migrationen anwenden: %v", err)
|
||||
}
|
||||
return pool
|
||||
}
|
||||
|
||||
// Akzeptanzkriterium 1/4: cmd/workflow-app bindet alle Workflow-API-Pakete
|
||||
// an einen einzigen ServeMux, jedes Paket beantwortet mindestens einen
|
||||
// Endpunkt.
|
||||
func TestBuildMux_AllPackagesAnswer(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
taskStore := task.NewStore(pool)
|
||||
mux, err := buildMux(pool, taskStore, "test-service-token", "mandant-a", "http://127.0.0.1:0", "")
|
||||
if err != nil {
|
||||
t.Fatalf("buildMux: %v", err)
|
||||
}
|
||||
ts := httptest.NewServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
do := func(method, path, userID, serviceToken string, body []byte) *http.Response {
|
||||
req, err := http.NewRequest(method, ts.URL+path, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatalf("request bauen: %v", err)
|
||||
}
|
||||
if userID != "" {
|
||||
req.Header.Set("X-User-Id", userID)
|
||||
}
|
||||
if serviceToken != "" {
|
||||
req.Header.Set("X-Service-Token", serviceToken)
|
||||
}
|
||||
if len(body) > 0 {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp, err := ts.Client().Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("%s %s: %v", method, path, err)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// healthz
|
||||
if resp := do(http.MethodGet, "/healthz", "", "", nil); resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("/healthz: status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
|
||||
// caseapi: POST /cases
|
||||
caseBody, _ := json.Marshal(map[string]any{"case_number": "A-1", "responsible": "frank"})
|
||||
respCase := do(http.MethodPost, "/cases", "frank", "", caseBody)
|
||||
if respCase.StatusCode != http.StatusCreated {
|
||||
t.Errorf("caseapi POST /cases: status = %d, want 201", respCase.StatusCode)
|
||||
}
|
||||
|
||||
// approvalapi: GET /approvals
|
||||
if resp := do(http.MethodGet, "/approvals", "frank", "", nil); resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("approvalapi GET /approvals: status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
|
||||
// caseoverviewapi: GET /cases
|
||||
if resp := do(http.MethodGet, "/cases", "frank", "", nil); resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("caseoverviewapi GET /cases: status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
|
||||
// routingruleapi: GET /routing-rules
|
||||
if resp := do(http.MethodGet, "/routing-rules", "frank", "", nil); resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("routingruleapi GET /routing-rules: status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
|
||||
// moduleadapter: GET /registrations
|
||||
if resp := do(http.MethodGet, "/registrations", "", "test-service-token", nil); resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("moduleadapter GET /registrations: status = %d, want 200", resp.StatusCode)
|
||||
}
|
||||
|
||||
// backchannelapi: die tenant-Registry ist in diesem Test nicht real
|
||||
// erreichbar -- Nachweis, dass die Route real gebunden ist (eigene
|
||||
// Fehlermeldung statt generischer 404-Mux-Antwort), reicht fuer AC4.
|
||||
respBC := do(http.MethodGet, "/objects/dms/document/1/context", "", "", nil)
|
||||
if respBC.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("backchannelapi GET /objects/...: status = %d, want 401 (eigene Auth-Antwort, kein Mux-404)", respBC.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// Akzeptanzkriterium 3 / Pflichtpruefung 2: fehlende Pflicht-Umgebungsvariable
|
||||
// fuehrt zu einer klaren Fehlermeldung und keinem offenen Port, kein
|
||||
// stiller Absturz.
|
||||
func TestMain_MissingRequiredEnv_FailsClosedWithClearMessage(t *testing.T) {
|
||||
root := findModuleRoot(t)
|
||||
addr := "127.0.0.1:19601"
|
||||
|
||||
cmd := exec.Command("go", "run", "./cmd/workflow-app")
|
||||
cmd.Dir = root
|
||||
cmd.Env = append(os.Environ(),
|
||||
"NEXARCH_WORKFLOW_APP_LISTEN_ADDR="+addr,
|
||||
// NEXARCH_WORKFLOW_APP_TENANT_DSN bewusst nicht gesetzt.
|
||||
"NEXARCH_WORKFLOW_APP_SERVICE_TOKEN=irrelevant",
|
||||
"NEXARCH_WORKFLOW_APP_TENANT_SLUG=irrelevant",
|
||||
"NEXARCH_TENANT_REGISTRY_URL=http://127.0.0.1:0",
|
||||
)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &out
|
||||
|
||||
err := cmd.Run()
|
||||
if err == nil {
|
||||
t.Fatal("workflow-app haette mit fehlendem NEXARCH_WORKFLOW_APP_TENANT_DSN nicht erfolgreich beenden duerfen")
|
||||
}
|
||||
if !strings.Contains(out.String(), "NEXARCH_WORKFLOW_APP_TENANT_DSN") {
|
||||
t.Errorf("erwarte klare Fehlermeldung zur fehlenden Variable, habe: %s", out.String())
|
||||
}
|
||||
|
||||
conn, dialErr := net.DialTimeout("tcp", addr, 500*time.Millisecond)
|
||||
if dialErr == nil {
|
||||
conn.Close()
|
||||
t.Fatalf("port %s war trotz fehlender Pflichtvariable offen", addr)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
# ENG-11 – Prüfprotokoll: Workflow-App gebündelter Diensteinstiegspunkt
|
||||
|
||||
## Umsetzung
|
||||
|
||||
`workflow/cmd/workflow-app/main.go` bündelt alle bestehenden Workflow-HTTP-API-Pakete
|
||||
auf einem gemeinsamen `net/http.ServeMux`, analog `connect/cmd/connect-app`:
|
||||
|
||||
- `caseapi` (Akte anlegen/lesen/Zustandsübergang)
|
||||
- `approvalapi` (Genehmigungsaufgaben)
|
||||
- `caseoverviewapi` (Akten-/Fristenübersicht)
|
||||
- `backchannelapi` (Rückkanal für DMS/Mail, ENG-09/VOR-04)
|
||||
- `routingruleapi` (Routing-Regeln)
|
||||
- `moduleadapter` (Modul-Registrierung und Ereignisempfang, ENG-09)
|
||||
|
||||
`escalationnotify.RunAt` (TSK-05, Eskalations-Versand über Core CFG-02) läuft
|
||||
in Produktion sonst nie automatisch — es existiert kein systemd-Timer wie bei
|
||||
RET-07/BAK-08 dafür. `main()` startet dafür eine Ticker-Goroutine
|
||||
(`startEscalationDispatch`), sofern die Versand-Konfiguration gesetzt ist.
|
||||
Fehlt sie, startet der Dienst trotzdem (die Versand-Konfiguration ist nicht
|
||||
Kernbetriebsrisiko wie eine fehlende DSN), nur ohne Eskalationsversand — mit
|
||||
klarer Log-Meldung.
|
||||
|
||||
`routingsuggestapi` und `taskapi`/eigenständige `escalationapi` existieren im
|
||||
aktuellen Codestand nicht als HTTP-Pakete (nur `task`/`escalation` als reine
|
||||
Bibliotheken ohne `RegisterRoutes`) — nichts zu verdrahten, aus dem Scope
|
||||
"nur Zusammenführung bestehender Pakete" folgt: keine neuen HTTP-Schichten
|
||||
für sie angelegt.
|
||||
|
||||
## Umgebungsvariablen
|
||||
|
||||
Pflicht (fehlend → `log.Fatal`, klare deutschsprachige Meldung, kein Port offen):
|
||||
|
||||
- `NEXARCH_WORKFLOW_APP_TENANT_DSN`
|
||||
- `NEXARCH_WORKFLOW_APP_SERVICE_TOKEN` (moduleadapter, fail-closed per `svcauth.Equal`)
|
||||
- `NEXARCH_WORKFLOW_APP_TENANT_SLUG` (moduleadapter + backchannel eigener Mandant)
|
||||
- `NEXARCH_TENANT_REGISTRY_URL` (Core-TEN-01-Bestätigung für backchannel, gleiche
|
||||
Konvention wie `archive/cmd/moduleadapter-api`)
|
||||
|
||||
Optional:
|
||||
|
||||
- `NEXARCH_WORKFLOW_APP_LISTEN_ADDR` (Default `127.0.0.1:9600`)
|
||||
- `NEXARCH_TENANT_REGISTRY_SERVICE_TOKEN`
|
||||
- `NEXARCH_WORKFLOW_APP_NOTIFY_BASE_URL` / `NEXARCH_WORKFLOW_APP_NOTIFY_SERVICE_TOKEN`
|
||||
(TSK-05-Versand; ohne Base-URL läuft der Dienst ohne Eskalationsversand weiter)
|
||||
- `NEXARCH_WORKFLOW_APP_ESCALATION_INTERVAL` (Go-Duration, Default `5m`)
|
||||
|
||||
Keine Zugangsdaten im Code — ausschließlich über Umgebungsvariablen (AC2).
|
||||
|
||||
## Automatisierte Tests
|
||||
|
||||
`workflow/cmd/workflow-app/main_test.go`:
|
||||
|
||||
- `TestBuildMux_AllPackagesAnswer` (AC1/AC4): baut denselben Mux wie `main()`
|
||||
gegen eine echte, frisch migrierte Testdatenbank (`TEST_TENANT_DSN`) und ruft
|
||||
je einen Endpunkt pro Paket real über HTTP auf.
|
||||
- `TestMain_MissingRequiredEnv_FailsClosedWithClearMessage` (AC3): startet die
|
||||
Binary als echten Subprozess ohne `NEXARCH_WORKFLOW_APP_TENANT_DSN`, prüft
|
||||
nicht-null Exit-Code, die erwartete Fehlermeldung im Log und dass der
|
||||
konfigurierte Port anschließend nicht erreichbar ist.
|
||||
|
||||
## Prüfungen (Host 192.168.1.204, Wegwerf-DB `eng11_workflow_test`/`eng11_workflow_run`,
|
||||
Rolle `eng11_workflow`, alles nach Abschluss gelöscht)
|
||||
|
||||
| # | Prüfung | Zielwert | Ergebnis |
|
||||
|---|---|---|---|
|
||||
| 1 | Realer Start auf Host 204 mit Wegwerf-DB, ein Aufruf je Paket | jeweils kein 404 vom Mux | `/healthz`=200, `POST /cases`=201 (Akte angelegt), `GET /approvals`=200, `GET /routing-rules`=200, `GET /registrations`=200, `GET /cases`=200, `GET /objects/.../context`=401 (eigene Auth-Antwort, Route real gebunden) |
|
||||
| 2 | Fehlende Pflicht-Umgebungsvariable | Fehlermeldung, Exit≠0, kein Port offen | `NEXARCH_WORKFLOW_APP_TENANT_DSN muss gesetzt sein`, Exit-Code 1, `curl` auf den konfigurierten Port: Verbindung abgelehnt (Code 000) |
|
||||
| 3 | Bestehende Modul-Testsuite bleibt grün | `go test ./...` ohne neue Fehlschläge | `go test -p 1 ./...` (workflow-Modul, serialisiert wegen bekannter paralleler Extension-Race in bestehenden Tests, unabhängig von dieser Änderung): alle Pakete `ok` |
|
||||
|
||||
`go vet ./...` im workflow-Modul: keine Befunde.
|
||||
|
||||
## Entscheidungen
|
||||
|
||||
- **Kein neues `cmd/migrate`**: außerhalb des Tickets (nur Zusammenführung
|
||||
bestehender API-Pakete); Migrationen für die Prüfungen wurden über ein
|
||||
wegwerf-Hilfsprogramm angewendet, nicht Teil des Commits.
|
||||
- **Ticker statt systemd-Timer für TSK-05**: Ticket verlangt Einbindung in
|
||||
`cmd/workflow-app`; ein separates `cmd/`-Binary für den Versand hätte dem
|
||||
Produkt-DNA-Satz "ein Dienst pro Modul" widersprochen.
|
||||
- **Escalation-Dispatch optional, übrige Pflichtvariablen hart**: DSN,
|
||||
Service-Token, Tenant-Slug und Tenant-Registry-URL sind für den
|
||||
Kernbetrieb des Dienstes unverzichtbar; die Versandkonfiguration für
|
||||
TSK-05 ist ein optionales Zusatzfeature, dessen Fehlen den Kern-Dienst
|
||||
nicht verhindern soll.
|
||||
- **`buildMux`-Extraktion aus `main()`**: minimale Auslagerung, damit der
|
||||
Aufbau real (mit Datenbank) testbar ist, ohne die Verdrahtung im Test zu
|
||||
duplizieren; `main()` bleibt sonst strukturell wie `connect-app`.
|
||||
Reference in new issue
Block a user