import { describe, it, expect, beforeEach, vi } from 'vitest' // Module-level mutable state — reset to a fresh instance per test (L139 interlock). type Lock = typeof import('../src/main/ytdlpLock') let lock: Lock beforeEach(async () => { vi.resetModules() lock = await import('../src/main/ytdlpLock') }) describe('ytdlpLock (L139 spawn↔update interlock)', () => { it('starts idle: not updating, zero spawns, settle already resolved', async () => { expect(lock.isYtdlpUpdating()).toBe(false) expect(lock.liveSpawnCount()).toBe(0) await expect(lock.whenYtdlpUpdateSettled()).resolves.toBeUndefined() }) it('acquire/release tracks the live count and clamps at zero', () => { lock.acquireSpawn() lock.acquireSpawn() expect(lock.liveSpawnCount()).toBe(2) lock.releaseSpawn() expect(lock.liveSpawnCount()).toBe(1) lock.releaseSpawn() lock.releaseSpawn() // extra release must not underflow expect(lock.liveSpawnCount()).toBe(0) }) it('beginYtdlpUpdate refuses (and does NOT take the lock) while a spawn is live', () => { lock.acquireSpawn() expect(lock.beginYtdlpUpdate()).toBe(false) expect(lock.isYtdlpUpdating()).toBe(false) lock.releaseSpawn() expect(lock.beginYtdlpUpdate()).toBe(true) expect(lock.isYtdlpUpdating()).toBe(true) lock.endYtdlpUpdate() expect(lock.isYtdlpUpdating()).toBe(false) }) it('holds a spawn until endYtdlpUpdate resolves whenYtdlpUpdateSettled', async () => { expect(lock.beginYtdlpUpdate()).toBe(true) let resolved = false const held = lock.whenYtdlpUpdateSettled().then(() => { resolved = true }) await Promise.resolve() // flush a microtask tick — still pending mid-update expect(resolved).toBe(false) lock.endYtdlpUpdate() await held expect(resolved).toBe(true) }) it('resolves immediately (no hold) when no update is in progress', async () => { let resolved = false await lock.whenYtdlpUpdateSettled().then(() => { resolved = true }) expect(resolved).toBe(true) }) it('refuses a CONCURRENT second update — the first end() must release only its own lock', async () => { expect(lock.beginYtdlpUpdate()).toBe(true) // A racing second begin (startup auto-update vs manual "Update now") must be // refused — accepting it would swap the settle promise, letting the first // update's end() release spawns held by the still-running second update. expect(lock.beginYtdlpUpdate()).toBe(false) expect(lock.isYtdlpUpdating()).toBe(true) // the refusal didn't clobber the lock let resolved = false const held = lock.whenYtdlpUpdateSettled().then(() => { resolved = true }) await Promise.resolve() expect(resolved).toBe(false) // still held by the rightful owner lock.endYtdlpUpdate() await held expect(resolved).toBe(true) expect(lock.isYtdlpUpdating()).toBe(false) }) it('a second update after one settles holds again on a fresh promise', async () => { lock.beginYtdlpUpdate() lock.endYtdlpUpdate() expect(lock.beginYtdlpUpdate()).toBe(true) let resolved = false const held = lock.whenYtdlpUpdateSettled().then(() => { resolved = true }) await Promise.resolve() expect(resolved).toBe(false) lock.endYtdlpUpdate() await held expect(resolved).toBe(true) }) })