Bin
2025-12-17 2e6c955be321cefd7e0c4a3031eab805e0a5a303
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
"""
Label Studio Open Source FSM Integration Tests.
 
Tests the core FSM functionality with real Django models in LSO,
focusing on coverage of state_manager.py and utils modules.
"""
 
import logging
from datetime import datetime, timezone
from unittest.mock import patch
 
import pytest
from core.current_request import CurrentContext
from django.contrib.auth import get_user_model
from django.core.cache import cache
from fsm.state_choices import AnnotationStateChoices, ProjectStateChoices, TaskStateChoices
from fsm.state_manager import StateManager, StateManagerError
from fsm.state_models import AnnotationState, ProjectState, TaskState
from fsm.utils import get_current_state_safe, is_fsm_enabled, resolve_organization_id
from organizations.tests.factories import OrganizationFactory
from projects.tests.factories import ProjectFactory
from tasks.models import Annotation
from tasks.tests.factories import AnnotationFactory, TaskFactory
from users.tests.factories import UserFactory
 
User = get_user_model()
logger = logging.getLogger(__name__)
 
 
@pytest.mark.django_db
class TestLSOFSMIntegration:
    """
    Test LSO FSM integration with real models.
 
    Focuses on improving coverage of state_manager.py and utils.py
    by testing error paths, cache behavior, and bulk operations.
    """
 
    @pytest.fixture(autouse=True)
    def setup_test_data(self):
        """Set up test data."""
        cache.clear()
        self.org = OrganizationFactory()
        self.user = UserFactory()
        CurrentContext.set_user(self.user)
        CurrentContext.set_organization_id(self.org.id)
        yield
        cache.clear()
        CurrentContext.clear()
 
    def test_project_creation_generates_state(self):
        """
        Test that creating a project automatically generates a state record.
 
        Validates:
        - Project model extends FsmHistoryStateModel
        - Automatic state transition on creation
        - State is CREATED for new projects
        """
        project = ProjectFactory(organization=self.org)
 
        # Check state was created
        state = StateManager.get_current_state_value(project)
        assert state == ProjectStateChoices.CREATED, f'Expected CREATED, got {state}'
 
        # Check state history exists
        history = list(ProjectState.objects.filter(project=project).order_by('created_at'))
        assert len(history) == 1
        assert history[0].state == ProjectStateChoices.CREATED
        assert history[0].transition_name == 'project_created'
 
    def test_task_creation_generates_state(self):
        """
        Test that creating a task automatically generates a state record.
 
        Validates:
        - Task model extends FsmHistoryStateModel
        - Automatic state transition on creation
        - State is CREATED for new tasks
        """
        project = ProjectFactory(organization=self.org)
        task = TaskFactory(project=project)
 
        # Check state was created
        state = StateManager.get_current_state_value(task)
        assert state == TaskStateChoices.CREATED, f'Expected CREATED, got {state}'
 
        # Check state history exists
        history = list(TaskState.objects.filter(task=task).order_by('created_at'))
        assert len(history) == 1
        assert history[0].state == TaskStateChoices.CREATED
 
    def test_annotation_creation_generates_state(self):
        """
        Test that creating an annotation automatically generates a state record.
 
        Validates:
        - Annotation model extends FsmHistoryStateModel
        - Automatic state transition on creation
        - State is SUBMITTED for new annotations in LSO
        """
        project = ProjectFactory(organization=self.org)
        task = TaskFactory(project=project)
        annotation = AnnotationFactory(task=task, completed_by=self.user)
 
        # Check state was created
        state = StateManager.get_current_state_value(annotation)
        assert state == AnnotationStateChoices.SUBMITTED, f'Expected SUBMITTED, got {state}'
 
        # Check state history exists
        history = list(AnnotationState.objects.filter(annotation=annotation).order_by('created_at'))
        assert len(history) >= 1
        assert history[0].state == AnnotationStateChoices.SUBMITTED
 
    def test_cache_functionality(self):
        """
        Test that StateManager caching works correctly.
 
        Validates:
        - State retrieval works consistently
        - Cache doesn't cause incorrect state returns
        - Multiple accesses return same state
        """
        project = ProjectFactory(organization=self.org)
 
        # First access
        cache.clear()
        state1 = StateManager.get_current_state_value(project)
        assert state1 == ProjectStateChoices.CREATED
 
        # Second access - should return same state (whether from cache or DB)
        state2 = StateManager.get_current_state_value(project)
        assert state2 == ProjectStateChoices.CREATED
 
        # States should match
        assert state1 == state2
 
    def test_get_current_state_safe_with_no_state(self):
        """
        Test get_current_state_safe returns None for entities without states.
 
        Validates:
        - Utility function handles entities with no state records
        - No exceptions raised
        - Returns None gracefully
        """
        # Create a project but delete its state records
        project = ProjectFactory(organization=self.org)
        ProjectState.objects.filter(project=project).delete()
        cache.clear()
 
        # Should return None, not raise
        state = get_current_state_safe(project)
        assert state is None
 
    def test_resolve_organization_id_from_entity(self):
        """
        Test resolve_organization_id utility function.
 
        Validates:
        - Extracts organization_id from entity with direct attribute
        - Extracts organization_id from entity with project relation
        - Falls back to CurrentContext
        """
        project = ProjectFactory(organization=self.org)
        task = TaskFactory(project=project)
 
        # Test direct attribute
        org_id = resolve_organization_id(project)
        assert org_id == self.org.id
 
        # Test via project relation
        org_id = resolve_organization_id(task)
        assert org_id == self.org.id
 
    def test_is_fsm_enabled_in_lso(self):
        """
        Test is_fsm_enabled checks feature flag correctly.
 
        Validates:
        - Feature flag check works
        - Returns True when enabled
        """
        result = is_fsm_enabled(user=self.user)
        assert result is True
 
    def test_state_manager_error_handling(self):
        """
        Test StateManager error handling for invalid entities.
 
        Validates:
        - Proper error raised for entities without state model
        - Error includes helpful message
        """
        # Create a mock entity that doesn't have a state model
        from unittest.mock import Mock
 
        mock_entity = Mock()
        mock_entity._meta = Mock()
        mock_entity._meta.model_name = 'nonexistent_model'
        mock_entity._meta.label_lower = 'test.nonexistent'
        mock_entity.pk = 1
 
        # Should raise StateManagerError
        with pytest.raises(StateManagerError) as exc_info:
            StateManager.get_current_state_value(mock_entity)
 
        assert 'No state model found' in str(exc_info.value)
 
    def test_warm_cache_bulk_operation(self):
        """
        Test bulk cache warming for multiple entities.
 
        Validates:
        - Warm cache operation works for multiple entities
        - Subsequent state retrievals are faster (cached)
        - Correct states for all entities
        """
        project = ProjectFactory(organization=self.org)
        tasks = [TaskFactory(project=project) for _ in range(5)]
 
        # Warm cache for all tasks
        cache.clear()
        StateManager.warm_cache(tasks)
 
        # Verify all states can be retrieved and are correct
        for task in tasks:
            state = StateManager.get_current_state_value(task)
            assert state == TaskStateChoices.CREATED
 
    def test_get_state_history(self):
        """
        Test retrieving state history for an entity.
 
        Validates:
        - History retrieval works
        - States are in chronological order
        - All transition metadata captured
        """
        project = ProjectFactory(organization=self.org)
 
        # Get history
        history = StateManager.get_state_history(project)
 
        # Should have at least creation state
        assert len(history) >= 1
        assert history[0].state == ProjectStateChoices.CREATED
        assert history[0].transition_name == 'project_created'
 
    def test_get_state_history_ordering(self):
        """
        Test that state history is returned in chronological order.
 
        Validates:
        - History is ordered by creation time
        - Oldest states appear first
        - All state records are included
        """
        project = ProjectFactory(organization=self.org)
 
        # Get history
        history = StateManager.get_state_history(project)
 
        # Should have at least one state (creation)
        assert len(history) >= 1
 
        # Verify ordering - each timestamp should be >= the previous
        timestamps = [h.created_at for h in history]
        assert timestamps == sorted(timestamps)
 
    def test_annotation_from_draft_workflow(self):
        """
        Test annotation created from draft has correct state.
 
        Validates:
        - Draft-based annotation workflow
        - Correct transition triggered
        - State is SUBMITTED in LSO
        """
        project = ProjectFactory(organization=self.org)
        task = TaskFactory(project=project)
 
        # Create annotation with draft flag
        annotation = Annotation(
            task=task,
            completed_by=self.user,
            result=[{'test': 'data'}],
            was_cancelled=False,
        )
        annotation.save()
 
        # Should be in SUBMITTED state
        state = StateManager.get_current_state_value(annotation)
        assert state == AnnotationStateChoices.SUBMITTED
 
    def test_state_manager_with_multiple_transitions(self):
        """
        Test that multiple state transitions are recorded correctly.
 
        Validates:
        - Multiple transitions create multiple records
        - History ordering is correct
        - Each transition has correct metadata
        """
        project = ProjectFactory(organization=self.org)
 
        # Create multiple state changes by updating project
        StateManager.get_current_state_value(project)
 
        # Update project settings (should create a state record in LSE, but not in LSO)
        project.maximum_annotations = 5
        project.save()
 
        # Get history
        history = list(ProjectState.objects.filter(project=project).order_by('created_at'))
 
        # In LSO, settings changes don't create new state records
        # so we should still have just the creation record
        assert len(history) == 1
        assert history[0].state == ProjectStateChoices.CREATED
 
 
