From 44edf320a004d1027630e3bbae21c298430725af Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 15 Sep 2026 10:13:07 -0700 Subject: [PATCH] reset job state on success --- client/src/Pages/Logs/index.tsx | 2 +- .../src/domain/jobs/job.repository.mongo.ts | 4 ++ .../test/integration/jobsRepository.test.ts | 53 ++++++++++++++++++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/client/src/Pages/Logs/index.tsx b/client/src/Pages/Logs/index.tsx index 9d71e3253..b21e331c5 100644 --- a/client/src/Pages/Logs/index.tsx +++ b/client/src/Pages/Logs/index.tsx @@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next"; const LogsPage = () => { const { t } = useTranslation(); - const [activeTab, setActiveTab] = useState(1); + const [activeTab, setActiveTab] = useState(0); return ( { it("does not release a lock owned by another worker (fenced no-op)", async () => { // A worker whose lease expired and was reclaimed must not clobber the new owner's lock. - await seedJob({ lockedBy: "other-worker", lockedUntil: NOW + LOCK_MS, runCount: 7 }); + await seedJob({ lockedBy: "other-worker", lockedUntil: NOW + LOCK_MS, runCount: 7, failCount: 2, lastFailReason: "timeout" }); const ok = await repo.recordSuccess("check:mon-1", NOW, 60_000, NOW + 1_000); @@ -189,6 +189,55 @@ describe("MongoJobsRepository", () => { expect(row?.lockedBy).toBe("other-worker"); // lock untouched expect(row?.lockedUntil).toBe(NOW + LOCK_MS); expect(row?.runCount).toBe(7); // not double-counted + expect(row?.failCount).toBe(2); // failure state untouched + expect(row?.lastFailReason).toBe("timeout"); + }); + + it("clears the failure state so failCount counts consecutive failures", async () => { + await seedJob({ lockedBy: ownedBy(repo), lockedUntil: NOW + LOCK_MS, failCount: 3, lastFailReason: "connection refused" }); + + await repo.recordSuccess("check:mon-1", NOW, 60_000, NOW + 1_000); + + const row = await readRow("check:mon-1"); + expect(row?.failCount).toBe(0); + expect(row?.lastFailReason).toBeNull(); + }); + }); + + // ── recordOneShot (park after a single run) ───────────────────────────────── + describe("recordOneShot", () => { + it("parks the job, releases the lease, counts the run and clears the failure state", async () => { + await seedJob({ + intervalMs: null, + lockedBy: ownedBy(repo), + lockedUntil: NOW + LOCK_MS, + runCount: 1, + failCount: 2, + lastFailReason: "connection refused", + }); + + const ok = await repo.recordOneShot("check:mon-1", NOW + 1_000); + + expect(ok).toBe(true); + const row = await readRow("check:mon-1"); + expect(row?.nextScheduledAt).toBe(PARKED); + expect(row?.lockedBy).toBeNull(); + expect(row?.lockedUntil).toBeNull(); + expect(row?.lastFinishedAt).toBe(NOW + 1_000); + expect(row?.runCount).toBe(2); + expect(row?.failCount).toBe(0); + expect(row?.lastFailReason).toBeNull(); + }); + + it("does not touch a lock owned by another worker (fenced no-op)", async () => { + await seedJob({ intervalMs: null, lockedBy: "other-worker", lockedUntil: NOW + LOCK_MS, failCount: 2, lastFailReason: "timeout" }); + + expect(await repo.recordOneShot("check:mon-1", NOW + 1_000)).toBe(false); + const row = await readRow("check:mon-1"); + expect(row?.nextScheduledAt).toBe(NOW); // not parked + expect(row?.lockedBy).toBe("other-worker"); + expect(row?.failCount).toBe(2); + expect(row?.lastFailReason).toBe("timeout"); }); });