
    in\              
       ~   d Z ddlZddlZddlZddlmZmZ ddlmZ ddl	m
Z
 ej                  j                  d e ee      j                  j                               ddlmZ dZdded	efd
Zdededede
d	df
dZdededede
d	df
dZ G d d      Z G d d      Z G d d      Z G d d      Z G d d      Zy)u  
test_atomic_timer_write.py

task-timers.json 동시 쓰기 손상 수정(atomic write fix) 회귀 테스트.

배경:
  dispatch.py의 _patch_timer_metadata()와 memory/task-timer.py의 _save_timers()가
  task-timers.json에 동시 쓰기하여 JSON이 깨지는 버그가 발생함.
  수정 후 두 쪽 모두 아래 패턴을 사용한다:
    1. flock(LOCK_EX)로 진입 직렬화
    2. temp 파일 → fsync → os.replace() 원자적 교체
    3. (또는 utils/atomic_write.py의 atomic_json_write() 활용)

테스트는 dispatch.py / task-timer.py를 직접 임포트하지 않고
동일한 패턴을 복제하여 패턴 자체의 올바름을 검증한다.

실패 기준:
  - 원자적 쓰기 없이 open("w") + json.dump 만 쓸 경우 TC1/TC2/TC5는 확률적으로 FAIL.
  - atomic_json_write + flock 사용 시 모두 PASS.

작성자: 아르고스 (Argos) — dev1-team tester
    N)ThreadPoolExecutoras_completed)Path)Any)atomic_json_writez
task-100.1counterreturnc                 ,    t         t         ddddi| ddS )u/   테스트용 task-timers.json 초기 데이터.	dev1-teamrunning2026-04-12T10:00:00task_idteam_idstatus
start_timetasksr   last_updated)SAMPLE_TASK_IDr   s    N/home/jay/workspace/.worktrees/task-2116-dev1/tests/test_atomic_timer_write.py_make_sample_datar   -   s/     )&#3	
 -     
timer_file	lock_filer   metadatac                    |j                   j                  dd       t        |d      5 }t        j                  |t        j
                         	 | j                         s.	 t        j                  |t        j                         ddd       yt        | dd      5 }t        j                  |      }ddd       j                  di       j                  |      }|"|j                  |       d	|d
<   t        | |       t        j                  |t        j                         	 ddd       y# 1 sw Y   |xY w# t        j                  |t        j                         w xY w# 1 sw Y   yxY w)u   dispatch.py _patch_timer_metadata() 의 수정 후 패턴을 복제.

    flock(LOCK_EX) → 읽기 → 패치 → atomic_json_write → flock(LOCK_UN)
    Tparentsexist_okwNrutf-8encodingr   2026-04-12T10:00:01r   )parentmkdiropenfcntlflockLOCK_EXexistsLOCK_UNjsonloadgetupdater   )r   r   r   r   lock_fdfdata
task_entrys           r   _atomic_patch_timerr8   A   s#   
 4$7	i	 0GU]]+	0$$& KK/0 0
 j#8 $Ayy|$'2.227;J%!!(+'<^$!*d3KK/0 0
