MCPcopy Create free account
hub / github.com/cft0808/edict / TestConcurrentModifyTask

Class TestConcurrentModifyTask

tests/test_task_mutation_race.py:175–231  ·  view source on GitHub ↗

Simulate the race that existed before the fix: two threads concurrently modifying different fields of the same task.

Source from the content-addressed store, hash-verified

173
174
175class TestConcurrentModifyTask:
176 """Simulate the race that existed before the fix: two threads
177 concurrently modifying different fields of the same task."""
178
179 def test_concurrent_writes_both_persist(self, monkeypatch, tmp_path):
180 """Two threads updating different scheduler fields should both
181 be visible in the final state (no lost updates)."""
182 task = {
183 'id': 'T-RACE', 'title': '竞争测试', 'state': 'Doing',
184 'org': '兵部', 'updatedAt': '2026-04-22T02:00:00Z',
185 '_scheduler': {
186 'enabled': True, 'stallThresholdSec': 600, 'maxRetry': 2,
187 'retryCount': 0, 'escalationLevel': 0, 'autoRollback': True,
188 'lastProgressAt': '2026-04-22T02:00:00Z', 'stallSince': None,
189 'lastDispatchStatus': 'idle', 'rollbackCount': 0,
190 'field_a': 'initial_a', 'field_b': 'initial_b',
191 'snapshot': {'state': 'Assigned', 'org': '尚书省', 'now': '', 'savedAt': '', 'note': 'init'},
192 },
193 }
194 srv, _, tasks_path = _setup_server(monkeypatch, tmp_path, [task])
195 monkeypatch.setattr(srv, '_trigger_refresh', lambda: None)
196
197 barrier = threading.Barrier(2, timeout=5)
198 errors = []
199
200 def update_field_a():
201 try:
202 barrier.wait()
203 srv.modify_task('T-RACE', lambda t: t.setdefault('_scheduler', {}).update({'field_a': 'updated_a'}))
204 except Exception as e:
205 errors.append(e)
206
207 def update_field_b():
208 try:
209 barrier.wait()
210 srv.modify_task('T-RACE', lambda t: t.setdefault('_scheduler', {}).update({'field_b': 'updated_b'}))
211 except Exception as e:
212 errors.append(e)
213
214 t1 = threading.Thread(target=update_field_a)
215 t2 = threading.Thread(target=update_field_b)
216 t1.start()
217 t2.start()
218 t1.join(timeout=10)
219 t2.join(timeout=10)
220
221 assert not errors, f'Thread errors: {errors}'
222
223 data = json.loads(tasks_path.read_text(encoding='utf-8'))
224 sched = data[0].get('_scheduler', {})
225
226 # With atomic modify_task, BOTH updates must be visible.
227 # The old load_tasks/save_tasks pattern would lose one.
228 assert sched['field_a'] == 'updated_a', \
229 f'field_a lost: {sched.get("field_a")}'
230 assert sched['field_b'] == 'updated_b', \
231 f'field_b lost: {sched.get("field_b")}'
232

Callers

nothing calls this directly

Calls

no outgoing calls

Tested by

no test coverage detected