Der Re-Import löschte bisher alle Policy-Zeilen und legte sie neu an — das setzte
per-Dokument-Status, TISAX-Override, Freigabe/Version und nutzergepflegte
Variablenwerte zurück. Jetzt Diff/Upsert über stabile Schlüssel, verlustfrei.
- Schema: PolicyDocument.archivedAt + PolicyRequirement.archivedAt (Lifecycle,
orthogonal zum Freigabe-status). Migration additiv/nullable.
- import-policies.ts komplett auf Reconcile umgebaut:
- Dokumente (code): neu→anlegen; vorhanden→nur Inhaltsfelder aktualisieren,
Status/Version/Owner/Freigabe/Override bleiben; fehlend→archivedAt setzen
(deaktivieren, nicht löschen); Wiederauftauchen→reaktivieren.
- Anforderungen (reqId): analog inkl. Deaktivierung entfernter Anforderungen.
- Variablen (key): Metadaten aktualisieren, aber value NIE überschreiben
(Kundenpflege); fehlende bleiben bestehen (als obsolet ausgewiesen).
- Baseline (blId) und Nachweise (nr): Upsert; fehlende bleiben erhalten.
- Deaktivierung greift nur im Paket-Namensraum (L00/R*/VA-*/BASELINE/NACHWEIS)
— verwaltete Register aus import-managed (CRYPTO/RISKMATRIX/CLASSIFICATION/
HANDBUCH) werden nicht angetastet.
- Änderungsreport (added/updated/archived/reactivated/unchanged/obsolete) wird
zurückgegeben und ins Audit-Log geschrieben (Entity policy_package). Option
{ dryRun: true } liefert die Vorschau ohne Schreibzugriff. Idempotent.
- Aktive Ansichten (Bibliothek/Coverage, Dokument-Detail) filtern archivedAt: null;
Historie bleibt per Direktlink erreichbar.
Akzeptanztest scripts/test-reimport.ts: Status/Override/Einreicher + Variablenwert
bleiben nach Re-Import erhalten, entfernte Anforderung wird deaktiviert (nicht
gelöscht), verwaltete Register unberührt, zweiter Lauf idempotent. Browser: /policies
rendert unverändert (30 Dokumente). tsc + lint + build + Guard-Check grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
89 lines
5.1 KiB
TypeScript
89 lines
5.1 KiB
TypeScript
import "dotenv/config";
|
|
import { join } from "node:path";
|
|
import { prisma } from "../src/server/db";
|
|
import { importPolicies } from "../prisma/import-policies";
|
|
|
|
const SEED_DIR = join(process.cwd(), "seed", "isms-vorlagenpaket-v2");
|
|
const ok = (c: boolean, m: string) => console.log(`${c ? "✓" : "✗ FEHLER"} ${m}`);
|
|
|
|
async function main() {
|
|
const tenant = await prisma.tenant.findFirst({ where: { slug: "demo" } });
|
|
if (!tenant) throw new Error("Demo-Mandant fehlt");
|
|
const t = tenant.id;
|
|
|
|
// Ausgangszustand eines echten Dokuments + einer Variable sichern
|
|
const doc0 = await prisma.policyDocument.findFirst({ where: { tenantId: t, code: "R08" } });
|
|
const varName = "ORG_NAME";
|
|
const var0 = await prisma.policyVariable.findFirst({ where: { tenantId: t, key: varName } });
|
|
if (!doc0 || !var0) throw new Error("Erwartetes Dokument R08 / Variable ORG_NAME fehlt");
|
|
|
|
// 1) Kuratierten Zustand simulieren: Freigabe-Status, TISAX-Override, Einreicher,
|
|
// geänderter Titel (Inhalts-Diff) + nutzergepflegter Variablenwert.
|
|
await prisma.policyDocument.update({
|
|
where: { id: doc0.id },
|
|
data: { status: "ENTWURF", protectionOverride: "AL3", submittedBy: "tester", title: "MANUELL GEÄNDERTER TITEL" },
|
|
});
|
|
await prisma.policyVariable.update({ where: { id: var0.id }, data: { value: "Mustermann Spezial GmbH" } });
|
|
|
|
// 2) Entfernte Anforderung simulieren (nicht im Paket): muss deaktiviert, nicht gelöscht werden.
|
|
const fakeReqId = "ZZ-REMOVED-TEST-1";
|
|
await prisma.policyRequirement.deleteMany({ where: { tenantId: t, reqId: fakeReqId } });
|
|
await prisma.policyRequirement.create({
|
|
data: { tenantId: t, reqId: fakeReqId, policyCode: "R08", control: "0.0.0", obligation: "MUSS", requirement: "Test", implementation: "" },
|
|
});
|
|
|
|
// 3) Dry-Run: Vorschau ohne Schreibzugriff
|
|
const preview = await importPolicies(prisma, t, SEED_DIR, { dryRun: true });
|
|
console.log("\n— Dry-Run-Report —");
|
|
console.log(JSON.stringify(preview.report, null, 0));
|
|
ok(preview.report.documents.updated >= 1, "Dry-Run erkennt Titeländerung an R08 (updated ≥ 1)");
|
|
ok(preview.report.documents.archived === 0, "Dry-Run deaktiviert KEINE verwalteten Register (CRYPTO/HANDBUCH/…)");
|
|
ok(preview.report.requirements.archived === 1, "Dry-Run erkennt 1 zu deaktivierende Anforderung");
|
|
const stillDraftAfterDry = await prisma.policyDocument.findUnique({ where: { id: doc0.id } });
|
|
ok(stillDraftAfterDry?.title === "MANUELL GEÄNDERTER TITEL", "Dry-Run schreibt NICHT (Titel unverändert)");
|
|
|
|
// 4) Echter Re-Import
|
|
const run1 = await importPolicies(prisma, t, SEED_DIR, { dryRun: false, actorId: null });
|
|
console.log("\n— Re-Import-Report —");
|
|
console.log(JSON.stringify(run1.report, null, 0));
|
|
|
|
const docA = await prisma.policyDocument.findUnique({ where: { id: doc0.id } });
|
|
ok(docA?.status === "ENTWURF", "Freigabe-Status bleibt erhalten (ENTWURF)");
|
|
ok(docA?.protectionOverride === "AL3", "TISAX-Override bleibt erhalten (AL3)");
|
|
ok(docA?.submittedBy === "tester", "Einreicher (Freigabe-Workflow) bleibt erhalten");
|
|
ok(docA?.title === doc0.title, "Titel wurde aus dem Paket aktualisiert (Inhalt)");
|
|
ok(docA?.archivedAt === null, "Dokument bleibt aktiv");
|
|
|
|
const varA = await prisma.policyVariable.findUnique({ where: { id: var0.id } });
|
|
ok(varA?.value === "Mustermann Spezial GmbH", "Nutzergepflegter Variablenwert bleibt erhalten");
|
|
|
|
const fakeA = await prisma.policyRequirement.findFirst({ where: { tenantId: t, reqId: fakeReqId } });
|
|
ok(!!fakeA && fakeA.archivedAt !== null, "Entfernte Anforderung ist deaktiviert (archivedAt gesetzt), NICHT gelöscht");
|
|
|
|
const auditA = await prisma.auditLog.findFirst({ where: { tenantId: t, entity: "policy_package" }, orderBy: { createdAt: "desc" } });
|
|
ok(!!auditA, "Änderungsreport im Audit-Log protokolliert");
|
|
|
|
// 5) Idempotenz: zweiter Lauf ohne externe Änderung → keine Diffs
|
|
const run2 = await importPolicies(prisma, t, SEED_DIR, { dryRun: false });
|
|
const r = run2.report;
|
|
const noChanges =
|
|
r.documents.added + r.documents.updated + r.documents.archived + r.documents.reactivated === 0 &&
|
|
r.requirements.added + r.requirements.updated + r.requirements.archived + r.requirements.reactivated === 0 &&
|
|
r.variables.added + r.variables.updated === 0 && r.baseline.added + r.baseline.updated === 0 &&
|
|
r.evidence.added + r.evidence.updated === 0;
|
|
console.log("\n— Idempotenz-Report —");
|
|
console.log(JSON.stringify(r, null, 0));
|
|
ok(noChanges, "Zweiter Lauf ist idempotent (keine added/updated/archived/reactivated)");
|
|
|
|
// Aufräumen: Testartefakt entfernen, kuratierten Zustand zurücksetzen
|
|
await prisma.policyRequirement.deleteMany({ where: { tenantId: t, reqId: fakeReqId } });
|
|
await prisma.policyDocument.update({
|
|
where: { id: doc0.id },
|
|
data: { status: doc0.status, protectionOverride: doc0.protectionOverride, submittedBy: doc0.submittedBy, title: doc0.title, archivedAt: null },
|
|
});
|
|
await prisma.policyVariable.update({ where: { id: var0.id }, data: { value: var0.value } });
|
|
console.log("\n✓ aufgeräumt (Testartefakte entfernt, R08/ORG_NAME zurückgesetzt)");
|
|
}
|
|
|
|
main().then(() => process.exit(0)).catch((e) => { console.error(e); process.exit(1); });
|