import { jobProgress } from "./ops.service"; /** Shape run_all.py emits, with the shell trace lines it interleaves. */ const line = (i: number, n: number, name: string) => `[paso ${i}/${n}] ${name}\n+ /repo/migration/.venv/bin/python /repo/migration/${name} --env prod\n[${name}] target env: prod\n validation: OK`; describe("jobProgress", () => { it("returns null before any step marker appears", () => { // The safety backup runs before run_all.py, so this is the real state for // the first stretch of every REIMPORT. expect(jobProgress("== Respaldo de seguridad previo ==\ntablas capturadas: 39", "RUNNING")).toBeNull(); }); it("returns null for jobs that have no steps at all", () => { // BACKUP/RESTORE are a single mysqldump; a fabricated percentage would be // worse than none. expect(jobProgress("mysqldump ... done", "SUCCESS")).toBeNull(); }); it("tracks the most recent marker, not the first", () => { const log = [line(1, 9, "transform_customers.py"), line(2, 9, "transform_properties.py")].join("\n"); const p = jobProgress(log, "RUNNING"); expect(p).toMatchObject({ step: 2, total: 9, name: "transform_properties.py" }); }); /** * The point of the whole feature. While RUNNING, step i is IN PROGRESS, so * only i-1 are done. Counting i as complete would show 100% while the final * and slowest step (blob_extract) is still working. */ it("does not claim a running step is finished", () => { expect(jobProgress(line(1, 9, "transform_customers.py"), "RUNNING")?.percent).toBe(0); expect(jobProgress(line(9, 9, "blob_extract.py"), "RUNNING")?.percent).toBe(88); }); it("reaches 100 only once the job is no longer running", () => { expect(jobProgress(line(9, 9, "blob_extract.py"), "SUCCESS")?.percent).toBe(100); }); /** A job that died mid-way must report where it died, not 100%. */ it("reports the failed step rather than completion", () => { const p = jobProgress(line(5, 9, "transform_transactions.py"), "FAILED"); expect(p).toMatchObject({ step: 5, total: 9 }); expect(p!.percent).toBe(55); }); it("handles the 8-step SYNC list as well as the 9-step REIMPORT one", () => { expect(jobProgress(line(8, 8, "transform_bank.py"), "SUCCESS")?.percent).toBe(100); expect(jobProgress(line(4, 8, "transform_policies.py"), "RUNNING")?.percent).toBe(37); }); /** * Captured verbatim from `run_all.run(..., step=8, total=9)`. This is the * contract between the Python and this parser; if run_all.py's format * changes, this fails rather than the panel silently showing no progress. */ it("parses the exact line run_all.py emits", () => { const real = "[paso 8/9] transform_bank.py\n+ /repo/migration/.venv/bin/python /repo/migration/transform_bank.py --env prod"; expect(jobProgress(real, "RUNNING")).toMatchObject({ step: 8, total: 9, name: "transform_bank.py", percent: 77, }); }); it("ignores a malformed marker instead of reporting NaN", () => { expect(jobProgress("[paso 3/0] x.py", "RUNNING")).toBeNull(); }); /** The marker must be at line start so log text quoting it cannot spoof it. */ it("does not match a marker embedded mid-line", () => { expect(jobProgress("some output mentioning [paso 4/9] fake.py", "RUNNING")).toBeNull(); }); });