@pytest.mark.django_db
class TestLSOFSMUtilities:
    """
    Test LSO FSM utility functions.
 
    Focuses on improving coverage of utils.py module.
    """
 
    @pytest.fixture(autouse=True)
    def setup_test_data(self):
        """Set up test data."""
        cache.clear()
        self.org = OrganizationFactory()
        self.user = UserFactory()
        CurrentContext.set_user(self.user)
        CurrentContext.set_organization_id(self.org.id)
        yield
        cache.clear()
        CurrentContext.clear()
 
    def test_resolve_organization_id_with_user(self):
        """
        Test resolve_organization_id with user parameter.
 
        Validates:
        - User parameter takes precedence
        - Correct organization extracted
        """
        project = ProjectFactory(organization=self.org)
 
        org_id = resolve_organization_id(project, user=self.user)
        assert org_id is not None
 
    def test_resolve_organization_id_fallback_to_context(self):
        """
        Test resolve_organization_id falls back to CurrentContext.
 
        Validates:
        - CurrentContext used when no other source available
        - Correct ID returned
        """
        CurrentContext.set_organization_id(self.org.id)
 
        # Create entity without organization_id attribute
        from unittest.mock import Mock
 
        mock_entity = Mock(spec=[])  # No attributes
 
        org_id = resolve_organization_id(mock_entity)
        assert org_id == self.org.id
 
    def test_get_current_state_safe_with_state(self):
        """
        Test get_current_state_safe returns correct state value.
 
        Validates:
        - Function returns state string (not None)
        - State value is correct
        - Works correctly with database query
        """
        project = ProjectFactory(organization=self.org)
        cache.clear()
 
        # Should return state value string, not None
        state_value = get_current_state_safe(project)
        assert state_value is not None
        assert state_value == ProjectStateChoices.CREATED
 
    def test_state_manager_handles_concurrent_access(self):
        """
        Test StateManager handles concurrent access correctly.
 
        Validates:
        - No race conditions in state retrieval
        - Cache consistency
        - Multiple simultaneous requests work correctly
        """
        project = ProjectFactory(organization=self.org)
 
        # Simulate multiple concurrent accesses
        states = [StateManager.get_current_state_value(project) for _ in range(10)]
 
        # All should return the same state
        assert all(s == ProjectStateChoices.CREATED for s in states)
 
    def test_get_current_state_value_when_fsm_disabled(self):
        """
        Test get_current_state_value returns None when FSM is disabled.
 
        Validates:
        - Returns None instead of raising when feature flag is off
        - Handles disabled state gracefully
        """
        project = ProjectFactory(organization=self.org)
 
        with patch.object(StateManager, '_is_fsm_enabled', return_value=False):
            result = StateManager.get_current_state_value(project)
            assert result is None
 
    def test_get_states_in_time_range(self):
        """
        Test get_states_in_time_range for time-based queries.
 
        Validates:
        - UUID7-based time range queries work
        - Returns states within specified time range
        """
        from datetime import timedelta
 
        project = ProjectFactory(organization=self.org)
 
        # Get states from the last day
        start_time = datetime.now(timezone.utc) - timedelta(days=1)
        end_time = datetime.now(timezone.utc)
 
        states = StateManager.get_states_in_time_range(project, start_time, end_time)
 
        # Should have at least the creation state
        assert len(states) >= 1
 
    def test_invalidate_cache(self):
        """
        Test cache invalidation for entity state.
 
        Validates:
        - Cache is cleared for specific entity
        - Subsequent lookups hit database
        """
        project = ProjectFactory(organization=self.org)
 
        # Get state to populate cache
        state = StateManager.get_current_state_value(project)
        assert state == ProjectStateChoices.CREATED
 
        # Invalidate cache
        StateManager.invalidate_cache(project)
 
        # Next lookup should work (will hit DB)
        state_after = StateManager.get_current_state_value(project)
        assert state_after == ProjectStateChoices.CREATED
 
    def test_get_current_state_object(self):
        """
        Test get_current_state_object returns full state record.
 
        Validates:
        - Returns BaseState instance with full audit information
        - Contains all expected fields
        """
        project = ProjectFactory(organization=self.org)
 
        # Get current state object
        state_object = StateManager.get_current_state_object(project)
 
        assert state_object is not None
        assert state_object.state == ProjectStateChoices.CREATED
        assert hasattr(state_object, 'triggered_by')
        assert hasattr(state_object, 'transition_name')
 
    def test_transition_state_fsm_disabled(self):
        """
        Test transition_state returns True when FSM is disabled.
 
        Validates:
        - Returns True without creating state record
        - Handles disabled state gracefully
        """
        project = ProjectFactory(organization=self.org)
 
        with patch.object(StateManager, '_is_fsm_enabled', return_value=False):
            result = StateManager.transition_state(
                entity=project, new_state='NEW_STATE', transition_name='test', user=self.user
            )
            assert result is True
 
    def test_warm_cache_multiple_entities(self):
        """
        Test warm_cache with multiple entities for bulk operations.
 
        Validates:
        - Cache is populated for all entities
        - Subsequent get_current_state_value calls are fast (from cache)
        """
        projects = [ProjectFactory(organization=self.org) for _ in range(3)]
 
        # Warm cache for all projects
        StateManager.warm_cache(projects)
 
        # Verify all are cached (should not hit DB)
        for project in projects:
            state = StateManager.get_current_state_value(project)
            assert state == ProjectStateChoices.CREATED
 
    def test_fsm_disabled_via_current_context(self):
        """
        Test CurrentContext.set_fsm_disabled() directly.
 
        Validates:
        - Can disable FSM via CurrentContext
        - is_fsm_enabled() respects the flag
        - State is properly restored
        """
        from core.current_request import CurrentContext
 
        # Initially enabled
        assert is_fsm_enabled() is True
 
        # Disable FSM
        CurrentContext.set_fsm_disabled(True)
        assert is_fsm_enabled() is False
        assert CurrentContext.is_fsm_disabled() is True
 
        # Re-enable FSM
        CurrentContext.set_fsm_disabled(False)
        assert is_fsm_enabled() is True
        assert CurrentContext.is_fsm_disabled() is False