import assert from "node:assert/strict"; import { test } from "node:test"; import { UniqueID } from "@repo/rdx-ddd"; import { Maybe } from "@repo/rdx-utils"; import type { ProcessReviewSources } from "../application/process-review.builder"; import { buildProcessReview } from "../application/process-review.builder"; import { PersonProcessPersistenceMapper } from "../infrastructure/persistence/process-persistence.mapper"; import { PersonProcess } from "./process.aggregate"; import { ProcessTask } from "./process-task.entity"; import { ProcessTemplate } from "./process-template.aggregate"; import type { ProcessValuesInput } from "./process-values"; import { ProcessTemplateInputSchema, ProcessValuesSchema, StartProcessInputSchema, } from "./process-values"; import { ProcessTemplateValues } from "./value-objects/process-template-values.vo"; import { ProcessValues } from "./value-objects/process-values.vo"; const company = UniqueID.generateNewID(); const actor = UniqueID.generateNewID(); const person = UniqueID.generateNewID(); const templateId = UniqueID.generateNewID(); const now = "2026-10-06T12:00:00.000Z"; function values(input?: ProcessValuesInput) { const result = ProcessValues.create( input ?? { personId: person.toPrimitive(), templateId: templateId.toPrimitive(), templateName: "Salida", kind: "offboarding", name: "Salida Ana", state: "open", notes: null, tasks: [ProcessTask.create("Revisar correo").toPrimitive()], } ); assert.ok(result.isSuccess); return result.data; } function process() { const result = PersonProcess.create(company, values(), Maybe.some(actor), now); assert.ok(result.isSuccess); return result.data; } test("procesos: contratos estrictos, normalización y límites", () => { const input = values().toPrimitive(); assert.ok(ProcessValuesSchema.safeParse(input).success); assert.equal(ProcessValuesSchema.safeParse({ ...input, extra: true }).success, false); assert.equal( ProcessValuesSchema.safeParse({ ...input, tasks: [input.tasks[0], input.tasks[0]] }).success, false ); assert.equal( ProcessTemplateInputSchema.safeParse({ name: "Plantilla", kind: "change", active: true, notes: null, instructions: ["Duplicada", "duplicada"], }).success, false ); assert.equal( StartProcessInputSchema.safeParse({ personId: person.toPrimitive(), templateId: templateId.toPrimitive(), name: "A", notes: null, actorId: actor.toPrimitive(), }).success, false ); }); test("procesos: UUID v7, tareas con identidad, actualización idempotente e historial", () => { const initial = process(); assert.equal(initial.id.toPrimitive()[14], "7"); assert.equal(initial.values.tasks[0]?.id.toPrimitive()[14], "7"); assert.equal(initial.history[0]?.id.toPrimitive()[14], "7"); const identical = initial.update(values(initial.values.toPrimitive()), actor, now); assert.ok(identical.isSuccess); assert.equal(identical.data, initial); const changed = initial.update( values({ ...initial.values.toPrimitive(), notes: "Revisión" }), actor, now ); assert.ok(changed.isSuccess); assert.ok(changed.data.id.equals(initial.id)); assert.equal(changed.data.history.length, 2); assert.equal(initial.history.length, 1); assert.equal( changed.data.history[1]?.actorId.getOrUndefined()?.toPrimitive(), actor.toPrimitive() ); }); test("procesos: completar exige todas resueltas; cierre terminal y sujeto/tareas fijos", () => { const initial = process(); const primitive = initial.values.toPrimitive(); assert.equal( initial.update(values({ ...primitive, state: "completed" }), actor, now).isSuccess, false ); assert.equal( initial.update( values({ ...primitive, personId: UniqueID.generateNewID().toPrimitive() }), actor, now ).isSuccess, false ); assert.equal( initial.update( values({ ...primitive, tasks: primitive.tasks.map((t) => ({ ...t, title: "Otra tarea" })) }), actor, now ).isSuccess, false ); const finished = initial.update( values({ ...primitive, state: "completed", tasks: primitive.tasks.map((t) => ({ ...t, status: "not_applicable" })), }), actor, now ); assert.ok(finished.isSuccess); assert.equal( finished.data.update( values({ ...finished.data.values.toPrimitive(), notes: "Alterar cerrado" }), actor, now ).isSuccess, false ); const same = finished.data.update(finished.data.values, actor, now); assert.ok(same.isSuccess); assert.equal(same.data, finished.data); }); test("procesos: añadir revisiones conserva las resueltas e identidades y no duplica", () => { const initial = process(); const reference = { kind: "remote" as const, id: UniqueID.generateNewID().toPrimitive(), label: "VPN", sourceKind: "personal_computer" as const, }; const appended = initial.appendReviews([reference], actor, now); assert.ok(appended.isSuccess); assert.equal(appended.data.values.tasks.length, 2); assert.ok(appended.data.values.tasks[0]?.id.equals(initial.values.tasks[0]?.id)); const primitive = appended.data.values.toPrimitive(); const resolved = appended.data.update( values({ ...primitive, tasks: primitive.tasks.map((t) => ({ ...t, status: "done" })) }), actor, now ); assert.ok(resolved.isSuccess); const repeat = resolved.data.appendReviews([reference], actor, now); assert.ok(repeat.isSuccess); assert.equal(repeat.data, resolved.data); assert.equal(repeat.data.history.length, 3); }); test("plantillas: copia de instrucciones, igualdad semántica y cambios independientes", () => { const result = ProcessTemplateValues.create({ name: "Salida", kind: "offboarding", active: true, notes: null, instructions: ["Comprobar"], }); assert.ok(result.isSuccess); const template = ProcessTemplate.create(result.data, Maybe.some(actor), now); assert.ok(template.isSuccess); const initial = process(); const input = { ...result.data.toPrimitive(), instructions: ["Nueva lista"], active: false }; const changed = ProcessTemplateValues.create(input); assert.ok(changed.isSuccess); const updated = template.data.update(changed.data, actor, now); assert.ok(updated.isSuccess); assert.equal(updated.data.history.length, 2); assert.equal(initial.values.tasks[0]?.toPrimitive().title, "Revisar correo"); }); test("procesos: rehidratación bidireccional y rechazo de raíces/historial alterados", () => { const mapper = new PersonProcessPersistenceMapper(); const initial = process(); const record = mapper.toPersistence(initial); assert.deepEqual( mapper.toDomain(record.root, record.revisions).values.toPrimitive(), initial.values.toPrimitive() ); assert.throws(() => mapper.toDomain({ ...record.root, currentKey: "bad" }, record.revisions)); assert.throws(() => mapper.toDomain(record.root, [])); assert.throws(() => mapper.toDomain( record.root, record.revisions.map((r) => ({ ...r, actorId: null })) ) ); assert.throws(() => mapper.toDomain( record.root, record.revisions.map((r) => ({ ...r, position: 3 })) ) ); }); const sources: ProcessReviewSources = { assets: [ { id: UniqueID.generateNewID().toPrimitive(), personId: person.toPrimitive(), name: "Portátil", available: true, }, ], profiles: [], authorizations: [], configurations: [ { id: UniqueID.generateNewID().toPrimitive(), personId: person.toPrimitive(), kind: "remote", label: "VPN móvil", state: "configured", sourceKind: "personal_mobile", }, { id: UniqueID.generateNewID().toPrimitive(), personId: person.toPrimitive(), kind: "mail", label: "Cerrado", state: "removed", }, { id: UniqueID.generateNewID().toPrimitive(), personId: actor.toPrimitive(), kind: "mail", label: "Otra persona", state: "configured", }, ], relationships: [ { id: UniqueID.generateNewID().toPrimitive(), personId: person.toPrimitive(), kind: "account", label: "Cuenta antigua aún configurada", endDate: "2000-01-01", configuration: "configured", }, ], }; test("revisión: personales, retirada pendiente y relaciones finalizadas aún configuradas", () => { const review = buildProcessReview(person.toPrimitive(), sources, "2026-10-06"); assert.equal(review.length, 3); assert.ok(review.some((r) => r.kind === "remote" && r.sourceKind === "personal_mobile")); assert.ok(review.some((r) => r.kind === "asset")); assert.ok(review.some((r) => r.kind === "account")); }); test("revisión: incluye configuraciones de perfiles archivados, excluye cerradas y evita duplicar", () => { const input = { ...sources, relationships: [...sources.relationships, ...sources.relationships], configurations: [ ...sources.configurations, { id: UniqueID.generateNewID().toPrimitive(), personId: person.toPrimitive(), kind: "printer" as const, label: "Archivado pero por retirar", state: "removal_pending", }, ], }; const review = buildProcessReview(person.toPrimitive(), input, "2026-10-06"); assert.equal(review.length, 4); assert.ok(review.some((r) => r.label === "Archivado pero por retirar")); });