Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd5ca8da53 |
No files matched your search
@@ -1,2 +0,0 @@
|
||||
# common/ ist modulübergreifend: Änderungen brauchen Review (Auswirkungsliste im PR)
|
||||
/common/ @patrick
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "archive/**"
|
||||
- "common/**"
|
||||
|
||||
jobs:
|
||||
pflichttest-gate:
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
name: common Pflichttest-Gate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "common/**"
|
||||
|
||||
jobs:
|
||||
pflichttest-gate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
- name: Gate bauen
|
||||
working-directory: common
|
||||
run: go build -o /tmp/pflichttestgate ./cmd/pflichttestgate
|
||||
- name: Geänderte Dateien gegen Pflichttest-Regel pruefen
|
||||
run: |
|
||||
git diff --name-only "origin/${{ github.base_ref }}...HEAD" -- common/ | /tmp/pflichttestgate
|
||||
@@ -1,33 +0,0 @@
|
||||
name: common Versions-Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "common/**"
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
- name: Build, Vet, Test
|
||||
working-directory: common
|
||||
run: |
|
||||
go build ./...
|
||||
go vet ./...
|
||||
go test ./... -count=1
|
||||
- name: Naechsten Tag setzen
|
||||
run: |
|
||||
minor=$(tr -d '[:space:]' < common/MINOR)
|
||||
last=$(git tag -l "common/v0.${minor}.*" --sort=-v:refname | head -n1)
|
||||
if [ -z "$last" ]; then next="common/v0.${minor}.0"; else
|
||||
patch=${last##*.}; base=${last%.*}; next="${base}.$((patch + 1))"
|
||||
fi
|
||||
git tag "$next"
|
||||
git push origin "$next"
|
||||
@@ -1,37 +0,0 @@
|
||||
name: DB-Integrationstests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "common/**"
|
||||
- "archive/**"
|
||||
- "ai/**"
|
||||
- "connect/**"
|
||||
- "scripts/ci-pg-cluster.sh"
|
||||
- ".gitea/workflows/db-tests.yml"
|
||||
|
||||
jobs:
|
||||
db-tests:
|
||||
runs-on: nexarch-test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
- name: Wegwerf-Postgres starten
|
||||
run: scripts/ci-pg-cluster.sh start
|
||||
- name: common
|
||||
working-directory: common
|
||||
run: go test ./... -count=1 -p 1
|
||||
- name: archive
|
||||
working-directory: archive
|
||||
run: go test ./... -count=1 -p 1
|
||||
- name: ai
|
||||
working-directory: ai
|
||||
run: go test ./... -count=1 -p 1
|
||||
- name: connect
|
||||
working-directory: connect
|
||||
run: go test ./... -count=1 -p 1
|
||||
- name: Wegwerf-Postgres stoppen
|
||||
if: always()
|
||||
run: scripts/ci-pg-cluster.sh stop
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "mail/**"
|
||||
- "common/**"
|
||||
|
||||
jobs:
|
||||
pflichttest-gate:
|
||||
|
||||
@@ -604,567 +604,3 @@ Keine Commits in dieser Session.
|
||||
- scripts/qa02-foundation-gate.sh | 2 +-
|
||||
|
||||
---
|
||||
## 2026-10-19 12:07 – 12:07 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/audit-log/app/page.tsx | 6 +++---
|
||||
- web/dms-browser/app/layout.tsx | 6 +++---
|
||||
- web/lic-admin/app/page.tsx | 18 +++++++++---------
|
||||
- web/portal/app/layout.tsx | 12 ++----------
|
||||
- web/portal/app/page.tsx | 4 ++--
|
||||
- web/shl/components/ModuleNavShell.tsx | 18 +++++++++++++++---
|
||||
- web/shl/components/Shell.tsx | 2 ++
|
||||
- web/shl/index.ts | 2 +-
|
||||
- web/shl/tokens/tokens.ts | 24 ++++++++++++++++++++++++
|
||||
- web/status-page/app/page.tsx | 12 ++++++------
|
||||
- web/tenant-admin/app/page.tsx | 8 ++++----
|
||||
|
||||
---
|
||||
## 2026-10-19 12:08 – 12:08 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/audit-log/app/page.tsx | 6 +++---
|
||||
- web/dms-browser/app/layout.tsx | 6 +++---
|
||||
- web/lic-admin/app/page.tsx | 18 +++++++++---------
|
||||
- web/portal/app/layout.tsx | 12 ++----------
|
||||
- web/portal/app/page.tsx | 4 ++--
|
||||
- web/shl/components/ModuleNavShell.tsx | 18 +++++++++++++++---
|
||||
- web/shl/components/Shell.tsx | 2 ++
|
||||
- web/shl/index.ts | 2 +-
|
||||
- web/shl/tokens/tokens.ts | 24 ++++++++++++++++++++++++
|
||||
- web/status-page/app/page.tsx | 12 ++++++------
|
||||
- web/tenant-admin/app/page.tsx | 8 ++++----
|
||||
|
||||
---
|
||||
## 2026-10-19 12:14 – 12:15 (1m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/audit-log/app/page.tsx | 6 +++---
|
||||
- web/dms-browser/app/layout.tsx | 6 +++---
|
||||
- web/lic-admin/app/page.tsx | 18 +++++++++---------
|
||||
- web/portal/app/layout.tsx | 12 ++----------
|
||||
- web/portal/app/page.tsx | 4 ++--
|
||||
- web/shl/components/ModuleNavShell.tsx | 18 +++++++++++++++---
|
||||
- web/shl/components/Shell.tsx | 2 ++
|
||||
- web/shl/index.ts | 2 +-
|
||||
- web/shl/tokens/tokens.ts | 24 ++++++++++++++++++++++++
|
||||
- web/status-page/app/page.tsx | 12 ++++++------
|
||||
- web/tenant-admin/app/page.tsx | 8 ++++----
|
||||
|
||||
---
|
||||
## 2026-10-19 12:29 – 12:30 (1m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- dms/internal/upload/service.go | 38 ++++++++++++++++++++++++++++++---
|
||||
- dms/internal/upload/service_test.go | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-10-19 12:31 – 12:31 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- dms/internal/upload/service.go | 25 ++++++++++++++++++++++++-
|
||||
- dms/internal/upload/service_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-10-19 12:44 – 12:53 (8m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** banking
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- dms/internal/upload/service.go | 25 ++++++++++++++++++++++++-
|
||||
- dms/internal/upload/service_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-10-19 12:54 – 12:54 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- dms/internal/upload/service.go | 25 ++++++++++++++++++++++++-
|
||||
- dms/internal/upload/service_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
---
|
||||
## 2026-10-19 13:47 – 13:50 (2m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** banking
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/mail-postfach/app/layout.tsx | 8 +++++---
|
||||
- web/shl/components/ModuleNavShell.tsx | 4 ++++
|
||||
|
||||
---
|
||||
## 2026-10-19 14:49 – 14:50 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** archivmail
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- MIGRATIONSKONZEPT.md | 2 +-
|
||||
- PRODUKTARCHITEKTUR.md | 2 +-
|
||||
- SICHERHEITSKONZEPT.md | 16 ++++++++--------
|
||||
- SKALIERUNGSKONZEPT.md | 2 +-
|
||||
- STORAGE-KONZEPT.md | 4 ++--
|
||||
- dms/internal/searchsync/searchsync_test.go | 4 ++--
|
||||
- web/portal/app/ModuleGrid.test.tsx | 3 ++-
|
||||
- web/portal/app/ModuleGrid.tsx | 17 +----------------
|
||||
- web/portal/lib/modules.test.ts | 4 +++-
|
||||
- web/shl/components/ModuleNavShell.tsx | 27 +++++++++++++++++++--------
|
||||
- web/shl/index.ts | 2 +-
|
||||
|
||||
---
|
||||
## 2026-10-19 14:58 – 23:11 (8h 12m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-20 04:03 – 04:03 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-20 04:03 – 04:03 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-20 04:03 – 04:03 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-20 04:03 – 04:03 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-20 04:03 – 04:03 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 11:18 – 11:18 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 11:20 – 11:20 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 11:54 – 11:54 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 11:55 – 11:55 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 11:57 – 11:57 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 11:58 – 11:58 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 12:37 – 12:38 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 12:38 – 12:38 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 12:40 – 12:40 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 12:42 – 12:42 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 12:42 – 12:44 (1m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 12:47 – 12:52 (4m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 12:53 – 12:54 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 12:55 – 12:56 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 13:00 – 13:01 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** Trad-Bot
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 13:35 – 13:39 (4m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 13:42 – 13:52 (10m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 13:53 – 13:53 (0m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 13:54 – 14:06 (12m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 14:08 – 14:41 (32m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 15:03 – 15:25 (22m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
## 2026-10-27 15:27 – 15:43 (15m)
|
||||
**Beschreibung:** Claude Code Session
|
||||
**Projekt:** nexarch
|
||||
|
||||
### Commits
|
||||
Keine Commits in dieser Session.
|
||||
|
||||
### Geänderte Dateien
|
||||
- web/dms-browser/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/dms-upload/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/pdf-viewer/components/AccountBar.tsx | 14 +++++++-------
|
||||
- web/shl/components/AccountBar.tsx | 22 ++++++++++++++++++++++
|
||||
- web/shl/index.ts | 3 +++
|
||||
|
||||
---
|
||||
+1
-12
@@ -6,17 +6,6 @@ Stand: 2026-08-26. Zusammenführung aller bisherigen Entscheidungen aus `nexarch
|
||||
|
||||
NEXARCH ist die Nachfolgeplattform von **archivmail** (E-Mail-Archivierung) und **archivdms** (Dokumentenmanagement) — nicht als Umbenennung, sondern als modulare Neuentwicklung unter der Positionierung **"Next Generation Information Management"**: E-Mails, Dokumente, digitale Archivierung, OCR, Volltextsuche, Workflows, Compliance, Integrationen, KI — als ein zusammenhängendes System, nicht als zwei getrennte Werkzeuge nebeneinander.
|
||||
|
||||
## 1a. Vorbildsysteme
|
||||
|
||||
NEXARCH vereint die Stärken bestehender Systeme und vermeidet deren bekannte Schwächen (Details: `dms-kanban/known-issues-archivdms.md`, `dms-kanban/known-issues-archivmail.md`).
|
||||
|
||||
| Bereich | Vorbildsysteme |
|
||||
|---|---|
|
||||
| Mail | **archivmail** (eigenes Produkt, Vorgänger), **MailPiler** |
|
||||
| DMS | **paperless-ngx**, **Alfresco**, **Docspell**, **ecoDMS**, **archivdms** (eigenes Produkt, Vorgänger) |
|
||||
|
||||
Weitere Vergleichsprojekte (z. B. OpenKM, Nuxeo, Mayan EDMS, Seafile) sind noch zu prüfen. Für das DMS-Modul siehe auch `dms/README.md`.
|
||||
|
||||
## 2. Architekturprinzip
|
||||
|
||||
**Ein modularer Monolith aus sieben eigenständigen, aber koordinierten Diensten** — kein Microservice-Zwang, aber klare Dienstgrenzen mit eigenem Datenmodell, eigenem Go-Binary, eigenem Kanban-Board:
|
||||
@@ -75,7 +64,7 @@ Kein Modul kopiert die Daten eines anderen — stattdessen ein wiederkehrendes *
|
||||
|
||||
Gemeinsamer Nenner aller drei: **Referenz statt Kopie, Adapter statt Direktkopplung.**
|
||||
|
||||
Go-`internal/`-Pakete sind zwischen den Modulen nicht sichtbar; identische Querschnittspakete liegen deshalb im versionierten Modul `common/` (Git-Tags `common/vX.Y.Z`, siehe `common/README.md`). So bleiben Module einzeln baubar (Docker, eigenes Repo, Partner-Build). Fachlich verschiedene Pakete (`audittrail`, `cutoverstage`, `crypto`) bleiben modul-lokal.
|
||||
Go-`internal/`-Pakete sind zwischen den Modulen nicht sichtbar; identische Querschnittspakete werden deshalb im geteilten Modul `common/` gehalten (per `replace` in den `go.mod` eingebunden, keine eigenen Abhängigkeiten außer der Standardbibliothek). Pilot: `common/httpclient`, `common/apierror`. Fachlich verschiedene Pakete (`audittrail`, `cutoverstage`, `crypto`) bleiben modul-lokal.
|
||||
|
||||
Dasselbe Prinzip gilt auch innerhalb eines Moduls, auf Code-Ebene: Hauptdateien/Entry-Points bleiben klein, Fachlogik wandert in eigene Module. Vor einer neuen Funktions-/Modul-Datei wird geprüft, ob dieselbe Funktionalität nicht schon woanders existiert (auch modulübergreifend, z. B. in Core) — keine doppelten Funktionsdateien. Das eigene Kanban-Planungswerkzeug wurde nach genau diesem Muster korrigiert (sieben `kanban.html` enthielten identisches JS/CSS als Volltext-Duplikat, damals in `_shared/kanban.js`/`kanban.css` ausgelagert) — die Boards selbst sind inzwischen komplett aus dem Repo entfernt (Ticket-Historie liegt in der Gitea-PR-Historie), das Prinzip "keine doppelten Funktionsdateien" gilt unverändert für den eigentlichen NEXARCH-Code.
|
||||
|
||||
|
||||
+17
-45
@@ -41,10 +41,9 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearchapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/summarize"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/summaryapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/tenantregistryclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/threadsummarize"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/vectorindex"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/tenantregistryclient"
|
||||
)
|
||||
|
||||
const workerPollInterval = 2 * time.Second
|
||||
@@ -53,24 +52,13 @@ const defaultListenAddr = "127.0.0.1:9600"
|
||||
const defaultModel = "default"
|
||||
|
||||
type config struct {
|
||||
dsn string
|
||||
serviceToken string
|
||||
addr string
|
||||
model string
|
||||
moduleURLs map[string]string
|
||||
coreBaseURL string
|
||||
coreServiceToken string
|
||||
policyURL string
|
||||
policyServiceToken string
|
||||
policyDisabled bool
|
||||
}
|
||||
|
||||
// policyClient liefert nil nur mit NEXARCH_AI_APP_POLICY_DISABLED=1.
|
||||
func (c config) policyClient() *policyclient.Client {
|
||||
if c.policyURL == "" {
|
||||
return nil
|
||||
}
|
||||
return policyclient.New(c.policyURL, c.policyServiceToken)
|
||||
dsn string
|
||||
serviceToken string
|
||||
addr string
|
||||
model string
|
||||
moduleURLs map[string]string
|
||||
coreBaseURL string
|
||||
coreServiceToken string
|
||||
}
|
||||
|
||||
// parseModuleBaseURLs liest "modul=url,modul2=url2" (EXT-05: je Modul eine
|
||||
@@ -96,19 +84,13 @@ func parseModuleBaseURLs(raw string) map[string]string {
|
||||
// Programmabbruchs weiter unten im Code.
|
||||
func loadConfig(getenv func(string) string) (config, error) {
|
||||
cfg := config{
|
||||
dsn: getenv("NEXARCH_AI_APP_TENANT_DSN"),
|
||||
serviceToken: getenv("NEXARCH_AI_APP_SERVICE_TOKEN"),
|
||||
addr: getenv("NEXARCH_AI_APP_LISTEN_ADDR"),
|
||||
model: getenv("NEXARCH_AI_APP_MODEL"),
|
||||
moduleURLs: parseModuleBaseURLs(getenv("NEXARCH_AI_APP_MODULE_BASE_URLS")),
|
||||
coreBaseURL: getenv("NEXARCH_AI_APP_CORE_BASE_URL"),
|
||||
coreServiceToken: getenv("NEXARCH_AI_APP_CORE_SERVICE_TOKEN"),
|
||||
policyURL: getenv("NEXARCH_AI_APP_POLICY_URL"),
|
||||
policyServiceToken: getenv("NEXARCH_AI_APP_POLICY_SERVICE_TOKEN"),
|
||||
policyDisabled: getenv("NEXARCH_AI_APP_POLICY_DISABLED") == "1",
|
||||
}
|
||||
if cfg.policyURL != "" && cfg.policyServiceToken == "" {
|
||||
return config{}, errors.New("ai-app: NEXARCH_AI_APP_POLICY_SERVICE_TOKEN ist erforderlich, wenn NEXARCH_AI_APP_POLICY_URL gesetzt ist")
|
||||
dsn: getenv("NEXARCH_AI_APP_TENANT_DSN"),
|
||||
serviceToken: getenv("NEXARCH_AI_APP_SERVICE_TOKEN"),
|
||||
addr: getenv("NEXARCH_AI_APP_LISTEN_ADDR"),
|
||||
model: getenv("NEXARCH_AI_APP_MODEL"),
|
||||
moduleURLs: parseModuleBaseURLs(getenv("NEXARCH_AI_APP_MODULE_BASE_URLS")),
|
||||
coreBaseURL: getenv("NEXARCH_AI_APP_CORE_BASE_URL"),
|
||||
coreServiceToken: getenv("NEXARCH_AI_APP_CORE_SERVICE_TOKEN"),
|
||||
}
|
||||
if cfg.dsn == "" {
|
||||
return config{}, errors.New("ai-app: NEXARCH_AI_APP_TENANT_DSN ist erforderlich")
|
||||
@@ -116,9 +98,6 @@ func loadConfig(getenv func(string) string) (config, error) {
|
||||
if cfg.serviceToken == "" {
|
||||
return config{}, errors.New("ai-app: NEXARCH_AI_APP_SERVICE_TOKEN ist erforderlich")
|
||||
}
|
||||
if cfg.policyURL == "" && !cfg.policyDisabled {
|
||||
return config{}, errors.New("ai-app: NEXARCH_AI_APP_POLICY_URL ist erforderlich (oder NEXARCH_AI_APP_POLICY_DISABLED=1 als Uebergang)")
|
||||
}
|
||||
if cfg.addr == "" {
|
||||
cfg.addr = defaultListenAddr
|
||||
}
|
||||
@@ -150,11 +129,7 @@ func build(pool *pgxpool.Pool, cfg config, prov provider.Provider, workerID stri
|
||||
).RegisterRoutes(mux)
|
||||
|
||||
searchSvc := semanticsearch.New(prov, vectorindex.New(pool), cfg.model)
|
||||
policy := cfg.policyClient()
|
||||
if policy == nil {
|
||||
log.Print("ai-app: WARNUNG: Rechtepruefung ai.search.read deaktiviert (NEXARCH_AI_APP_POLICY_DISABLED=1)")
|
||||
}
|
||||
semanticsearchapi.NewServer(searchSvc, cfg.serviceToken).WithPolicy(policy).RegisterRoutes(mux)
|
||||
semanticsearchapi.NewServer(searchSvc, cfg.serviceToken).RegisterRoutes(mux)
|
||||
|
||||
chunkStore := vectorindex.New(pool)
|
||||
docchatapi.NewServer(
|
||||
@@ -202,9 +177,6 @@ func build(pool *pgxpool.Pool, cfg config, prov provider.Provider, workerID stri
|
||||
return wiring{mux: mux, queue: q, worker: worker, quotaStore: quotaStore}
|
||||
}
|
||||
|
||||
// Muss ueber dem Provider-Timeout (120s) liegen, sonst kappt der Server langsame Inferenz-Antworten.
|
||||
const serverWriteTimeout = 130 * time.Second
|
||||
|
||||
func main() {
|
||||
cfg, err := loadConfig(os.Getenv)
|
||||
if err != nil {
|
||||
@@ -229,7 +201,7 @@ func main() {
|
||||
Handler: w.mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: serverWriteTimeout,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/mock"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/queue"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
// AK3 + AK4: eine fehlende Pflicht-Umgebungsvariable liefert einen klaren
|
||||
@@ -44,36 +44,10 @@ func TestLoadConfig_MissingRequiredVars(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Issue #369: Policy-URL ohne Service-Token darf nicht still ohne
|
||||
// Rechtepruefung starten.
|
||||
func TestLoadConfig_PolicyURLRequiresServiceToken(t *testing.T) {
|
||||
func TestLoadConfig_Defaults(t *testing.T) {
|
||||
env := map[string]string{
|
||||
"NEXARCH_AI_APP_TENANT_DSN": "postgres://x",
|
||||
"NEXARCH_AI_APP_SERVICE_TOKEN": "tok",
|
||||
"NEXARCH_AI_APP_POLICY_URL": "http://127.0.0.1:8093",
|
||||
}
|
||||
get := func(k string) string { return env[k] }
|
||||
if _, err := loadConfig(get); err == nil || !strings.Contains(err.Error(), "NEXARCH_AI_APP_POLICY_SERVICE_TOKEN") {
|
||||
t.Fatalf("erwartete fehler zur fehlenden NEXARCH_AI_APP_POLICY_SERVICE_TOKEN, bekam %v", err)
|
||||
}
|
||||
env["NEXARCH_AI_APP_POLICY_SERVICE_TOKEN"] = "policy-tok"
|
||||
cfg, err := loadConfig(get)
|
||||
if err != nil {
|
||||
t.Fatalf("unerwarteter fehler: %v", err)
|
||||
}
|
||||
if cfg.policyClient() == nil {
|
||||
t.Fatal("policy-client muss bei gesetzter URL aktiv sein")
|
||||
}
|
||||
if (config{}).policyClient() != nil {
|
||||
t.Fatal("ohne URL darf kein policy-client entstehen")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_Defaults(t *testing.T) {
|
||||
env := map[string]string{
|
||||
"NEXARCH_AI_APP_TENANT_DSN": "postgres://x",
|
||||
"NEXARCH_AI_APP_SERVICE_TOKEN": "tok",
|
||||
"NEXARCH_AI_APP_POLICY_DISABLED": "1",
|
||||
}
|
||||
cfg, err := loadConfig(func(k string) string { return env[k] })
|
||||
if err != nil {
|
||||
@@ -87,12 +61,6 @@ func TestLoadConfig_Defaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerWriteTimeoutCoversProviderTimeout(t *testing.T) {
|
||||
if serverWriteTimeout <= 120*time.Second {
|
||||
t.Fatalf("serverWriteTimeout = %v, muss ueber dem Provider-Timeout von 120s liegen", serverWriteTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestPool(t *testing.T) *pgxpool.Pool {
|
||||
t.Helper()
|
||||
dsn := os.Getenv("TEST_TENANT_DSN")
|
||||
@@ -216,18 +184,3 @@ func TestBuild_QueueWorkerProcessesJobWithQuota(t *testing.T) {
|
||||
t.Errorf("quota-zaehlerstand = %d, want 1 (GOV-04-Verbrauchszaehlung nicht verdrahtet)", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_PolicyRequiredUnlessDisabled(t *testing.T) {
|
||||
env := map[string]string{
|
||||
"NEXARCH_AI_APP_TENANT_DSN": "postgres://x",
|
||||
"NEXARCH_AI_APP_SERVICE_TOKEN": "tok",
|
||||
}
|
||||
get := func(k string) string { return env[k] }
|
||||
if _, err := loadConfig(get); err == nil || !strings.Contains(err.Error(), "NEXARCH_AI_APP_POLICY_URL") {
|
||||
t.Fatalf("erwartete fehler zur fehlenden NEXARCH_AI_APP_POLICY_URL, bekam %v", err)
|
||||
}
|
||||
env["NEXARCH_AI_APP_POLICY_DISABLED"] = "1"
|
||||
if _, err := loadConfig(get); err != nil {
|
||||
t.Fatalf("mit POLICY_DISABLED=1 darf der Start nicht scheitern: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
fixtureTenant = "beispiel-mandant"
|
||||
fixtureTenant = "beispiel-mandant"
|
||||
fixtureSourceType = "dms_document"
|
||||
fixtureSourceID = "beispiel-dokument-1"
|
||||
)
|
||||
@@ -77,6 +77,5 @@ func main() {
|
||||
docchatapi.NewServer(chatSvc, "test-token").RegisterRoutes(mux)
|
||||
|
||||
log.Println("searchchatapi-mockserver listening on 127.0.0.1:19603")
|
||||
// archtest:allow Einmaliges lokales pa11y-Testwerkzeug, kein Produktdienst
|
||||
log.Fatal(http.ListenAndServe("127.0.0.1:19603", mux))
|
||||
}
|
||||
@@ -4,10 +4,7 @@ go 1.25.0
|
||||
|
||||
toolchain go1.25.13
|
||||
|
||||
require (
|
||||
gitea.perlbach24.de/scripte/nexarch/common v0.2.0
|
||||
github.com/jackc/pgx/v5 v5.11.0
|
||||
)
|
||||
require github.com/jackc/pgx/v5 v5.11.0
|
||||
|
||||
require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
gitea.perlbach24.de/scripte/nexarch/common v0.2.0 h1:gcOjoOvUkT0Zmqq3m7WQp1MfXSgmzrGTF2E/+NkO2bw=
|
||||
gitea.perlbach24.de/scripte/nexarch/common v0.2.0/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// Package auditclient ist ein schlanker HTTP-Client fuer Core AUD-08
|
||||
// (internal/audit, POST /audit/ingest). AI ist ein physisch getrenntes
|
||||
// Go-Modul und kann Cores internal/audit daher nicht direkt importieren -
|
||||
// nur ueber dessen HTTP-Schnittstelle. Gleiches Muster wie
|
||||
// connect/internal/auditclient (ENG-06) und archive/internal/auditclient.
|
||||
package auditclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/httpclient"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
ServiceToken string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL, serviceToken string) *Client {
|
||||
return &Client{BaseURL: baseURL, ServiceToken: serviceToken, HTTPClient: httpclient.Default()}
|
||||
}
|
||||
|
||||
type ingestRequest struct {
|
||||
TenantSlug string `json:"tenant_slug"`
|
||||
Actor string `json:"actor"`
|
||||
Action string `json:"action"`
|
||||
Target string `json:"target"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
// Ingest meldet EIN Audit-Ereignis an Core. Absichtlich keine
|
||||
// Update-/Delete-Methode: der Ingest-Endpunkt (AUD-08) ruft denselben
|
||||
// alleinigen, anfuegenden Schreibpfad wie core-interne Aufrufer auf
|
||||
// (audit.Log.Record), gegen den Core selbst per Datenbank-Trigger jede
|
||||
// nachtraegliche Aenderung/Loeschung verweigert (AUD-02).
|
||||
func (c *Client) Ingest(ctx context.Context, tenantSlug, actor, action, target string, metadata map[string]any) error {
|
||||
body, err := json.Marshal(ingestRequest{
|
||||
TenantSlug: tenantSlug, Actor: actor, Action: action, Target: target, Metadata: metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("auditclient: request kodieren: %w", err)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/audit/ingest", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("auditclient: request bauen: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Service-Token", c.ServiceToken)
|
||||
|
||||
resp, err := c.httpClient().Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("auditclient: aufruf fehlgeschlagen: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
return fmt.Errorf("auditclient: unerwarteter status %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) httpClient() *http.Client {
|
||||
if c.HTTPClient != nil {
|
||||
return c.HTTPClient
|
||||
}
|
||||
return httpclient.Default()
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package auditclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIngest_SendsExpectedRequest(t *testing.T) {
|
||||
var gotToken string
|
||||
var gotBody ingestRequest
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotToken = r.Header.Get("X-Service-Token")
|
||||
_ = json.NewDecoder(r.Body).Decode(&gotBody)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New(srv.URL, "test-token")
|
||||
err := c.Ingest(context.Background(), "tenant_a", "svc:ai", "ai.decision.classify", "doc:1",
|
||||
map[string]any{"model": "llama3", "prompt_version": 2})
|
||||
if err != nil {
|
||||
t.Fatalf("Ingest: %v", err)
|
||||
}
|
||||
if gotToken != "test-token" {
|
||||
t.Errorf("token = %q, want test-token", gotToken)
|
||||
}
|
||||
if gotBody.Action != "ai.decision.classify" || gotBody.Target != "doc:1" {
|
||||
t.Errorf("unerwarteter request: %+v", gotBody)
|
||||
}
|
||||
if gotBody.Metadata["model"] != "llama3" {
|
||||
t.Errorf("metadata.model = %v, want llama3", gotBody.Metadata["model"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngest_UnexpectedStatusIsError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "zugriff verweigert", http.StatusForbidden)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
c := New(srv.URL, "test-token")
|
||||
if err := c.Ingest(context.Background(), "tenant_a", "svc:ai", "ai.decision.classify", "doc:1", nil); err == nil {
|
||||
t.Fatal("erwartet fehler bei status 403, habe nil")
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/decisionaudit"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/mock"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/ollama"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
)
|
||||
|
||||
var testSchema = Schema{Categories: []string{"rechnung", "vertrag", "kuendigung"}}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newStore(t *testing.T) *Store {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/moduletype"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
// --- Vertrag 1: GOV-01 registrierter Objekttyp (moduletype.ObjectType) ---
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newRecorder(t *testing.T) *Recorder {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/auditclient"
|
||||
)
|
||||
|
||||
// Decision ist eine einzelne, abgeschlossene KI-gestuetzte Entscheidung
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/auditclient"
|
||||
)
|
||||
|
||||
type recordedRequest struct {
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/loadtest"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/ollama"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearch"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/loadtest"
|
||||
)
|
||||
|
||||
// QA-08 Pruefung 1 (AK1): echte parallele Chat-Anfragen gegen den realen,
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/docchat"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearch"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/vectorindex"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/classify"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/queue"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newPool(t *testing.T) *pgxpool.Pool {
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/extractvalidate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/invoiceextract"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/extractmapping"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/metadatapushclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/extractmapping"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/metadatapushclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newMappingRegistry(t *testing.T) *extractmapping.Registry {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newRegistry(t *testing.T) *Registry {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Package httpclient liefert den HTTP-Client fuer ausgehende Dienstaufrufe
|
||||
// (Kopie des Musters aus connect/internal/httpclient, ENG-06): Zeitlimit
|
||||
// statt endlosem Warten, keine Weiterleitungen (ein Service-Token wuerde
|
||||
// sonst an das Weiterleitungsziel mitgesendet).
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const DefaultTimeout = 15 * time.Second
|
||||
|
||||
var shared = New(DefaultTimeout)
|
||||
|
||||
func Default() *http.Client { return shared }
|
||||
|
||||
func New(timeout time.Duration) *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNew_DoesNotFollowRedirects(t *testing.T) {
|
||||
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("weiterleitungsziel haette nie erreicht werden duerfen")
|
||||
}))
|
||||
defer target.Close()
|
||||
|
||||
redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, target.URL, http.StatusFound)
|
||||
}))
|
||||
defer redirector.Close()
|
||||
|
||||
resp, err := Default().Get(redirector.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusFound {
|
||||
t.Fatalf("status = %d, want %d (keine weiterleitung gefolgt)", resp.StatusCode, http.StatusFound)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Package loadtest implementiert die fuer QA-07/QA-08 gemeinsam genutzte
|
||||
// Last- und Leistungsmessung: parallele Sessions ausfuehren, Latenzen
|
||||
// aufzeichnen, Durchsatz bestimmen. k6/vegeta sind auf diesem Rechner nicht
|
||||
// installierbar (Projektregel: keine zusaetzlichen Toolchains/Dienste
|
||||
// installieren) — echter Ersatz ohne externe Abhaengigkeit: reale
|
||||
// nebenlaeufige Aufrufe ueber die Go-Standardbibliothek, gegen den echten,
|
||||
// in dieser Sitzung gebauten Code. Eigenstaendige Kopie aus
|
||||
// mail/internal/loadtest, da Go-Module keine internal/-Pakete teilen.
|
||||
package loadtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Result fasst Latenz- und Durchsatzwerte eines Lasttest-Laufs zusammen.
|
||||
type Result struct {
|
||||
Protocol string
|
||||
Concurrency int
|
||||
Sessions int
|
||||
Errors int
|
||||
Duration time.Duration
|
||||
Latencies []time.Duration // aufsteigend sortiert
|
||||
}
|
||||
|
||||
// Throughput liefert Sessions pro Sekunde über die Gesamtlaufzeit.
|
||||
func (r Result) Throughput() float64 {
|
||||
if r.Duration <= 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(r.Sessions) / r.Duration.Seconds()
|
||||
}
|
||||
|
||||
// Percentile liefert die Latenz am p-ten Perzentil (0.0–1.0).
|
||||
func (r Result) Percentile(p float64) time.Duration {
|
||||
if len(r.Latencies) == 0 {
|
||||
return 0
|
||||
}
|
||||
idx := int(p * float64(len(r.Latencies)-1))
|
||||
if idx < 0 {
|
||||
idx = 0
|
||||
}
|
||||
if idx >= len(r.Latencies) {
|
||||
idx = len(r.Latencies) - 1
|
||||
}
|
||||
return r.Latencies[idx]
|
||||
}
|
||||
|
||||
// Max liefert die höchste gemessene Latenz.
|
||||
func (r Result) Max() time.Duration {
|
||||
if len(r.Latencies) == 0 {
|
||||
return 0
|
||||
}
|
||||
return r.Latencies[len(r.Latencies)-1]
|
||||
}
|
||||
|
||||
// Run führt totalSessions Sessions über maximal concurrency parallele
|
||||
// Worker aus und misst je Session die Latenz von sessionFunc.
|
||||
func Run(ctx context.Context, protocol string, concurrency, totalSessions int, sessionFunc func(ctx context.Context) error) Result {
|
||||
var mu sync.Mutex
|
||||
latencies := make([]time.Duration, 0, totalSessions)
|
||||
errCount := 0
|
||||
|
||||
sem := make(chan struct{}, concurrency)
|
||||
var wg sync.WaitGroup
|
||||
start := time.Now()
|
||||
for i := 0; i < totalSessions; i++ {
|
||||
sem <- struct{}{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
t0 := time.Now()
|
||||
err := sessionFunc(ctx)
|
||||
elapsed := time.Since(t0)
|
||||
mu.Lock()
|
||||
latencies = append(latencies, elapsed)
|
||||
if err != nil {
|
||||
errCount++
|
||||
}
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
duration := time.Since(start)
|
||||
|
||||
sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
|
||||
return Result{
|
||||
Protocol: protocol,
|
||||
Concurrency: concurrency,
|
||||
Sessions: totalSessions,
|
||||
Errors: errCount,
|
||||
Duration: duration,
|
||||
Latencies: latencies,
|
||||
}
|
||||
}
|
||||
|
||||
// ResourceSample ist eine Momentaufnahme des Ressourcenverbrauchs DIESES
|
||||
// Prozesses: Heap über die Go-Runtime, kumulierte CPU-Zeit über
|
||||
// /proc/self/stat (Linux, kein externes Werkzeug nötig).
|
||||
type ResourceSample struct {
|
||||
HeapAllocBytes uint64
|
||||
CPUTimeSeconds float64
|
||||
}
|
||||
|
||||
func SampleResources() ResourceSample {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
return ResourceSample{HeapAllocBytes: m.HeapAlloc, CPUTimeSeconds: readSelfCPUTimeSeconds()}
|
||||
}
|
||||
|
||||
// readSelfCPUTimeSeconds liefert die kumulierte User+System-CPU-Zeit dieses
|
||||
// Prozesses in Sekunden. Liefert 0, wenn /proc/self/stat nicht lesbar ist
|
||||
// (z. B. auf Nicht-Linux-Systemen) — Aufrufer behandeln das als "keine
|
||||
// CPU-Messung verfügbar", kein harter Fehler.
|
||||
func readSelfCPUTimeSeconds() float64 {
|
||||
data, err := os.ReadFile("/proc/self/stat")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
fields := strings.Fields(string(data))
|
||||
if len(fields) < 15 {
|
||||
return 0
|
||||
}
|
||||
utime, err1 := strconv.ParseFloat(fields[13], 64)
|
||||
stime, err2 := strconv.ParseFloat(fields[14], 64)
|
||||
if err1 != nil || err2 != nil {
|
||||
return 0
|
||||
}
|
||||
const clockTicksPerSecond = 100 // Linux-Standard-USER_HZ
|
||||
return (utime + stime) / clockTicksPerSecond
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"net/url"
|
||||
"sort"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/httpclient"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
|
||||
@@ -81,7 +81,7 @@ func TestPush_UnknownFieldKey_ReportedNotDropped(t *testing.T) {
|
||||
|
||||
c := New(srv.URL, "svc-token-123")
|
||||
result, err := c.Push(context.Background(), "doc-1", "ai", map[string]string{
|
||||
"rechnungsdatum": "2026-01-15",
|
||||
"rechnungsdatum": "2026-01-15",
|
||||
"nicht_existent_feld": "wert",
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
+205
-11
@@ -1,27 +1,221 @@
|
||||
// Package migrate bindet die gemeinsame Migrationslogik aus common an den
|
||||
// modulspezifischen Fortschrittstabellennamen.
|
||||
// Package migrate implements versioned, revertible SQL migrations (no ORM) — a
|
||||
// standalone copy of the Core/DMS pattern, since AI is its own Go module
|
||||
// and cannot import other modules' internal/ packages.
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
cmigrate "gitea.perlbach24.de/scripte/nexarch/common/migrate"
|
||||
)
|
||||
|
||||
type Migration = cmigrate.Migration
|
||||
// Migration ist eine einzelne versionierte Migrationsdatei.
|
||||
type Migration struct {
|
||||
Version string // Dateiname ohne .up.sql/.down.sql, z.B. "0001_documents"
|
||||
UpSQL string
|
||||
DownSQL string
|
||||
}
|
||||
|
||||
// Load liest alle *.up.sql/*.down.sql-Paare aus dir, sortiert nach
|
||||
// Dateiname (Akzeptanzkriterium: Migrationen sind versioniert).
|
||||
func Load(dir string) ([]Migration, error) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("migrationsverzeichnis %q lesen: %w", dir, err)
|
||||
}
|
||||
var versions []string
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".up.sql") {
|
||||
continue
|
||||
}
|
||||
versions = append(versions, strings.TrimSuffix(e.Name(), ".up.sql"))
|
||||
}
|
||||
sort.Strings(versions)
|
||||
|
||||
migrations := make([]Migration, 0, len(versions))
|
||||
for _, v := range versions {
|
||||
up, err := os.ReadFile(filepath.Join(dir, v+".up.sql"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("migration %q: up.sql lesen: %w", v, err)
|
||||
}
|
||||
down, err := os.ReadFile(filepath.Join(dir, v+".down.sql"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("migration %q: down.sql lesen (jede Migration braucht ein Rollback): %w", v, err)
|
||||
}
|
||||
migrations = append(migrations, Migration{Version: v, UpSQL: string(up), DownSQL: string(down)})
|
||||
}
|
||||
return migrations, nil
|
||||
}
|
||||
|
||||
// TrackingTable ist modulspezifisch benannt, damit sich Module in
|
||||
// derselben Datenbank/demselben Schema nicht gegenseitig den
|
||||
// Migrationsfortschritt ueberschreiben (FDN-13).
|
||||
const TrackingTable = "connect_schema_migrations"
|
||||
|
||||
var migrator = cmigrate.MustNew(TrackingTable)
|
||||
const legacyTrackingTable = "schema_migrations"
|
||||
|
||||
func Load(dir string) ([]Migration, error) { return cmigrate.Load(dir) }
|
||||
// ensureTrackingTable legt TrackingTable an. Beim erstmaligen Anlegen wird
|
||||
// ein vorhandener Bestand aus der frueheren, unqualifizierten
|
||||
// schema_migrations uebernommen (nur Versionen, die dieses Modul kennt, und
|
||||
// nur wenn dessen Basisversion dort steht — eine schema_migrations eines
|
||||
// anderen Moduls oder eines anderen Schemas wird weder gelesen noch
|
||||
// veraendert). Die alte Tabelle
|
||||
// bleibt unangetastet.
|
||||
func ensureTrackingTable(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) error {
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s anlegen: %w", TrackingTable, err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
func Up(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) ([]string, error) {
|
||||
return migrator.Up(ctx, pool, migrations)
|
||||
// Serialisiert gleichzeitige Erstanlage/Uebernahme (mehrere App-Instanzen).
|
||||
if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtext('connect_schema_migrations'))`); err != nil {
|
||||
return fmt.Errorf("%s anlegen: %w", TrackingTable, err)
|
||||
}
|
||||
|
||||
var existed bool
|
||||
if err := tx.QueryRow(ctx, `SELECT to_regclass(current_schema() || '.' || $1::text) IS NOT NULL`, TrackingTable).Scan(&existed); err != nil {
|
||||
return fmt.Errorf("%s anlegen: %w", TrackingTable, err)
|
||||
}
|
||||
if existed {
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(ctx, `
|
||||
CREATE TABLE `+TrackingTable+` (
|
||||
version TEXT PRIMARY KEY,
|
||||
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
`); err != nil {
|
||||
return fmt.Errorf("%s anlegen: %w", TrackingTable, err)
|
||||
}
|
||||
|
||||
if len(migrations) > 0 {
|
||||
var legacy bool
|
||||
if err := tx.QueryRow(ctx, `SELECT to_regclass(current_schema() || '.' || $1::text) IS NOT NULL`, legacyTrackingTable).Scan(&legacy); err != nil {
|
||||
return fmt.Errorf("%s pruefen: %w", legacyTrackingTable, err)
|
||||
}
|
||||
if legacy {
|
||||
versions := make([]string, len(migrations))
|
||||
for i, m := range migrations {
|
||||
versions[i] = m.Version
|
||||
}
|
||||
var hasBase bool
|
||||
if err := tx.QueryRow(ctx,
|
||||
`SELECT EXISTS (SELECT 1 FROM `+legacyTrackingTable+` WHERE version = $1)`, versions[0],
|
||||
).Scan(&hasBase); err != nil {
|
||||
return fmt.Errorf("%s lesen: %w", legacyTrackingTable, err)
|
||||
}
|
||||
if hasBase {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO `+TrackingTable+` (version, applied_at)
|
||||
SELECT version, applied_at FROM `+legacyTrackingTable+` WHERE version = ANY($1)
|
||||
`, versions); err != nil {
|
||||
return fmt.Errorf("bestand aus %s uebernehmen: %w", legacyTrackingTable, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func DownOne(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) (string, error) {
|
||||
return migrator.DownOne(ctx, pool, migrations)
|
||||
func appliedVersions(ctx context.Context, pool *pgxpool.Pool) (map[string]bool, error) {
|
||||
rows, err := pool.Query(ctx, `SELECT version FROM `+TrackingTable)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("angewendete migrationen lesen: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
applied := map[string]bool{}
|
||||
for rows.Next() {
|
||||
var v string
|
||||
if err := rows.Scan(&v); err != nil {
|
||||
return nil, fmt.Errorf("migrationsversion lesen: %w", err)
|
||||
}
|
||||
applied[v] = true
|
||||
}
|
||||
return applied, rows.Err()
|
||||
}
|
||||
|
||||
// Up wendet alle noch nicht angewendeten Migrationen in Reihenfolge an
|
||||
// (Akzeptanzkriterium 2: vorwaerts ausfuehrbar) — bereits angewendete
|
||||
// werden uebersprungen, damit Up auf einer leeren UND auf einer bestehenden
|
||||
// DB funktioniert (Pruefung 1).
|
||||
func Up(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) (applied []string, err error) {
|
||||
if err := ensureTrackingTable(ctx, pool, migrations); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
already, err := appliedVersions(ctx, pool)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, m := range migrations {
|
||||
if already[m.Version] {
|
||||
continue
|
||||
}
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return applied, fmt.Errorf("transaktion fuer %q starten: %w", m.Version, err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, m.UpSQL); err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
return applied, fmt.Errorf("migration %q anwenden: %w", m.Version, err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO `+TrackingTable+` (version) VALUES ($1)`, m.Version); err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
return applied, fmt.Errorf("migration %q als angewendet markieren: %w", m.Version, err)
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return applied, fmt.Errorf("migration %q committen: %w", m.Version, err)
|
||||
}
|
||||
applied = append(applied, m.Version)
|
||||
}
|
||||
return applied, nil
|
||||
}
|
||||
|
||||
// DownOne macht die zuletzt angewendete Migration rueckgaengig
|
||||
// (Akzeptanzkriterium 2: rueckwaerts ausfuehrbar) und liefert deren Version,
|
||||
// oder "" falls keine Migration angewendet war.
|
||||
func DownOne(ctx context.Context, pool *pgxpool.Pool, migrations []Migration) (version string, err error) {
|
||||
if err := ensureTrackingTable(ctx, pool, migrations); err != nil {
|
||||
return "", err
|
||||
}
|
||||
already, err := appliedVersions(ctx, pool)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var last *Migration
|
||||
for i := len(migrations) - 1; i >= 0; i-- {
|
||||
if already[migrations[i].Version] {
|
||||
last = &migrations[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if last == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("transaktion fuer rollback von %q starten: %w", last.Version, err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, last.DownSQL); err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
return "", fmt.Errorf("migration %q zurueckrollen: %w", last.Version, err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `DELETE FROM `+TrackingTable+` WHERE version = $1`, last.Version); err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
return "", fmt.Errorf("migration %q aus %s entfernen: %w", last.Version, TrackingTable, err)
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return "", fmt.Errorf("rollback von %q committen: %w", last.Version, err)
|
||||
}
|
||||
return last.Version, nil
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/modelconfig"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
type fakeLister struct {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
// fakeVerifier simuliert Core TEN-01 fuer die Registry-Tests, ohne einen
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/moduletype"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/moduletype"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
type fakeVerifier struct{ validTenants map[string]bool }
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
// sensitivePathPatterns beschreibt die Bereiche aus
|
||||
// TESTSTRATEGIE-AI.md Abschnitt 3.
|
||||
var sensitivePathPatterns = []*regexp.Regexp{
|
||||
regexp.MustCompile(`(^|/)ai/internal/svcauth/`),
|
||||
regexp.MustCompile(`(^|/)ai/internal/tenantfeature/`),
|
||||
regexp.MustCompile(`(^|/)ai/internal/semanticsearch/`),
|
||||
regexp.MustCompile(`(^|/)ai/internal/semanticsearchapi/`),
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
// Negativtest des Gates selbst (QA-01 Prüfung 1): ein Diff mit
|
||||
// geänderter ai/internal/tenantfeature/tenantfeature.go ohne begleitende Testdatei
|
||||
// geänderter ai/internal/svcauth/svcauth.go ohne begleitende Testdatei
|
||||
// muss als Verstoß erkannt werden.
|
||||
package pflichttestgate
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCheckDiff_FlagsSensitiveChangeWithoutTest(t *testing.T) {
|
||||
violations := CheckDiff([]string{"ai/internal/tenantfeature/tenantfeature.go"})
|
||||
violations := CheckDiff([]string{"ai/internal/svcauth/svcauth.go"})
|
||||
if len(violations) != 1 {
|
||||
t.Fatalf("erwartet genau 1 verstoß, habe %d: %+v", len(violations), violations)
|
||||
}
|
||||
if violations[0].Package != "ai/internal/tenantfeature" {
|
||||
if violations[0].Package != "ai/internal/svcauth" {
|
||||
t.Fatalf("falsches package gemeldet: %+v", violations[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckDiff_PassesWhenTestFileAccompanies(t *testing.T) {
|
||||
violations := CheckDiff([]string{
|
||||
"ai/internal/tenantfeature/tenantfeature.go",
|
||||
"ai/internal/tenantfeature/tenantfeature_test.go",
|
||||
"ai/internal/svcauth/svcauth.go",
|
||||
"ai/internal/svcauth/svcauth_test.go",
|
||||
})
|
||||
if len(violations) != 0 {
|
||||
t.Fatalf("erwartet keine verstöße, habe: %+v", violations)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newStore(t *testing.T) *Store {
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
// Package classification ist das Prüfgate für QA-03: misst Klassifizierungs- und
|
||||
// Package qa03gate ist das Prüfgate für QA-03: misst Klassifizierungs- und
|
||||
// Extraktionsgenauigkeit am Referenzkorpus (QA-01, ai/internal/refcorpus)
|
||||
// und belegt die Wirkung von Korrektur-Feedback auf künftige Vorschläge.
|
||||
// Reines Prüfgate — ruft nur bestehende, bereits gemergte Pakete auf
|
||||
// (classify CLS-02, extract EXT-05-Grundlage, correction/historysuggest
|
||||
// CLS-05), ohne deren Code zu ändern.
|
||||
package classification
|
||||
package qa03gate
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package searchquality ist das Prüfgate für QA-04: es misst Suchqualität,
|
||||
// Package qa04gate ist das Prüfgate für QA-04: es misst Suchqualität,
|
||||
// RAG-Antwortgüte und Quellenverweis-Nachvollziehbarkeit auf einem festen,
|
||||
// synthetischen Testkorpus (SEM-01..06 bereits gemergt) und ändert keines der
|
||||
// geprüften Pakete. Der Korpus stammt aus refcorpus (QA-01), die "Einbettung"
|
||||
@@ -7,7 +7,7 @@
|
||||
// einem externen Modell abhängt. Reine Termfrequenz ohne IDF-Gewichtung wurde
|
||||
// probiert und verworfen: seltene, fachlich entscheidende Wörter (z.B.
|
||||
// "Gesamtbetrag") gingen gegen häufige, generische Wörter unter.
|
||||
package searchquality
|
||||
package qa04gate
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -231,3 +231,4 @@ func (p *embedProvider) Chat(ctx context.Context, req provider.ChatRequest) (pro
|
||||
func (p *embedProvider) Complete(ctx context.Context, req provider.CompletionRequest) (provider.CompletionResponse, error) {
|
||||
return provider.CompletionResponse{}, errors.New("qa04gate: Complete wird vom Prüfgate nicht genutzt")
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package searchquality
|
||||
package qa04gate
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package searchquality
|
||||
package qa04gate
|
||||
|
||||
// testQuestion ist eine feste Testfrage des QA-04-Prüfgates gegen den
|
||||
// refcorpus-Testkorpus (QA-01). erwarteteSourceID ist leer, wenn die Frage
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
// Package governance ist das Prüfgate für QA-05 (Datenschutz- &
|
||||
// Package qa05gate ist das Prüfgate für QA-05 (Datenschutz- &
|
||||
// Governance-Prüfung). Es bringt keine neue Fachlogik ein, sondern führt
|
||||
// GOV-02/GOV-03/GOV-05 end-to-end gegen eine echte Postgres-Wegwerf-DB bzw.
|
||||
// einen Mock-Core zusammen, um die drei QA-05-Akzeptanzkriterien in der
|
||||
// Praxis (nicht nur auf Store-Ebene) nachzuweisen.
|
||||
package governance
|
||||
package qa05gate
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/decisionaudit"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
|
||||
@@ -23,8 +24,7 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/openai"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/residency"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/tenantfeature"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
// realPool liefert einen frischen, migrierten Pool auf der Wegwerf-DB aus
|
||||
@@ -36,7 +36,7 @@ func realPool(t *testing.T) *pgxpool.Pool {
|
||||
t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest übersprungen")
|
||||
}
|
||||
pool := testdb.Pool(t, dsn)
|
||||
migrationsDir, err := filepath.Abs("../../../migrations")
|
||||
migrationsDir, err := filepath.Abs("../../migrations")
|
||||
if err != nil {
|
||||
t.Fatalf("migrationsverzeichnis: %v", err)
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/limits"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/loadtest"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/loadtest"
|
||||
)
|
||||
|
||||
// QA-08 Pruefung 2 (AK2): realistische parallele Klassifizierungs-/
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newPool(t *testing.T) *pgxpool.Pool {
|
||||
|
||||
@@ -16,9 +16,9 @@ import (
|
||||
// eine Zeitueberschreitung real simulieren laesst (respektiert ctx wie ein
|
||||
// echter HTTP-Client es tun wuerde).
|
||||
type slowProvider struct {
|
||||
delay time.Duration
|
||||
calls atomic.Int64
|
||||
cancelled atomic.Int64
|
||||
delay time.Duration
|
||||
calls atomic.Int64
|
||||
cancelled atomic.Int64
|
||||
}
|
||||
|
||||
func (s *slowProvider) Chat(ctx context.Context, req provider.ChatRequest) (provider.ChatResponse, error) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newPool(t *testing.T) *pgxpool.Pool {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/provider/openai"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/residency"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func setup(t *testing.T) *residency.Store {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/residency"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/residency"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/residencyapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
const testToken = "test-service-token"
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/historysuggest"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newStore(t *testing.T) *Store {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/historysuggest"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/routingfeedback"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/routingfeedback"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
const testToken = "test-service-token"
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearch"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/vectorindex"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
)
|
||||
|
||||
func newRealPool(t *testing.T) *pgxpool.Pool {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Package semanticsearchapi ist die HTTP-Bindung der semantischen Suche
|
||||
// (SEM-03). Aufrufer ist das anfragende Modul (DMS/Mail), nicht der
|
||||
// Endnutzer direkt — Service-Token-Pflicht wie moduleadapter (Workflow
|
||||
// ENG-02), fail closed. Zusaetzlich (Issue #369) Rechtepruefung
|
||||
// ai.search.read gegen Core fuer den Mandanten aus dem Anfrage-Koerper.
|
||||
// ENG-02), fail closed.
|
||||
package semanticsearchapi
|
||||
|
||||
import (
|
||||
@@ -11,30 +10,18 @@ import (
|
||||
"net/http"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/semanticsearch"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
|
||||
)
|
||||
|
||||
const permissionSearchRead = "ai.search.read"
|
||||
|
||||
type Server struct {
|
||||
svc *semanticsearch.Service
|
||||
serviceToken string
|
||||
policy *policyclient.Client
|
||||
}
|
||||
|
||||
func NewServer(svc *semanticsearch.Service, serviceToken string) *Server {
|
||||
return &Server{svc: svc, serviceToken: serviceToken}
|
||||
}
|
||||
|
||||
// WithPolicy aktiviert die Rechtepruefung gegen Core (POST /authorize).
|
||||
// Rolle ist fest "user", weil der Modul-zu-Modul-Aufruf keine Nutzerrolle
|
||||
// transportiert (Uebergang, gleiches Muster wie connect/internal/statusapi).
|
||||
func (s *Server) WithPolicy(client *policyclient.Client) *Server {
|
||||
s.policy = client
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) RegisterRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("POST /search", s.requireAuth(s.handleSearch))
|
||||
}
|
||||
@@ -50,22 +37,6 @@ func (s *Server) requireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) permitted(w http.ResponseWriter, r *http.Request, tenantSlug, permission string) bool {
|
||||
if s.policy == nil {
|
||||
return true
|
||||
}
|
||||
allowed, err := s.policy.Allowed(r.Context(), tenantSlug, "user", permission)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "berechtigungsdienst nicht erreichbar")
|
||||
return false
|
||||
}
|
||||
if !allowed {
|
||||
writeError(w, http.StatusForbidden, "keine berechtigung")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type refRequest struct {
|
||||
SourceType string `json:"source_type"`
|
||||
SourceID string `json:"source_id"`
|
||||
@@ -88,9 +59,6 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, "ungueltiger anfrage-koerper")
|
||||
return
|
||||
}
|
||||
if !s.permitted(w, r, req.TenantSlug, permissionSearchRead) {
|
||||
return
|
||||
}
|
||||
|
||||
refs := make([]semanticsearch.Ref, 0, len(req.AllowedRefs))
|
||||
for _, ref := range req.AllowedRefs {
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
package semanticsearchapi_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/policyclient"
|
||||
)
|
||||
|
||||
// Steht fuer Core's POST /authorize-Vertrag ({"allowed": bool}); prueft
|
||||
// die echte Kette requireAuth -> permitted -> policyclient -> 403/200.
|
||||
func newFakePolicyAPI(t *testing.T, allow bool, gotBody *map[string]string) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/authorize" || r.Header.Get("X-Service-Token") == "" {
|
||||
t.Fatalf("unerwartete anfrage: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
if gotBody != nil {
|
||||
_ = json.NewDecoder(r.Body).Decode(gotBody)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]bool{"allowed": allow})
|
||||
}))
|
||||
}
|
||||
|
||||
func searchRequest(t *testing.T) *http.Request {
|
||||
t.Helper()
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"tenant_slug": "acme",
|
||||
"query": "test",
|
||||
"top_k": 5,
|
||||
"allowed_refs": []map[string]string{{"source_type": "dms_document", "source_id": "doc-1"}},
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPost, "/search", bytes.NewReader(body))
|
||||
req.Header.Set("X-Service-Token", "geheimes-token")
|
||||
return req
|
||||
}
|
||||
|
||||
func TestHandleSearch_PolicyDenies_Rejected(t *testing.T) {
|
||||
var got map[string]string
|
||||
policyAPI := newFakePolicyAPI(t, false, &got)
|
||||
defer policyAPI.Close()
|
||||
mux := http.NewServeMux()
|
||||
newTestServer().WithPolicy(policyclient.New(policyAPI.URL, "test-token")).RegisterRoutes(mux)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, searchRequest(t))
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("erwartet 403 ohne berechtigung, bekam %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if got["permission"] != "ai.search.read" || got["tenant"] != "acme" {
|
||||
t.Fatalf("falsche policy-anfrage: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleSearch_PolicyAllows_Passes(t *testing.T) {
|
||||
policyAPI := newFakePolicyAPI(t, true, nil)
|
||||
defer policyAPI.Close()
|
||||
mux := http.NewServeMux()
|
||||
newTestServer().WithPolicy(policyclient.New(policyAPI.URL, "test-token")).RegisterRoutes(mux)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, searchRequest(t))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("erwartet 200 mit berechtigung, bekam %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleSearch_PolicyUnavailable_FailsClosed(t *testing.T) {
|
||||
policyAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer policyAPI.Close()
|
||||
mux := http.NewServeMux()
|
||||
newTestServer().WithPolicy(policyclient.New(policyAPI.URL, "test-token")).RegisterRoutes(mux)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, searchRequest(t))
|
||||
if rec.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("erwartet 503 bei ausfall des berechtigungsdienstes, bekam %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleSearch_PolicyCheckedOnlyAfterServiceToken(t *testing.T) {
|
||||
policyAPI := newFakePolicyAPI(t, true, nil)
|
||||
defer policyAPI.Close()
|
||||
mux := http.NewServeMux()
|
||||
newTestServer().WithPolicy(policyclient.New(policyAPI.URL, "test-token")).RegisterRoutes(mux)
|
||||
|
||||
req := searchRequest(t)
|
||||
req.Header.Set("X-Service-Token", "falsch")
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("erwartet 401 bei falschem service-token, bekam %d", rec.Code)
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/summarize"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/threadsummarize"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
||||
@@ -125,10 +125,10 @@ func wordSet(s string) map[string]bool {
|
||||
// TypeMetrics fasst die Bewertungen aller Stichproben eines
|
||||
// Zusammenfassungstyps zusammen (AK1).
|
||||
type TypeMetrics struct {
|
||||
Type string
|
||||
Count int
|
||||
Type string
|
||||
Count int
|
||||
AverageGroundedness float64
|
||||
FlaggedCount int
|
||||
FlaggedCount int
|
||||
}
|
||||
|
||||
// AggregateByType berechnet je Zusammenfassungstyp die Kennzahl über alle
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Package svcauth vergleicht Service-Token in konstanter Zeit (eigene Kopie
|
||||
// je Modul, siehe workflow/internal/svcauth — Go-Module teilen keine
|
||||
// internal/-Pakete über Modulgrenzen; Lehre aus RET-14/F-08: Schutz beim
|
||||
// ersten Entwurf einbauen statt nachzuruesten).
|
||||
package svcauth
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
)
|
||||
|
||||
// Equal vergleicht die SHA-256-Werte beider Eingaben in konstanter Zeit.
|
||||
// Ein leeres erwartetes Token stimmt nie.
|
||||
func Equal(got, want string) bool {
|
||||
if want == "" {
|
||||
return false
|
||||
}
|
||||
g, w := sha256.Sum256([]byte(got)), sha256.Sum256([]byte(want))
|
||||
return subtle.ConstantTimeCompare(g[:], w[:]) == 1
|
||||
}
|
||||
@@ -27,6 +27,7 @@ func (s *stubProvider) Embed(ctx context.Context, req provider.EmbeddingRequest)
|
||||
return provider.EmbeddingResponse{}, nil
|
||||
}
|
||||
|
||||
|
||||
// AK1: Konfidenz wird je Vorschlag mitgeliefert.
|
||||
func TestSuggest_ReturnsConfidencePerSuggestion(t *testing.T) {
|
||||
p := &stubProvider{response: `[{"tag":"rechnung","confidence":0.9,"reasoning":"enthaelt rechnungsnummer"},{"tag":"steuer","confidence":0.4,"reasoning":"erwaehnt umsatzsteuer"}]`}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
)
|
||||
|
||||
func newStore(t *testing.T) *Store {
|
||||
|
||||
File renamed without changes.
File renamed without changes.
@@ -7,7 +7,6 @@ package testdb
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -17,21 +16,6 @@ import (
|
||||
|
||||
var seq atomic.Int64
|
||||
|
||||
// AdminDSN liefert TEST_ADMIN_DSN. Fehlt er, wird uebersprungen; mit
|
||||
// REQUIRE_DB=1 (CI) zaehlt das Fehlen als Fehler.
|
||||
func AdminDSN(t testing.TB) string {
|
||||
t.Helper()
|
||||
dsn := os.Getenv("TEST_ADMIN_DSN")
|
||||
if dsn != "" {
|
||||
return dsn
|
||||
}
|
||||
if os.Getenv("REQUIRE_DB") == "1" {
|
||||
t.Fatal("testdb: TEST_ADMIN_DSN nicht gesetzt, aber REQUIRE_DB=1")
|
||||
}
|
||||
t.Skip("testdb: TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen")
|
||||
return ""
|
||||
}
|
||||
|
||||
// Pool legt ein Schema an und liefert einen darauf ausgerichteten Pool;
|
||||
// beides wird per t.Cleanup wieder entfernt.
|
||||
func Pool(t testing.TB, dsn string) *pgxpool.Pool {
|
||||
@@ -30,9 +30,9 @@ type Thread struct {
|
||||
|
||||
// Result ist die Zusammenfassung eines Threads.
|
||||
type Result struct {
|
||||
Summary string
|
||||
MessageCount int
|
||||
LastMessageID string
|
||||
Summary string
|
||||
MessageCount int
|
||||
LastMessageID string
|
||||
}
|
||||
|
||||
// Options steuert die Zusammenfassung.
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/migrate"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/testdb"
|
||||
"gitea.perlbach24.de/scripte/nexarch/ai/internal/vectorindex"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/testdb"
|
||||
)
|
||||
|
||||
func newPool(t *testing.T) *pgxpool.Pool {
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/complianceapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/rbacclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/retentionapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/kennzahlenapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/rbacclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/retentionapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -13,10 +13,10 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/legalholdapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/moduleadapter"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/tenantregistryclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/tenantregistryclient"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/restoretest"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/rbacclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/retentionapi"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/scrub"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -30,7 +30,7 @@ Verbindlich für jeden Pull Request, der Dateien in einem der folgenden Bereiche
|
||||
- **WORM-Durchsetzung** (`archive/internal/wormstore/`) — die Storage-Object-Lock-Garantie selbst.
|
||||
- **Zugriffsprüfung** (`archive/internal/rbacclient/`, `archive/internal/retentionapi/`) — RBAC-Anbindung
|
||||
und die APIs, die sie durchsetzen.
|
||||
- **Tenant-/Schlüssel-Bezug** (`archive/internal/kekclient/`,
|
||||
- **Tenant-/Schlüssel-Bezug** (`archive/internal/tenantregistryclient/`, `archive/internal/kekclient/`,
|
||||
`archive/internal/backupcrypto/`) — unabhängige Tenant-Validierung und Verschlüsselungspfade.
|
||||
|
||||
Regel: **jede geänderte `.go`-Datei in einem dieser Bereiche muss von einer geänderten oder neuen
|
||||
|
||||
+5
-4
@@ -4,10 +4,7 @@ go 1.25.0
|
||||
|
||||
toolchain go1.25.13
|
||||
|
||||
require (
|
||||
gitea.perlbach24.de/scripte/nexarch/common v0.1.0
|
||||
github.com/jackc/pgx/v5 v5.9.2
|
||||
)
|
||||
require github.com/jackc/pgx/v5 v5.9.2
|
||||
|
||||
require (
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
@@ -16,3 +13,7 @@ require (
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
)
|
||||
|
||||
require gitea.perlbach24.de/scripte/nexarch/common v0.0.0
|
||||
|
||||
replace gitea.perlbach24.de/scripte/nexarch/common => ../common
|
||||
@@ -1,5 +1,3 @@
|
||||
gitea.perlbach24.de/scripte/nexarch/common v0.1.0 h1:biNzFr2LOMZC7b1leY3smQtrMmSMp6IW80OvR4LSjLY=
|
||||
gitea.perlbach24.de/scripte/nexarch/common v0.1.0/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package archtest
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Architekturtest: im Produktivcode dieses Moduls gibt es keine Aufrufe ueber
|
||||
// den Standard-Client ohne Zeitlimit.
|
||||
func TestNoUnboundedDefaultClientInProductionCode(t *testing.T) {
|
||||
root, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
forbidden := regexp.MustCompile(`http\.DefaultClient|http\.(Get|Post|Head|PostForm)\(`)
|
||||
err = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
if n := d.Name(); n == "node_modules" || n == "testdata" || n == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, line := range strings.Split(string(data), "\n") {
|
||||
if t := strings.TrimSpace(line); strings.HasPrefix(t, "//") {
|
||||
continue
|
||||
}
|
||||
if forbidden.MatchString(line) {
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
t.Errorf("%s:%d nutzt den Standard-Client ohne Zeitlimit: %s", rel, i+1, strings.TrimSpace(line))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func moduleRoot(t *testing.T) string {
|
||||
// Antwort bzw. der rohe ListenAndServe-Aufruf genau die erlaubte
|
||||
// Implementierung der Regel selbst ist (API-20/API-21), kein Verstoss.
|
||||
// Das Archive-Modul hat keine weiteren, physisch verschachtelten Go-Module.
|
||||
var exempt = []string{}
|
||||
var exempt = []string{"internal/httpharden"}
|
||||
|
||||
// TestNoErrorTextLeaksIntoHTTPResponses ist Akzeptanzkriterium 1: kein
|
||||
// err.Error() direkt in einer HTTP-Antwort, ausser ueber apierror.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Package auditclient ist ein schlanker HTTP-Client für Core AUD-08
|
||||
// (internal/audit, POST /audit/ingest). Archive ist ein physisch
|
||||
// getrenntes Go-Modul und kann Cores internal/audit daher nicht direkt
|
||||
// importieren — nur über dessen HTTP-Schnittstelle. Gleiches Muster wie
|
||||
// archive/internal/notifyclient (CFG-05).
|
||||
package auditclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
ServiceToken string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL, serviceToken string) *Client {
|
||||
return &Client{BaseURL: baseURL, ServiceToken: serviceToken, HTTPClient: httpclient.Default()}
|
||||
}
|
||||
|
||||
type ingestRequest struct {
|
||||
TenantSlug string `json:"tenant_slug"`
|
||||
Actor string `json:"actor"`
|
||||
Action string `json:"action"`
|
||||
Target string `json:"target"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
// Ingest meldet EIN Audit-Ereignis an Core. Jeder Fehler (Transport,
|
||||
// Timeout, unerwarteter Status) wird als Fehler zurückgegeben — der
|
||||
// Aufrufer entscheidet, ob/wie er das nachbehandelt (siehe
|
||||
// archive/internal/audittrail für den outage-toleranten Wrapper).
|
||||
func (c *Client) Ingest(ctx context.Context, tenantSlug, actor, action, target string, metadata map[string]any) error {
|
||||
body, err := json.Marshal(ingestRequest{
|
||||
TenantSlug: tenantSlug, Actor: actor, Action: action, Target: target, Metadata: metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("auditclient: request kodieren: %w", err)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/audit/ingest", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("auditclient: request bauen: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Service-Token", c.ServiceToken)
|
||||
|
||||
resp, err := c.httpClient().Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("auditclient: aufruf fehlgeschlagen: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
return fmt.Errorf("auditclient: unerwarteter status %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) httpClient() *http.Client {
|
||||
if c.HTTPClient != nil {
|
||||
return c.HTTPClient
|
||||
}
|
||||
return httpclient.Default()
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/auditclient"
|
||||
)
|
||||
|
||||
type Recorder struct {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/auditclient"
|
||||
)
|
||||
|
||||
func setupOutboxTest(t *testing.T) *pgxpool.Pool {
|
||||
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/auditclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/audittrail"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/referenceset"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/auditclient"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// Package httpharden liefert gehaertete http.Server-Grundeinstellungen
|
||||
// (Timeouts, Header-Limit) und ein Body-Limit fuer alle Dienste (QA-15 F-05).
|
||||
// Dieselbe Datei existiert in archive/internal/httpharden und
|
||||
// dms/internal/httpharden, weil die Module getrennte go.mod-Dateien haben.
|
||||
package httpharden
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/apierror"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultReadHeaderTimeout = 10 * time.Second
|
||||
DefaultReadTimeout = 30 * time.Second
|
||||
DefaultWriteTimeout = 60 * time.Second
|
||||
DefaultIdleTimeout = 120 * time.Second
|
||||
DefaultMaxHeaderBytes = 1 << 20
|
||||
DefaultMaxBodyBytes = 1 << 20
|
||||
|
||||
EnvReadHeaderTimeout = "NEXARCH_HTTP_READ_HEADER_TIMEOUT_SECONDS"
|
||||
EnvReadTimeout = "NEXARCH_HTTP_READ_TIMEOUT_SECONDS"
|
||||
EnvWriteTimeout = "NEXARCH_HTTP_WRITE_TIMEOUT_SECONDS"
|
||||
EnvIdleTimeout = "NEXARCH_HTTP_IDLE_TIMEOUT_SECONDS"
|
||||
EnvMaxBodyBytes = "NEXARCH_HTTP_MAX_BODY_BYTES"
|
||||
)
|
||||
|
||||
// Options weicht vom Standard ab. Nullwerte bedeuten Standard bzw. Env-Wert;
|
||||
// StreamingBodies/StreamingResponses setzen ReadTimeout/WriteTimeout auf 0
|
||||
// (unbegrenzt), damit lange Uploads und Downloads nicht abbrechen. Schutz
|
||||
// gegen langsame Header bleibt ueber ReadHeaderTimeout und IdleTimeout.
|
||||
type Options struct {
|
||||
StreamingBodies bool
|
||||
StreamingResponses bool
|
||||
}
|
||||
|
||||
func envSeconds(name string, def time.Duration) time.Duration {
|
||||
v := os.Getenv(name)
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil || n < 1 || n > 3600 {
|
||||
log.Fatalf("%s muss eine ganze Zahl zwischen 1 und 3600 sein, war %q", name, v)
|
||||
}
|
||||
return time.Duration(n) * time.Second
|
||||
}
|
||||
|
||||
// MaxBodyBytes liest die Body-Obergrenze (Standard 1 MiB, Env
|
||||
// NEXARCH_HTTP_MAX_BODY_BYTES, 1 KiB bis 1 GiB).
|
||||
func MaxBodyBytes() int64 {
|
||||
v := os.Getenv(EnvMaxBodyBytes)
|
||||
if v == "" {
|
||||
return DefaultMaxBodyBytes
|
||||
}
|
||||
n, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil || n < 1<<10 || n > 1<<30 {
|
||||
log.Fatalf("%s muss zwischen 1024 und 1073741824 liegen, war %q", EnvMaxBodyBytes, v)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// NewServer baut einen http.Server mit gehaerteten Timeouts.
|
||||
func NewServer(addr string, h http.Handler, opt Options) *http.Server {
|
||||
s := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: h,
|
||||
ReadHeaderTimeout: envSeconds(EnvReadHeaderTimeout, DefaultReadHeaderTimeout),
|
||||
ReadTimeout: envSeconds(EnvReadTimeout, DefaultReadTimeout),
|
||||
WriteTimeout: envSeconds(EnvWriteTimeout, DefaultWriteTimeout),
|
||||
IdleTimeout: envSeconds(EnvIdleTimeout, DefaultIdleTimeout),
|
||||
MaxHeaderBytes: DefaultMaxHeaderBytes,
|
||||
}
|
||||
if opt.StreamingBodies {
|
||||
s.ReadTimeout = 0
|
||||
}
|
||||
if opt.StreamingResponses {
|
||||
s.WriteTimeout = 0
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ListenAndServe ersetzt http.ListenAndServe: JSON-API mit Body-Limit und
|
||||
// (QA-15 F-14) apierror.Normalize aussen um jeden Handler — 5xx-Antworten
|
||||
// verlieren dadurch automatisch jeden Interna-Text (Datenbank-/Pfadfehler),
|
||||
// unabhaengig davon, was der einzelne Handler geschrieben hat.
|
||||
func ListenAndServe(addr string, h http.Handler) error {
|
||||
return NewServer(addr, apierror.Normalize(LimitBody(MaxBodyBytes(), h)), Options{}).ListenAndServe()
|
||||
}
|
||||
|
||||
// ListenAndServeStreaming fuer Dienste mit grossen Uploads/Downloads: keine
|
||||
// globale Body-Grenze und keine Lese-/Schreib-Gesamtzeit, nur Header-Timeout.
|
||||
func ListenAndServeStreaming(addr string, h http.Handler) error {
|
||||
return NewServer(addr, apierror.Normalize(h), Options{StreamingBodies: true, StreamingResponses: true}).ListenAndServe()
|
||||
}
|
||||
|
||||
// LimitBody begrenzt Request-Bodies. Ein Content-Length ueber dem Limit wird
|
||||
// vor dem Handler mit 413 abgewiesen, chunked Bodies ueber MaxBytesReader.
|
||||
func LimitBody(max int64, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.ContentLength > max {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusRequestEntityTooLarge)
|
||||
_, _ = w.Write([]byte(`{"error":{"code":"payload_too_large","message":"Anfrage zu gross"}}`))
|
||||
return
|
||||
}
|
||||
if r.Body != nil && r.Body != http.NoBody {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, max)
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
File renamed without changes.
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/svcauth"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/svcauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/archive/internal/svcauth"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
var sensitivePathPatterns = []*regexp.Regexp{
|
||||
regexp.MustCompile(`(^|/)archive/internal/wormstore/`),
|
||||
regexp.MustCompile(`(^|/)archive/internal/rbacclient/`),
|
||||
regexp.MustCompile(`(^|/)archive/internal/tenantregistryclient/`),
|
||||
regexp.MustCompile(`(^|/)archive/internal/retentionapi/`),
|
||||
regexp.MustCompile(`(^|/)archive/internal/kekclient/`),
|
||||
regexp.MustCompile(`(^|/)archive/internal/backupcrypto/`),
|
||||
|
||||
@@ -60,6 +60,7 @@ func TestCheckDiff_CoversAllSensitiveAreas(t *testing.T) {
|
||||
cases := []string{
|
||||
"archive/internal/wormstore/wormstore.go",
|
||||
"archive/internal/rbacclient/rbacclient.go",
|
||||
"archive/internal/tenantregistryclient/tenantregistryclient.go",
|
||||
"archive/internal/retentionapi/handler.go",
|
||||
"archive/internal/kekclient/kekclient.go",
|
||||
"archive/internal/backupcrypto/reposecret.go",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package retentionverify implementiert VAL-03: Stichprobenvergleich von
|
||||
// retain_until (Quelle aus TAR-01 vs. nach der Migration im generischen
|
||||
// Retention-Objektmodell) je Tenant, nach dem Berichtsmuster aus VAL-01
|
||||
// (internal/migration/verify): Abweichung ist entweder erklaert oder
|
||||
// (internal/migrationverify): Abweichung ist entweder erklaert oder
|
||||
// blockierender Befund, nie reine Notiz.
|
||||
package retentionverify
|
||||
|
||||
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,60 @@
|
||||
// Package tenantregistryclient ist ein schlanker HTTP-Client fuer Core
|
||||
// TEN-01 (Tenant-Registry). Archive ist ein physisch getrenntes Go-Modul
|
||||
// und kann Core TEN-01 daher nicht direkt importieren - nur ueber dessen
|
||||
// HTTP-Schnittstelle abfragen. Gleiches Muster wie archive/internal/
|
||||
// rbacclient (RET-08) und archive/internal/notifyclient (RET-07).
|
||||
package tenantregistryclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpclient"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Client fragt Core TEN-01 nach der Gueltigkeit eines Tenant-Kontexts.
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
ServiceToken string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL, serviceToken string) *Client {
|
||||
return &Client{BaseURL: baseURL, ServiceToken: serviceToken}
|
||||
}
|
||||
|
||||
// VerifyTenant prueft, ob tenantSlug in der Core-Tenant-Registry als
|
||||
// existierender Tenant gefuehrt wird - unabhaengig davon, was das
|
||||
// aufrufende Modul (DMS/Mail) selbst behauptet. Ein Transport-/
|
||||
// Serverfehler wird als Fehler zurueckgegeben, NICHT stillschweigend als
|
||||
// gueltig behandelt - der Aufrufer muss fail-closed (ablehnen) reagieren.
|
||||
func (c *Client) VerifyTenant(ctx context.Context, tenantSlug string) (bool, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+"/tenants/"+url.PathEscape(tenantSlug), nil)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("tenantregistryclient: request bauen: %w", err)
|
||||
}
|
||||
req.Header.Set("X-Service-Token", c.ServiceToken)
|
||||
|
||||
resp, err := c.httpClient().Do(req)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("tenantregistryclient: aufruf fehlgeschlagen: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return true, nil
|
||||
case http.StatusNotFound:
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("tenantregistryclient: unerwarteter status %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) httpClient() *http.Client {
|
||||
if c.HTTPClient != nil {
|
||||
return c.HTTPClient
|
||||
}
|
||||
return httpclient.Default()
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package tenantregistryclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVerifyTenant_ExistingTenant(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/tenants/mandant-a" {
|
||||
t.Errorf("pfad = %q, want /tenants/mandant-a", r.URL.Path)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
c := New(server.URL, "test-token")
|
||||
ok, err := c.VerifyTenant(context.Background(), "mandant-a")
|
||||
if err != nil {
|
||||
t.Fatalf("verifytenant: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatal("erwartet gueltigen tenant, habe false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyTenant_UnknownTenant(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
c := New(server.URL, "test-token")
|
||||
ok, err := c.VerifyTenant(context.Background(), "unbekannt")
|
||||
if err != nil {
|
||||
t.Fatalf("verifytenant: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Fatal("erwartet ungueltigen tenant, habe true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyTenant_ServerErrorIsFailClosed(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
c := New(server.URL, "test-token")
|
||||
ok, err := c.VerifyTenant(context.Background(), "mandant-a")
|
||||
if err == nil {
|
||||
t.Fatal("erwartet fehler bei 500-antwort der tenant-registry")
|
||||
}
|
||||
if ok {
|
||||
t.Fatal("erwartet false bei fehler (fail-closed)")
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
const exitPermanentDBError = 2
|
||||
|
||||
type pinger interface {
|
||||
Ping(ctx context.Context) error
|
||||
}
|
||||
|
||||
func isPermanentDBError(err error) bool {
|
||||
var pgErr *pgconn.PgError
|
||||
if !errors.As(err, &pgErr) {
|
||||
return false
|
||||
}
|
||||
switch pgErr.Code {
|
||||
case "28000", "28P01", "3D000":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func waitForDB(ctx context.Context, p pinger, timeout time.Duration, sleep func(time.Duration)) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
delay := time.Second
|
||||
for {
|
||||
err := p.Ping(ctx)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if isPermanentDBError(err) {
|
||||
return fmt.Errorf("datenbank lehnt Anmeldung oder Datenbank ab (dauerhaft): %w", err)
|
||||
}
|
||||
if time.Now().Add(delay).After(deadline) {
|
||||
return fmt.Errorf("datenbank nach %s nicht erreichbar: %w", timeout, err)
|
||||
}
|
||||
sleep(delay)
|
||||
if delay < 30*time.Second {
|
||||
delay *= 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
type fakePinger struct {
|
||||
errs []error
|
||||
calls int
|
||||
}
|
||||
|
||||
func (f *fakePinger) Ping(context.Context) error {
|
||||
i := f.calls
|
||||
f.calls++
|
||||
if i < len(f.errs) {
|
||||
return f.errs[i]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestWaitForDB_PermanentAuthErrorStopsImmediately(t *testing.T) {
|
||||
p := &fakePinger{errs: []error{&pgconn.PgError{Code: "28P01"}, nil}}
|
||||
sleeps := 0
|
||||
err := waitForDB(context.Background(), p, time.Minute, func(time.Duration) { sleeps++ })
|
||||
if err == nil || !isPermanentDBError(err) {
|
||||
t.Fatalf("erwartet dauerhaften fehler, war %v", err)
|
||||
}
|
||||
if p.calls != 1 || sleeps != 0 {
|
||||
t.Fatalf("kein retry erwartet, calls=%d sleeps=%d", p.calls, sleeps)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForDB_TransientErrorRetriesWithBackoff(t *testing.T) {
|
||||
p := &fakePinger{errs: []error{errors.New("connection refused"), errors.New("connection refused")}}
|
||||
var delays []time.Duration
|
||||
if err := waitForDB(context.Background(), p, time.Minute, func(d time.Duration) { delays = append(delays, d) }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.calls != 3 || len(delays) != 2 || delays[1] <= delays[0] {
|
||||
t.Fatalf("calls=%d delays=%v", p.calls, delays)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForDB_GivesUpAfterTimeout(t *testing.T) {
|
||||
p := &fakePinger{errs: []error{errors.New("x"), errors.New("x"), errors.New("x"), errors.New("x")}}
|
||||
err := waitForDB(context.Background(), p, 0, func(time.Duration) {})
|
||||
if err == nil || isPermanentDBError(err) {
|
||||
t.Fatalf("erwartet transienten timeout-fehler, war %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPermanentDBError(t *testing.T) {
|
||||
for code, want := range map[string]bool{"28P01": true, "28000": true, "3D000": true, "57P03": false} {
|
||||
if got := isPermanentDBError(&pgconn.PgError{Code: code}); got != want {
|
||||
t.Errorf("%s: got %v", code, got)
|
||||
}
|
||||
}
|
||||
if isPermanentDBError(errors.New("connection refused")) {
|
||||
t.Error("verbindungsfehler ist transient")
|
||||
}
|
||||
}
|
||||
+1
-12
@@ -17,10 +17,10 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/audit"
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/auth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/authtoken"
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/lockout"
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/moduletoken"
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/rbac"
|
||||
@@ -82,17 +82,6 @@ func main() {
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
dbWait := time.Duration(envInt("NEXARCH_ACCOUNT_DB_STARTUP_TIMEOUT_SECONDS", 120, 0, 3600)) * time.Second
|
||||
if err := waitForDB(ctx, pool, dbWait, time.Sleep); err != nil {
|
||||
log.Printf("account-api: %v — DSN in NEXARCH_ACCOUNT_TENANT_DSN (Rolle, Passwort, Datenbank) pruefen", err)
|
||||
if isPermanentDBError(err) {
|
||||
pool.Close()
|
||||
os.Exit(exitPermanentDBError)
|
||||
}
|
||||
pool.Close()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
totpCipher, err := newTOTPCipher()
|
||||
if err != nil {
|
||||
log.Fatalf("totp-verschluesselung: %v", err)
|
||||
|
||||
@@ -18,10 +18,10 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/common/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/audit"
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/auditadmin"
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/db"
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/httpharden"
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/svcauth"
|
||||
)
|
||||
|
||||
|
||||
Loaded 100 of 510 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user