"""Job 상태 머신 + in-proc 브로커 단위 테스트.""" from __future__ import annotations import time import pytest from luke_scribe.errors import JobAlreadyTerminal, QueueFull from luke_scribe.jobqueue.broker import InProcBroker from luke_scribe.jobqueue.jobs import Job, JobStatus class TestJobStateMachine: def test_legal_transitions(self): job = Job() assert job.status == JobStatus.QUEUED job.transition(JobStatus.PROCESSING) assert job.status == JobStatus.PROCESSING job.transition(JobStatus.COMPLETED) assert job.status == JobStatus.COMPLETED def test_illegal_transition_rejected(self): job = Job() job.transition(JobStatus.PROCESSING) job.transition(JobStatus.COMPLETED) with pytest.raises(JobAlreadyTerminal): job.transition(JobStatus.PROCESSING) # terminal에서 되돌아갈 수 없음 def test_queued_cancel(self): job = Job() job.transition(JobStatus.CANCELLED) assert job.status == JobStatus.CANCELLED assert job.cancelled_at is not None def test_lease_expiry(self): job = Job() job.refresh_lease(60.0, now=1000.0) assert not job.lease_expired(now=1050.0) assert job.lease_expired(now=1100.0) def test_attempted_profiles_persisted(self): job = Job(attempted_profiles=["float16", "int8_float16"]) assert job.to_meta()["attempted_profiles"] == ["float16", "int8_float16"] restored = Job.from_meta(job.to_meta()) assert restored.attempted_profiles == ["float16", "int8_float16"] class TestInProcBroker: def test_enqueue_claim_complete(self): broker = InProcBroker() job = Job() broker.enqueue(job) claimed = broker.claim_next("w1", 60.0) assert claimed is not None assert claimed.status == JobStatus.PROCESSING broker.transition(claimed, JobStatus.COMPLETED) assert broker.get(job.id).status == JobStatus.COMPLETED def test_queue_position(self): broker = InProcBroker() a, b = Job(), Job() broker.enqueue(a) broker.enqueue(b) assert a.queue_position == 0 assert b.queue_position == 1 def test_queue_full(self): broker = InProcBroker() broker.settings.max_queue = 2 broker.enqueue(Job()) broker.enqueue(Job()) with pytest.raises(QueueFull): broker.enqueue(Job()) def test_cancel_queued(self): broker = InProcBroker() job = Job() broker.enqueue(job) cancelled = broker.cancel(job.id) assert cancelled.status == JobStatus.CANCELLED assert broker.queue_depth() == 0 def test_cancel_processing_sets_flag(self): broker = InProcBroker() job = Job() broker.enqueue(job) broker.claim_next("w1", 60.0) cancelled = broker.cancel(job.id) assert cancelled.cancel_requested is True assert cancelled.status == JobStatus.PROCESSING # 워커가 세그먼트 경계에서 종료 def test_reconcile_stale_recovers_crash(self): """Eng 리뷰 P11: 워커 크래시 → stale processing → failed 복구.""" broker = InProcBroker() job = Job() broker.enqueue(job) broker.claim_next("w1", 60.0) # processing + lease 60s # lease 만료 시뮬레이션 job2 = broker.get(job.id) job2.lease_expires_at = time.time() - 1 broker.save_meta(job2) reclaimed = broker.reconcile_stale(60.0) assert job.id in reclaimed assert broker.get(job.id).status == JobStatus.FAILED assert broker.get(job.id).error_code == "worker_crash"