$ $ KK/0 0sH   %E"D9!$E"D9D-2AD9?$E"-D6	2D99&EE""E+
_lock_filec                    | j                         syt        | dd      5 }t        j                  |      }ddd       j	                  di       j	                  |      }||j                  |       d|d<   t        | dd      5 }t        j                  ||d	d
       ddd       y# 1 sw Y   sxY w# 1 sw Y   yxY w)u   수정 전 패턴: flock 없이 open("w") + json.dump 직접 쓰기.

    이 함수는 TC1/TC2가 원자적 패턴 없이 FAIL함을 보이기 위한 대조군이다.
    실제 프로덕션 코드에서는 절대 사용 금지.
    Nr#   r$   r%   r   r'   r   r"   F   )ensure_asciiindent)r.   r*   r0   r1   r2   r3   dump)r   r9   r   r   r5   r6   r7   s          r   _unsafe_patch_timerr?   W   s     	j#	0 Ayy|'2&**73J(#4^	j#	0 9A		$a89 9 9 9s   B(B4(B14B=c                   $    e Zd ZdZdZdZd Zd Zy)TestConcurrentDispatchPatchu   TC1: 2개 이상의 _patch_timer_metadata 호출이 동시에 실행되어도
    task-timers.json이 유효한 JSON 상태를 유지해야 한다.
      c                     |dz  dz  |dz  dz  j                   j                  dd       t        t                      g }dt        ddf fd	}t         j                  
      5 }t         j                        D cg c]  }|j                  ||       }}t        |      D ]&  }|j                         }||j                  |       ( 	 ddd       |r
J d|        j                  d      }	t        j                  |	      }
d|
v sJ t        |
d   v sJ |
d   t           }|d   t        k(  sJ |d   dk(  sJ yc c}w # 1 sw Y   yxY w)uM   TC1: 10개 스레드 × 5회 = 50번 동시 패치 후 JSON 유효성 확인.memorytask-timers.json.task-timers.lockTr   
thread_idxr	   Nc                 h    t        j                        D ]  }t        t        d|  |        y )Nzbot-)bot	iteration)range
ITERATIONSr8   r   )rH   ir   selfr   s     r   workerzJTestConcurrentDispatchPatch.test_concurrent_dispatch_patch.<locals>.worker   s9    4??+ #"zl+r   max_workers   워커 예외 발생: r$   r%   r   r   r   r   )r(   r)   r   r   intr   CONCURRENCYrL   submitr   	exceptionappend	read_textr0   loadsr   )rO   tmp_patherrorsrP   poolidxfuturesfutexcrawr6   entryr   r   s   `           @@r   test_concurrent_dispatch_patchz:TestConcurrentDispatchPatch.test_concurrent_dispatch_patchy   sy   (+==
x'*==	t< 	*&7&9:	s 	t 	  D,<,<= 	';@AQAQ;RSCt{{63/SGS#G, 'mmo?MM#&'	' <3F8<<z ""G"4zz# $g... Wn-Y>111Y;...' T	' 	's$   -EE"EEEEc                 0    |dz  dz  |dz  dz  j                   j                  dd       t        t                      dt        ddf fd	}t         j                  
      5 }t         j                        D cg c]  }|j                  ||       }}t        |      D ]  }|j                           	 ddd       	 t        j                  j                  d             yc c}w # 1 sw Y   5xY w# t        j                  $ r Y yw xY w)u  대조 실험: atomic write 없이 동시 패치하면 JSON이 깨질 수 있음을 시연.

        이 테스트는 _unsafe_patch_timer를 사용하며,
        손상이 발생하면 json.loads가 JSONDecodeError를 던진다.
        손상이 발생하지 않더라도 pytest.warns 등을 쓰지 않고,
        손상이 '발생할 수 있음'을 문서화하는 smoke test다.

        주의: 파일시스템/OS 버퍼 타이밍에 따라 항상 손상이 발생하진 않으므로
        이 테스트 자체는 손상 여부를 assert하지 않는다.
        대신 atomic 패턴이 정상 동작함을 TC1_main이 보증한다.
        rE   rF   rG   Tr   _r	   Nc                     t        j                        D ]  }	 t        t        |        y # t        $ r Y %w xY w)N)rK   )rL   rM   r?   r   	Exception)rf   rN   r   rO   r   s     r   unsafe_workerzfTestConcurrentDispatchPatch.test_concurrent_dispatch_patch_without_atomic_fails.<locals>.unsafe_worker   sH    4??+ 	'"!&"#		 ! s   1	==rQ   r$   r%   )r(   r)   r   r   rT   r   rU   rL   rV   r   rW   r0   rZ   rY   JSONDecodeError)	rO   r[   ri   r]   r^   r_   r`   r   r   s	   `      @@r   3test_concurrent_dispatch_patch_without_atomic_failszOTestConcurrentDispatchPatch.test_concurrent_dispatch_patch_without_atomic_fails   s
    (+==
x'*==	t<*&7&9:
	S 
	T 
	  D,<,<= 	 BGHXHXBYZ3t{{=#6ZGZ#G,   	 	JJz++W+=> [	  	  ## 		s0   +C3C."C3%C? .C33C<?DDN)__name__
__module____qualname____doc__rU   rM   rd   rk    r   r   rA   rA   r   s    I KJ)/V'r   rA   c                       e Zd ZdZdZd Zy)!TestConcurrentDispatchAndTimerEndut   TC2: dispatch 패치와 task-timer 저장이 동시에 실행되어도
    JSON 무결성이 유지되어야 한다.rB   c                 R   |dz  dz  |dz  dz  j                   j                  dd       t               }t        |       g }dt        ddffd	}d
