import { describe, it, expect } from "vitest"; import { migrationReducer, INITIAL_STATE } from "./useCategoryMigration"; import type { MigrationPlan, MappingRow, } from "../services/categoryMappingService"; import type { BackupResult } from "../services/categoryBackupService"; import type { MigrationOutcome } from "../services/categoryMigrationService"; // --------------------------------------------------------------------------- // Fixture helpers // --------------------------------------------------------------------------- function makeRow(v2: number, v1: number | null): MappingRow { return { v2CategoryId: v2, v2CategoryName: `v2-${v2}`, v1TargetId: v1, v1TargetName: v1 === null ? null : `v1-${v1}`, confidence: v1 === null ? "none" : "high", reason: v1 === null ? "review" : "keyword", }; } function makePlan(rows: MappingRow[], preserved: MappingRow[] = []): MigrationPlan { const unresolved = rows.filter((r) => r.v1TargetId === null); return { rows, preserved, unresolved, stats: { total: rows.length, high: rows.filter((r) => r.confidence === "high").length, medium: rows.filter((r) => r.confidence === "medium").length, low: rows.filter((r) => r.confidence === "low").length, none: unresolved.length, }, }; } const FAKE_BACKUP: BackupResult = { path: "/tmp/backup.sref", size: 1024, checksum: "abc", verifiedAt: new Date().toISOString(), encrypted: false, }; const FAKE_OUTCOME: MigrationOutcome = { succeeded: true, insertedV1Count: 139, updatedTransactionsCount: 100, updatedBudgetsCount: 10, updatedKeywordsCount: 20, deletedV2Count: 40, customPreservedCount: 2, backupPath: "/tmp/backup.sref", }; // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("migrationReducer", () => { it("starts on discover with no plan / backup / outcome", () => { expect(INITIAL_STATE.step).toBe("discover"); expect(INITIAL_STATE.plan).toBeNull(); expect(INITIAL_STATE.backup).toBeNull(); expect(INITIAL_STATE.outcome).toBeNull(); expect(INITIAL_STATE.unresolved).toBe(0); }); it("LOAD_PLAN stores plan and counts unresolved rows", () => { const plan = makePlan([makeRow(10, 1011), makeRow(11, null)]); const next = migrationReducer(INITIAL_STATE, { type: "LOAD_PLAN", plan }); expect(next.plan).toBe(plan); expect(next.unresolved).toBe(1); expect(next.errors).toEqual([]); }); it("RESOLVE_ROW sets target, bumps confidence from none to medium, decreases unresolved", () => { const plan = makePlan([makeRow(10, null), makeRow(11, null)]); const s1 = migrationReducer(INITIAL_STATE, { type: "LOAD_PLAN", plan }); expect(s1.unresolved).toBe(2); const s2 = migrationReducer(s1, { type: "RESOLVE_ROW", v2CategoryId: 10, v1TargetId: 1011, v1TargetName: "Paie régulière", }); expect(s2.unresolved).toBe(1); const resolved = s2.plan!.rows.find((r) => r.v2CategoryId === 10)!; expect(resolved.v1TargetId).toBe(1011); expect(resolved.v1TargetName).toBe("Paie régulière"); expect(resolved.confidence).toBe("medium"); }); it("GO_NEXT blocks simulate -> consent when unresolved > 0", () => { const plan = makePlan([makeRow(10, null)]); let s = migrationReducer(INITIAL_STATE, { type: "LOAD_PLAN", plan }); s = migrationReducer(s, { type: "GO_NEXT" }); // discover -> simulate expect(s.step).toBe("simulate"); const sBlocked = migrationReducer(s, { type: "GO_NEXT" }); expect(sBlocked.step).toBe("simulate"); // blocked because unresolved=1 }); it("GO_NEXT advances simulate -> consent once all rows are resolved", () => { const plan = makePlan([makeRow(10, null)]); let s = migrationReducer(INITIAL_STATE, { type: "LOAD_PLAN", plan }); s = migrationReducer(s, { type: "GO_NEXT" }); // simulate s = migrationReducer(s, { type: "RESOLVE_ROW", v2CategoryId: 10, v1TargetId: 1011, v1TargetName: "Paie régulière", }); s = migrationReducer(s, { type: "GO_NEXT" }); // consent expect(s.step).toBe("consent"); }); it("GO_BACK goes simulate -> discover and consent -> simulate", () => { const plan = makePlan([makeRow(10, 1011)]); let s = migrationReducer(INITIAL_STATE, { type: "LOAD_PLAN", plan }); s = migrationReducer(s, { type: "GO_NEXT" }); // simulate s = migrationReducer(s, { type: "GO_NEXT" }); // consent expect(s.step).toBe("consent"); s = migrationReducer(s, { type: "GO_BACK" }); expect(s.step).toBe("simulate"); s = migrationReducer(s, { type: "GO_BACK" }); expect(s.step).toBe("discover"); }); it("START_RUN moves the step to running and clears errors", () => { const base = { ...INITIAL_STATE, step: "consent" as const, errors: ["old"] }; const s = migrationReducer(base, { type: "START_RUN" }); expect(s.step).toBe("running"); expect(s.errors).toEqual([]); }); it("SET_BACKUP stores the backup and keeps the current step", () => { const base = { ...INITIAL_STATE, step: "running" as const }; const s = migrationReducer(base, { type: "SET_BACKUP", backup: FAKE_BACKUP }); expect(s.backup).toBe(FAKE_BACKUP); expect(s.step).toBe("running"); }); it("SET_OUTCOME moves to success on succeeded=true", () => { const base = { ...INITIAL_STATE, step: "running" as const }; const s = migrationReducer(base, { type: "SET_OUTCOME", outcome: FAKE_OUTCOME, }); expect(s.step).toBe("success"); expect(s.outcome).toBe(FAKE_OUTCOME); expect(s.errors).toEqual([]); }); it("SET_OUTCOME moves to error when outcome.succeeded=false and records the error", () => { const failed: MigrationOutcome = { ...FAKE_OUTCOME, succeeded: false, error: "sql" }; const base = { ...INITIAL_STATE, step: "running" as const }; const s = migrationReducer(base, { type: "SET_OUTCOME", outcome: failed }); expect(s.step).toBe("error"); expect(s.outcome).toBe(failed); expect(s.errors).toContain("sql"); }); it("FAIL appends to errors and moves to error step", () => { const base = { ...INITIAL_STATE, step: "running" as const }; const s = migrationReducer(base, { type: "FAIL", error: "backup failed" }); expect(s.step).toBe("error"); expect(s.errors).toEqual(["backup failed"]); }); it("SELECT_ROW stores the selected row id", () => { const s1 = migrationReducer(INITIAL_STATE, { type: "SELECT_ROW", v2CategoryId: 42 }); expect(s1.selectedRowV2Id).toBe(42); const s2 = migrationReducer(s1, { type: "SELECT_ROW", v2CategoryId: null }); expect(s2.selectedRowV2Id).toBeNull(); }); it("RESET returns the initial state", () => { const middle = { ...INITIAL_STATE, step: "success" as const, errors: ["x"], outcome: FAKE_OUTCOME, }; const s = migrationReducer(middle, { type: "RESET" }); expect(s).toEqual(INITIAL_STATE); }); });