Files
sciagent/be0/tests/test_imagehub_tasks.py
T
Thinh Lam 688fac73e9
CI/CD / backend (push) Failing after 2m8s
CI/CD / frontend (push) Failing after 1m40s
CI/CD / deploy (push) Has been skipped
sciagent code + Gitea Actions CI/CD
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:38:30 +07:00

158 lines
6.4 KiB
Python

"""Unit tests for the ImageHub task-pipeline domain (project-workflow §3/§4 transitions).
The pipeline service is pure (plain functions over ``StageInfo`` lists — no Postgres, no FastAPI),
so the whole state machine is exercised here with plain data. The thin HTTP wrappers in
``imagehub_routes`` are verified live; this file owns the transition rules.
Covers: stage ordering, the new-task start state, TP1 finalize (advance / -> Ground Truth),
TP2 accept + accept-with-corrections, TP3 reject (-> first stage), the guards (wrong-state finalize/
review, bad decision), and RS1 reference-standard (Ground-Truth-only).
"""
from __future__ import annotations
import unittest
from src.imagehub_task_pipeline import (
StageInfo,
TaskPipelineError,
compute_finalize,
compute_review,
first_stage,
initial_transition,
order_stages,
stage_after,
state_for_stage,
validate_set_reference,
)
# A canonical pipeline: Label(0) -> Review_1(1) -> Review_2(2). Deliberately unsorted in the list
# so order_stages / first_stage / stage_after are actually exercised, not given pre-sorted input.
LABEL = StageInfo(id="s-label", kind="label", seq=0)
REV1 = StageInfo(id="s-rev1", kind="review", seq=1)
REV2 = StageInfo(id="s-rev2", kind="review", seq=2)
PIPELINE = [REV2, LABEL, REV1] # out of order on purpose
class StageOrderingTests(unittest.TestCase):
def test_order_stages_sorts_by_seq(self):
self.assertEqual([s.id for s in order_stages(PIPELINE)], ["s-label", "s-rev1", "s-rev2"])
def test_first_stage_is_lowest_seq(self):
self.assertEqual(first_stage(PIPELINE).id, "s-label")
def test_first_stage_empty_raises_409(self):
with self.assertRaises(TaskPipelineError) as ctx:
first_stage([])
self.assertEqual(ctx.exception.status, 409)
def test_stage_after_returns_next(self):
self.assertEqual(stage_after(PIPELINE, "s-label").id, "s-rev1")
self.assertEqual(stage_after(PIPELINE, "s-rev1").id, "s-rev2")
def test_stage_after_last_is_none(self):
self.assertIsNone(stage_after(PIPELINE, "s-rev2"))
def test_stage_after_unknown_or_none_is_none(self):
self.assertIsNone(stage_after(PIPELINE, "nope"))
self.assertIsNone(stage_after(PIPELINE, None))
def test_state_for_stage(self):
self.assertEqual(state_for_stage(LABEL), "inLabel")
self.assertEqual(state_for_stage(REV1), "inReview")
class InitialTransitionTests(unittest.TestCase):
def test_new_task_starts_inlabel_at_first_stage(self):
t = initial_transition(PIPELINE)
self.assertEqual(t.pipeline_state, "inLabel")
self.assertEqual(t.current_stage_id, "s-label")
self.assertEqual(t.queue_status, "assigned")
def test_new_task_starts_inreview_when_first_stage_is_review(self):
t = initial_transition([REV1])
self.assertEqual(t.pipeline_state, "inReview")
self.assertEqual(t.current_stage_id, "s-rev1")
class FinalizeTests(unittest.TestCase):
def test_finalize_label_advances_to_first_review(self):
t = compute_finalize("inLabel", "s-label", PIPELINE)
self.assertEqual(t.pipeline_state, "inReview")
self.assertEqual(t.current_stage_id, "s-rev1")
self.assertEqual(t.queue_status, "assigned")
def test_finalize_advances_label_to_next_label(self):
# PreLabel(0,label) -> Label(1,label): finalizing the first stays inLabel at the next.
prelabel = StageInfo(id="s-pre", kind="label", seq=0)
label = StageInfo(id="s-lab", kind="label", seq=1)
t = compute_finalize("inLabel", "s-pre", [prelabel, label])
self.assertEqual(t.pipeline_state, "inLabel")
self.assertEqual(t.current_stage_id, "s-lab")
def test_finalize_single_label_goes_to_ground_truth(self):
t = compute_finalize("inLabel", "s-label", [LABEL])
self.assertEqual(t.pipeline_state, "groundTruth")
self.assertIsNone(t.current_stage_id)
def test_finalize_when_not_inlabel_raises_409(self):
with self.assertRaises(TaskPipelineError) as ctx:
compute_finalize("inReview", "s-rev1", PIPELINE)
self.assertEqual(ctx.exception.status, 409)
def test_finalize_when_groundtruth_raises_409(self):
with self.assertRaises(TaskPipelineError):
compute_finalize("groundTruth", None, PIPELINE)
class ReviewTests(unittest.TestCase):
def test_accept_advances_to_next_review(self):
t = compute_review("inReview", "s-rev1", PIPELINE, "accept")
self.assertEqual(t.pipeline_state, "inReview")
self.assertEqual(t.current_stage_id, "s-rev2")
def test_accept_on_last_review_goes_to_ground_truth(self):
t = compute_review("inReview", "s-rev2", PIPELINE, "accept")
self.assertEqual(t.pipeline_state, "groundTruth")
self.assertIsNone(t.current_stage_id)
def test_accept_with_corrections_advances(self):
t = compute_review("inReview", "s-rev1", PIPELINE, "acceptWithCorrections")
self.assertEqual(t.pipeline_state, "inReview")
self.assertEqual(t.current_stage_id, "s-rev2")
def test_reject_returns_to_first_stage(self):
t = compute_review("inReview", "s-rev2", PIPELINE, "reject")
self.assertEqual(t.pipeline_state, "inLabel")
self.assertEqual(t.current_stage_id, "s-label")
self.assertEqual(t.queue_status, "assigned")
def test_review_when_not_inreview_raises_409(self):
with self.assertRaises(TaskPipelineError) as ctx:
compute_review("inLabel", "s-label", PIPELINE, "accept")
self.assertEqual(ctx.exception.status, 409)
def test_review_invalid_decision_raises_422(self):
with self.assertRaises(TaskPipelineError) as ctx:
compute_review("inReview", "s-rev1", PIPELINE, "maybe")
self.assertEqual(ctx.exception.status, 422)
class ReferenceStandardTests(unittest.TestCase):
def test_set_reference_ok_on_ground_truth(self):
# Should not raise.
validate_set_reference("groundTruth", True)
def test_set_reference_blocked_off_ground_truth(self):
with self.assertRaises(TaskPipelineError) as ctx:
validate_set_reference("inLabel", True)
self.assertEqual(ctx.exception.status, 409)
def test_unset_reference_allowed_in_any_state(self):
# Removing the flag is always fine, even mid-pipeline.
validate_set_reference("inLabel", False)
validate_set_reference("inReview", False)
if __name__ == "__main__":
unittest.main()