Bin
2025-12-17 05a69820e0c402b0b33c063d3b922f0a0571cbbb
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
"""
Integration tests for the FSM system.
Tests the complete FSM functionality including models, state management,
and API endpoints.
"""
 
from datetime import datetime
from unittest.mock import patch
 
from django.test import TestCase
from fsm.state_manager import get_state_manager
from fsm.state_models import AnnotationState, ProjectState, TaskState
from projects.tests.factories import ProjectFactory
from tasks.tests.factories import AnnotationFactory, TaskFactory
from users.tests.factories import UserFactory
 
 
class TestFSMModels(TestCase):
    """Test FSM model functionality"""
 
    def setUp(self):
        self.user = UserFactory(email='test@example.com')
        self.project = ProjectFactory(created_by=self.user)
        self.task = TaskFactory(project=self.project, data={'text': 'test'})
 
        # Clear cache to ensure tests start with clean state
        from django.core.cache import cache
 
        cache.clear()
 
    def test_task_state_creation(self):
        """Test TaskState creation and basic functionality"""
        task_state = TaskState.objects.create(
            task=self.task,
            project_id=self.task.project_id,  # Denormalized from task.project_id
            state='CREATED',
            triggered_by=self.user,
            reason='Task created for testing',
        )
 
        # Check basic fields
        assert task_state.state == 'CREATED'
        assert task_state.task == self.task
        assert task_state.triggered_by == self.user
 
        # Check UUID7 functionality
        assert task_state.id.version == 7
        assert isinstance(task_state.timestamp_from_uuid, datetime)
 
        # Check string representation
        str_repr = str(task_state)
        assert 'Task' in str_repr
        assert 'CREATED' in str_repr
 
    def test_annotation_state_creation(self):
        """Test AnnotationState creation and basic functionality"""
        annotation = AnnotationFactory(task=self.task, completed_by=self.user, result=[])
 
        annotation_state = AnnotationState.objects.create(
            annotation=annotation,
            task_id=annotation.task.id,  # Denormalized from annotation.task_id
            project_id=annotation.task.project_id,  # Denormalized from annotation.task.project_id
            completed_by_id=annotation.completed_by.id if annotation.completed_by else None,  # Denormalized
            state='DRAFT',
            triggered_by=self.user,
            reason='Annotation draft created',
        )
 
        # Check basic fields
        assert annotation_state.state == 'DRAFT'
        assert annotation_state.annotation == annotation
 
        # Check terminal state property
        assert not annotation_state.is_terminal_state
 
        # Test completed state
        completed_state = AnnotationState.objects.create(
            annotation=annotation,
            task_id=annotation.task.id,
            project_id=annotation.task.project_id,
            completed_by_id=annotation.completed_by.id if annotation.completed_by else None,
            state='COMPLETED',
            triggered_by=self.user,
        )
        assert completed_state.is_terminal_state
 
    def test_project_state_creation(self):
        """Test ProjectState creation and basic functionality"""
        project_state = ProjectState.objects.create(
            project=self.project, state='CREATED', triggered_by=self.user, reason='Project created for testing'
        )
 
        # Check basic fields
        assert project_state.state == 'CREATED'
        assert project_state.project == self.project
 
        # Test terminal state
        assert not project_state.is_terminal_state
 
        completed_state = ProjectState.objects.create(project=self.project, state='COMPLETED', triggered_by=self.user)
        assert completed_state.is_terminal_state
 
 