t        ddffd}g }t        | j                        5 }t        | j                  dz        D ]D  }|j                  |j                  ||             |j                  |j                  ||             F t        |      D ]&  }	|	j                         }
|
|j                  |
       ( 	 ddd       |r
J d|        j                  d      }t        j                  |      }d|v sJ t        |d   v sJ |d   t           }|j!                  d      t        k(  sJ t#        |d   t$              sJ y# 1 sw Y   xY w)uA   TC2: 패치 워커 5개 + 저장 워커 5개가 동시에 실행.rE   rF   rG   Tr   rH   r	   Nc           	      `    t        d      D ]  }t        t        d| dz  dz    d       ! y )NrC   dev      claude-sonnet-4-6)rolemodel)rL   r8   r   )rH   rf   r   r   s     r   patch_workerz^TestConcurrentDispatchAndTimerEnd.test_concurrent_dispatch_and_timer_end.<locals>.patch_worker   s>    1X #"zA~123-r   _thread_idxc                 ,   t        d      D ]  }t        d      5 }t        j                  |t        j                         	 t        dd      5 }t        j                  |      }d d d        d   j                  t        i       }||d<   ||d   t        <   t        |       t        j                  |t        j                         	 d d d         y # 1 sw Y   oxY w# t        j                  |t        j                         w xY w# 1 sw Y   xY w)NrC   r"   r#   r$   r%   r   retry_count)rL   r*   r+   r,   r-   r0   r1   r2   r   r   r/   )r|   rN   lfdr5   r6   rc   r   r   s         r   save_workerz]TestConcurrentDispatchAndTimerEnd.test_concurrent_dispatch_and_timer_end.<locals>.save_worker   s    1X 8 )S) 8SKKU]]3
8!*cGD 0#'99Q<D0 $W 1 1." E/0m,8=Wn5 **d;C78 8	80 0 C78 8s;   %D	C C&>C $$D	CC  &DD		D	rQ   r;   rS   r$   r%   r   r   )r(   r)   r   r   rT   r   rU   rL   rX   rV   r   rW   rY   r0   rZ   r   r2   
isinstancedict)rO   r[   initial_datar\   r{   r   futures_listr]   r^   r`   ra   rb   r6   rc   r   r   s                 @@r   &test_concurrent_dispatch_and_timer_endzHTestConcurrentDispatchAndTimerEnd.test_concurrent_dispatch_and_timer_end   s   (+==
x'*==	t<(**l3	S 	T 		8S 	8T 	8& D,<,<= 	'T--23 C##DKKc$BC##DKKS$ABC $L1 'mmo?MM#&'		' <3F8<<z ""G"4zz# $g...Wn-yy#~555 $w-.../	' 	's   >B F?FF&N)rl   rm   rn   ro   rU   r   rp   r   r   rr   rr      s    1 K@/r   rr   c                   "    e Zd ZdZd Zd Zd Zy)TestDispatchRegressionBasicu?   TC3: _patch_timer_metadata 단순 1회 호출 회귀 테스트.c                    |dz  dz  }|dz  dz  }|j                   j                  dd       t        |t                      t	        ||t
        dddd	d
d	       t        j                  |j                  d            }d|v sJ |d   t
           }|j                  d      dk(  sJ |j                  d      dk(  sJ |j                  d      dk(  sJ |j                  d      d	k(  sJ |j                  d      d
k(  sJ |j                  d      dk(  sJ |j                  d      t
        k(  sJ |j                  d      dk(  sJ |j                  d      dk(  sJ y)u\   TC3: 패치 1회 후 해당 필드가 올바르게 기록되고 JSON이 유효해야 한다.rE   rF   rG   Tr   zbot-bdev1rx   B8C44F05r   r;   )rJ   ry   rz   schedule_idr~   	max_retryr$   r%   r   rJ   ry   rz   r   r~   r   r   r   r   r   r   N)
r(   r)   r   r   r8   r   r0   rZ   rY   r2   )rO   r[   r   r   r6   rc   s         r   test_dispatch_regression_basicz:TestDispatchRegressionBasic.test_dispatch_regression_basic"  sz   (+==
x'*==	t<*&7&9:%"
	
 zz*...@A $Wn- yy7***yy F***yy!%8888yy':555yy'1,,,yy%*** yy#~555yy#{222yy"i///r   c                    |dz  dz  }|dz  dz  }|j                   j                  dd       t        |t                      t	        ||dd       t        j                  |j                  d	
            }t        |d   v sJ d|d   vsJ y)u[   존재하지 않는 task_id에 패치해도 파일이 유효 상태를 유지해야 한다.rE   rF   rG   Tr   ztask-9999.9ghostrJ   r$   r%   r   N)	r(   r)   r   r   r8   r0   rZ   rY   r   )rO   r[   r   r   r6   s        r   &test_patch_nonexistent_task_id_is_noopzBTestDispatchRegressionBasic.test_patch_nonexistent_task_id_is_noopJ  s    (+==
x'*==	t<*&7&9:J	=gN zz*...@Ag...DM111r   c                 P   |dz  dz  }|dz  dz  }|j                   j                  dd       t               }dddd	d
|d   d<   t        ||       t	        ||t
        d       t        j                  |j                  d            }d|d   v sJ d       |d   d   d   dk(  sJ y)u?   패치 시 다른 task 항목이 유실되지 않아야 한다.rE   rF   rG   Tr   
task-200.1	dev2-team	completedz2026-04-12T09:00:00r   r   zbot-ar   r$   r%   u   다른 task 항목이 유실됨r   N)	r(   r)   r   r   r8   r   r0   rZ   rY   )rO   r[   r   r   r6   results         r    test_patch_preserves_other_tasksz<TestDispatchRegressionBasic.test_patch_preserves_other_tasksZ  s    (+==
x'*==	t< "#"!/	'
Wl# 	*d+J	>wOJ00'0BCvg.Q0QQ.g|,Y7;FFFr   N)rl   rm   rn   ro   r   r   r   rp   r   r   r   r     s    I&0P2 Gr   r   c                   (    e Zd ZdZd Zd Zd Zd Zy)TestAtomicWriteFsyncuc   atomic_json_write()가 fsync를 포함하여 쓰기 후 데이터 일관성을 보장함을 검증.c                    |dz  }t        d      }t        ||       |j                         sJ t        |dd      5 }t	        j
                  |      }ddd       |k(  sJ |d   dk(  sJ t        |d	   v sJ y# 1 sw Y   (xY w)
uY   쓰기 완료 후 파일을 다시 읽었을 때 동일 데이터를 반환해야 한다.rF   *   r   r#   r$   r%   Nr   r   )r   r   r.   r*   r0   r1   r   )rO   r[   targetpayloadr5   loadeds         r   test_atomic_write_fsyncz,TestAtomicWriteFsync.test_atomic_write_fsyncw  s    ..#B/&'* }} &#0 	"AYYq\F	"    i B&&&000	" 	"s   A;;Bc                     |dz  }t        |t                      t        |j                  d            }|g k(  s
J d|        y)uG   쓰기 성공 후 임시 파일(.tmp)이 남아있지 않아야 한다.rF   z*.tmpu   임시 파일이 남아있음: N)r   r   listglob)rO   r[   r   	tmp_filess       r   #test_atomic_write_no_temp_file_leftz8TestAtomicWriteFsync.test_atomic_write_no_temp_file_left  sG    ..&"3"56w/0	BM"A) MMr   c                     |dz  }t        |t        d             t        d      }ddddd	|d
   d<   t        ||       t        j                  |j	                  d            }|d   dk(  sJ d|d
   v sJ y)uQ   기존 파일을 덮어쓸 때 새 데이터만 정확히 기록되어야 한다.rF   rw   r   i  r   r   r   z2026-04-12T11:00:00r   r   r$   r%   r   N)r   r   r0   rZ   rY   )rO   r[   r   new_datar   s        r   &test_atomic_write_overwrites_correctlyz;TestAtomicWriteFsync.test_atomic_write_overwrites_correctly  s    .. 	&"3A">? %S1#"/	+
,' 	&(+F,,g,>?i C'''vg...r   c                     |dz  }t        |t        d             |j                         j                  }t        |t        d             |j                         j                  }||k7  sJ d       y)uB  atomic_json_write는 os.replace 기반으로 동작해야 한다 (in-place 쓰기 아님).

        검증 방법: 쓰기 전 원본 파일의 inode를 기록하고,
        쓰기 후 inode가 달라졌는지 확인한다.
        (os.replace는 새 inode를 갖는 파일로 교체하므로 inode가 바뀐다.)
        rF   rw   r   r;   uW   inode가 같음 — os.replace 대신 in-place 쓰기가 사용되고 있을 수 있음N)r   r   statst_ino)rO   r[   r   inode_beforeinode_afters        r   +test_atomic_write_uses_replace_not_in_placez@TestAtomicWriteFsync.test_atomic_write_uses_replace_not_in_place  sk     ..&"3A">?{{}++&"3A">?kkm** {* 	
e	
*r   N)rl   rm   rn   ro   r   r   r   r   rp   r   r   r   r   t  s    m1$N/*
r   r   c                   $    e Zd ZdZdZdZd Zd Zy)TestFlockPreventsRaceuU   flock(LOCK_EX)가 동시 쓰기를 직렬화하여 데이터 손실 없음을 검증.rB      c                     |dz  dz  |dz  dz  j                   j                  dd       t        i ddd       t               }g }d	t        d
df fd}t         j                        D ]7  }t         j                        D ]  }|j                  d|dz  |z    d        9 t         j                        5 }t         j                        D cg c]  }|j                  ||       }	}t        |	      D ]&  }
|
j                         }||j                  |       ( 	 ddd       |r
J d|        j                  d      }t        j                   |      }t        |d   j#                               }||z
  }|r#J dt%        |       dt'        |      dd  d        j                   j                  z  }|d   |k(  sJ d| d|d           yc c}w # 1 sw Y   xY w)u;  10개 스레드가 각각 다른 task_id를 추가할 때 모든 항목이 보존되어야 한다.

        flock 없이 동시에 쓰면 나중에 쓴 스레드가 이전 스레드의 쓰기를 덮어쓰므로
        일부 task_id가 유실된다. flock이 올바르게 동작하면 모두 보존된다.
        rE   rF   rG   Tr   r    r   rH   r	   Nc                 L   t        j                        D ]  }d| dz  |z    d}t        d      5 }t        j                  |t        j
                         	 t        dd      5 }t        j                  |      }d d d        |d| d	z  d
z    ddddd   |<   |dxx   d
z  cc<   t        |       t        j                  |t        j                         	 d d d         y # 1 sw Y   ixY w# t        j                  |t        j                         w xY w# 1 sw Y   xY w)Ntask-d   .1r"   r#   r$   r%   ru   rv   rw   z-teamr   r   r   r   r   )
rL   TASKS_PER_WORKERr*   r+   r,   r-   r0   r1   r   r/   )	rH   rN   r   r   r5   r6   r   rO   r   s	         r   add_tasks_workerzHTestFlockPreventsRace.test_flock_prevents_race.<locals>.add_tasks_worker  s   4001 8!*s"2Q"6!7r:)S) 8SKKU]]38!*cGD 0#'99Q<D0 (/),Z!^a-?,@'F&/*?	2Wg. Y1,)*d;C78 88
0 0 C78 8s;   %DC0&C$<8C04$D$C-)C00&DDD#	r   r   r   rQ   rS   r$   r%   r   u)   flock 미적용 시 손실되는 task_id u   개: rC   z...r   u   counter 불일치: 예상=u	   , 실제=)r(   r)   r   setrT   rL   rU   r   addr   rV   r   rW   rX   rY   r0   rZ   keyslensorted)rO   r[   expected_task_idsr\   r   t_idxrN   r]   r^   r_   r`   ra   rb   
final_dataactual_task_idsmissingtotal_expectedr   r   s   `                @@r   test_flock_prevents_racez.TestFlockPreventsRace.test_flock_prevents_race  s.    (+==
x'*==	t< 	*qRT&UVE	8 	8 	8( 4++, 	CE4001 C!%%eckAo->b&ABC	C  D,<,<= 	'EJ4K[K[E\]ct{{#3S9]G]#G, 'mmo?MM#&'	' <3F8<<z ""G"4ZZ_
 j16689#o5 	
7G~U6RY?[]\]K^J__bc	
{
 ))D,A,AA)$6 	
((8	*YBWAXY	
6+ ^	' 	's$   G"G3"G"G"G""G+c                   	
 |dz  dz  	|dz  dz  	j                   j                  dd       t        	t                      g 
dt        ddf	
fd	}t        | j                  
      5 }t        | j                        D cg c]  }|j                  ||       }}t        |      D ]  }|j                           	 ddd       t        j                  	j                  d            }t        |d   t              sJ d|d   cxk  r| j                  k  sJ  J t        
      | j                  k(  sJ t        t!        
            | j                  k(  sJ d       yc c}w # 1 sw Y   xY w)uh   flock이 쓰기를 직렬화하므로 마지막 writer의 데이터가 완전히 기록되어야 한다.rE   rF   rG   Tr   valuer	   Nc                    t        d      5 }t        j                  |t        j                         	 t        dd      5 }t	        j
                  |      }d d d        | d<   d| d|d<   t        |       j                  |        t        j                  |t        j                         	 d d d        y # 1 sw Y   bxY w# t        j                  |t        j                         w xY w# 1 sw Y   y xY w)	Nr"   r#   r$   r%   r   z2026-04-12T10:00:02dr   )	r*   r+   r,   r-   r0   r1   r   rX   r/   )r   r   r5   r6   r   r   	write_logs       r   sequential_writerzRTestFlockPreventsRace.test_flock_serializes_write_order.<locals>.sequential_writer  s    i% 
4C/4j#@ ,A#yy|,&+DO->uSk+JD(%j$7$$U+KKU]]3
4 
4, , KKU]]3
4 
4s:   %C.CB93C$C.9C	>C&C++C..C7rQ   r$   r%   r   r   u(   flock 미적용 시 중복 쓰기 발생)r(   r)   r   r   rT   r   rU   rL   rV   r   r   r0   rZ   rY   r   r   r   )rO   r[   r   r]   vr_   r`   finalr   r   r   s           @@@r   !test_flock_serializes_write_orderz7TestFlockPreventsRace.test_flock_serializes_write_order  sj   (+==
x'*==	t<*&7&9:		4S 	4T 	4  D,<,<= 	BGHXHXBYZQt{{#4a8ZGZ#G, 

	 

:///AB%	*C000E)$7t'7'777777 9~!1!11113y>"d&6&66b8bb6 [	 	s   -E(E#"E(#E((E1N)rl   rm   rn   ro   rU   r   r   r   rp   r   r   r   r     s    _K@
D#cr   r   )r   )ro   r+   r0   sysconcurrent.futuresr   r   pathlibr   typingr   pathinsertstr__file__r(   utils.atomic_writer   r   rT   r   r   r8   r?   rA   rr   r   r   r   rp   r   r   <module>r      s   .   
 ?  
 3tH~,,334 5 0 s T (0D 0T 0C 0UX 0]a 0,9D 9d 9S 9VY 9^b 96Y Y@F/ F/ZNG NGjC
 C
Tkc kcr   