class TestStateManager(TestCase):
    """Test StateManager functionality with mocked transaction support"""
 
    def setUp(self):
        from core.current_request import CurrentContext
 
        self.user = UserFactory(email='test@example.com')
 
        # Set up CurrentContext BEFORE creating entities that need FSM
        CurrentContext.set_user(self.user)
 
        self.project = ProjectFactory(created_by=self.user)
        if hasattr(self.project, 'organization') and self.project.organization:
            CurrentContext.set_organization_id(self.project.organization.id)
 
        self.task = TaskFactory(project=self.project, data={'text': 'test'})
        self.StateManager = get_state_manager()
 
        # Clear cache to ensure tests start with clean state
        from django.core.cache import cache
 
        cache.clear()
 
        # Ensure registry is properly initialized for TaskState
        from fsm.registry import state_model_registry
        from fsm.state_models import TaskState
 
        if not state_model_registry.get_model('task'):
            state_model_registry.register_model('task', TaskState)
 
    def tearDown(self):
        from core.current_request import CurrentContext
 
        CurrentContext.clear()
 
    def test_get_current_state_empty(self):
        """Test getting current state when task is created"""
        # With FsmHistoryStateModel, tasks automatically get a state on creation
        current_state = self.StateManager.get_current_state_value(self.task)
        assert current_state == 'CREATED'  # FsmHistoryStateModel auto-creates state
 
    @patch('fsm.state_manager.flag_set')
    def test_transition_state(self, mock_flag_set):
        """Test state transition functionality with immediate cache updates"""
        from django.core.cache import cache
 
        cache.clear()
 
        # Enable FSM feature flag
        mock_flag_set.return_value = True
 
        # Initial transition
        success = self.StateManager.transition_state(
            entity=self.task,
            new_state='CREATED',
            user=self.user,
            transition_name='create_task',
            reason='Initial task creation',
        )
 
        assert success
 
        # Check current state - should work with immediate cache update
        current_state = self.StateManager.get_current_state_value(self.task)
        assert current_state == 'CREATED'
 
        # Another transition
        success = self.StateManager.transition_state(
            entity=self.task,
            new_state='IN_PROGRESS',
            user=self.user,
            transition_name='start_work',
            context={'started_by': 'user'},
        )
 
        assert success
 
        current_state = self.StateManager.get_current_state_value(self.task)
        assert current_state == 'IN_PROGRESS'
 
    @patch('fsm.state_manager.flag_set')
    def test_get_current_state_object(self, mock_flag_set):
        """Test getting current state object with full details"""
        from django.core.cache import cache
 
        cache.clear()
 
        # Enable FSM feature flag
        mock_flag_set.return_value = True
 
        # Create some state transitions
        self.StateManager.transition_state(entity=self.task, new_state='CREATED', user=self.user)
        self.StateManager.transition_state(
            entity=self.task, new_state='IN_PROGRESS', user=self.user, context={'test': 'data'}
        )
 
        current_state_obj = self.StateManager.get_current_state_object(self.task)
 
        assert current_state_obj is not None
        assert current_state_obj.state == 'IN_PROGRESS'
        assert current_state_obj.previous_state == 'CREATED'
        assert current_state_obj.triggered_by == self.user
        assert current_state_obj.context_data == {'test': 'data'}
 
    @patch('fsm.state_manager.flag_set')
    def test_get_state_history(self, mock_flag_set):
        """Test state history retrieval"""
        from django.core.cache import cache
 
        cache.clear()
 
        # Enable FSM feature flag
        mock_flag_set.return_value = True
 
        transitions = [('CREATED', 'create_task'), ('IN_PROGRESS', 'start_work'), ('COMPLETED', 'finish_work')]
 
        for state, transition in transitions:
            self.StateManager.transition_state(
                entity=self.task, new_state=state, user=self.user, transition_name=transition
            )
 
        history = self.StateManager.get_state_history(self.task)
 
        # Should have 3 state records
        assert len(history) == 3
 
        # Should be ordered by most recent first (UUID7 ordering)
        states = [h.state for h in history]
        assert states == ['COMPLETED', 'IN_PROGRESS', 'CREATED']
 
        # Check previous states are set correctly
        assert history[2].previous_state is None  # First state has no previous
        assert history[1].previous_state == 'CREATED'
        assert history[0].previous_state == 'IN_PROGRESS'
 
    @patch('fsm.state_manager.flag_set')
    def test_get_states_in_time_range(self, mock_flag_set):
        """Test time-based state queries using StateManager"""
        from django.core.cache import cache
 
        cache.clear()
 
        # Enable FSM feature flag
        mock_flag_set.return_value = True
 
        # Create some states
        self.StateManager.transition_state(entity=self.task, new_state='CREATED', user=self.user)
        self.StateManager.transition_state(entity=self.task, new_state='IN_PROGRESS', user=self.user)
 
        # Get state history (which gives us states ordered by time)
        states = self.StateManager.get_state_history(self.task)
 
        # Should have at least the states we created
        assert len(states) >= 2
 
    @patch('fsm.state_manager.flag_set')
    def test_immediate_cache_update_success_case(self, mock_flag_set):
        """Test that cache is updated immediately on successful transitions"""
        from django.core.cache import cache
 
        cache.clear()
 
        # Enable FSM feature flag
        mock_flag_set.return_value = True
 
        # Perform a successful transition
        success = self.StateManager.transition_state(
            entity=self.task,
            new_state='CREATED',
            user=self.user,
            transition_name='create_task',
            reason='Initial task creation',
        )
 
        # Verify success and immediate cache update
        assert success
        current_state = self.StateManager.get_current_state_value(self.task)
        assert current_state == 'CREATED'
 
        # Perform another successful transition
        success = self.StateManager.transition_state(
            entity=self.task,
            new_state='IN_PROGRESS',
            user=self.user,
            transition_name='start_work',
        )
 
        assert success
        current_state = self.StateManager.get_current_state_value(self.task)
        assert current_state == 'IN_PROGRESS'
 
    @patch('fsm.state_manager.flag_set')
    def test_transaction_on_commit_success_case(self, mock_flag_set):
        """Test successful state transitions"""
        from django.core.cache import cache
 
        cache.clear()
 
        # Enable FSM feature flag
        mock_flag_set.return_value = True
 
        # Perform a successful transition
        success = self.StateManager.transition_state(
            entity=self.task,
            new_state='CREATED',
            user=self.user,
            transition_name='create_task',
        )
 
        # Verify transition succeeded
        assert success
        assert self.StateManager.get_current_state_value(self.task) == 'CREATED'
 
    @patch('fsm.state_manager.flag_set')
    def test_transaction_on_commit_database_failure_case(self, mock_flag_set):
        """Test state transitions work correctly"""
        from django.core.cache import cache
 
        cache.clear()
 
        # Enable FSM feature flag
        mock_flag_set.return_value = True
 
        # Perform a transition
        self.StateManager.transition_state(
            entity=self.task,
            new_state='CREATED',
            user=self.user,
            transition_name='create_task',
        )
 
        # Verify state was set correctly
        current_state = self.StateManager.get_current_state_value(self.task)
        assert current_state == 'CREATED'
 
    @patch('fsm.state_manager.flag_set')
    def test_immediate_cache_update_content(self, mock_flag_set):
        """Test that cache is immediately updated during transition"""
        from django.core.cache import cache
 
        cache.clear()
 
        # Enable FSM feature flag
        mock_flag_set.return_value = True
 
        # Perform a transition
        success = self.StateManager.transition_state(
            entity=self.task,
            new_state='CREATED',
            user=self.user,
        )
 
        assert success
 
        # Cache should be immediately updated during transition
        cache_key = self.StateManager.get_cache_key(self.task)
        cached_state = cache.get(cache_key)
        assert cached_state == 'CREATED'
 
        # Verify get_current_state_value uses the cached value
        current_state = self.StateManager.get_current_state_value(self.task)
        assert current_state == 'CREATED'
 
    @patch('fsm.state_manager.flag_set')
    def test_cache_cleanup_on_transaction_rollback(self, mock_flag_set):
        """Test cache behavior with state transitions"""
        from django.core.cache import cache
 
        cache.clear()
 
        # Enable FSM feature flag
        mock_flag_set.return_value = True
 
        # Create a state transition
        self.StateManager.transition_state(
            entity=self.task,
            new_state='CREATED',
            user=self.user,
            transition_name='create_task',
            reason='Test transition',
        )
 
        # Verify cache contains the state
        cache_key = self.StateManager.get_cache_key(self.task)
        cached_state = cache.get(cache_key)
        assert cached_state == 'CREATED'
 
    @patch('fsm.state_manager.flag_set')
    def test_same_state_transition_prevention(self, mock_flag_set):
        """Test that same-state transitions are prevented"""
        from django.core.cache import cache
 
        cache.clear()
 
        # Enable FSM feature flag
        mock_flag_set.return_value = True
 
        # Create initial state
        success = self.StateManager.transition_state(
            entity=self.task,
            new_state='CREATED',
            user=self.user,
        )
        assert success
 
        # Verify initial state is set
        current_state = self.StateManager.get_current_state_value(self.task)
        assert current_state == 'CREATED'
 
        # Get initial state count
        from fsm.state_models import TaskState
 
        initial_count = TaskState.objects.filter(task=self.task).count()
        assert initial_count == 1
 
        # Attempt same-state transition (should be skipped)
        success = self.StateManager.transition_state(
            entity=self.task,
            new_state='CREATED',  # Same state as current
            user=self.user,
            reason='This should be skipped',
        )
        assert success  # Returns True but doesn't create new record
 
        # Verify no new state record was created
        final_count = TaskState.objects.filter(task=self.task).count()
        assert final_count == initial_count  # Should still be 1
 
        # Verify state is still CREATED
        current_state = self.StateManager.get_current_state_value(self.task)
        assert current_state == 'CREATED'