
    i                    t   d Z ddlmZ ddlZddlZddlZddlZddlZddlZddl	m	Z	m
Z
mZ ddlmZ ddlZddlZ ed      Z ed      Z ej$                  d      Zh d	Z G d
 d      Z G d d      Z G d d      Z G d 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_absorption_health_check.py

absorption-health-check 시스템 단위/통합 테스트 (Heimdal, dev2-team)

커버리지:
  1. YAML 파싱 테스트
  2. 헬스체크 로직 단위 테스트 (file_exists, file_recent_activity,
     grep_pattern, audit_trail_recent, process_running)
  3. 출력 포맷 테스트
  4. CLI 필터 테스트 (--source, --status, --summary)
  5. 통합 테스트 (실제 스크립트 실행)
    )annotationsN)datetime	timedeltatimezone)Pathz6/home/jay/workspace/scripts/absorption-health-check.pyz3/home/jay/workspace/config/absorption-registry.yamlu      version: "1.0"
    sources:
      memory:
        description: "Memory & Learning 시스템"
        items:
          - id: "mem-001"
            name: "Memory Indexer"
            priority: high
            source_section: "memory"
            status: active
            health_check:
              type: file_exists
              path: ~/workspace/scripts/memory-indexer.py
          - id: "mem-002"
            name: "Memory Janitor"
            priority: medium
            source_section: "memory"
            status: implemented
            health_check:
              type: file_recent_activity
              path: ~/workspace/logs/memory-janitor.log
              max_age_hours: 48
      security:
        description: "보안 시스템"
        items:
          - id: "sec-001"
            name: "Secret Rotation Check"
            priority: high
            source_section: "security"
            status: recommended
            health_check:
              type: grep_pattern
              pattern: "ROTATION_POLICY"
              target: ~/workspace/scripts/secret-rotation-check.py
          - id: "sec-002"
            name: "Audit Trail"
            priority: medium
            source_section: "security"
            status: degraded
            health_check:
              type: audit_trail_recent
              path: ~/workspace/logs/audit.jsonl
              max_age_hours: 24
      infra:
        description: "인프라 시스템"
        items:
          - id: "inf-001"
            name: "Activity Watcher"
            priority: high
            source_section: "infra"
            status: active
            health_check:
              type: process_running
              process_name: activity-watcher
          - id: "inf-002"
            name: "Duplicate Checker"
            priority: low
            source_section: "infra"
            status: duplicate
            health_check:
              type: file_exists
              path: ~/workspace/scripts/activity-watcher.py
>   activedegraded	duplicateimplementedrecommendedc                      e Zd ZdZ ej
                         dd       Z ej
                         dd       ZddZddZ	ddZ
ddZddZdd	Zh d
ZddZddZddZddZddZddZy)TestYamlParsingu   YAML 파싱 및 구조 검증c                4    t        j                  t              S )u/   샘플 YAML 픽스처를 파싱한 딕셔너리)yaml	safe_loadSAMPLE_REGISTRY_YAMLselfs    S/home/jay/workspace/.worktrees/task-2116-dev1/tests/test_absorption_health_check.pyregistry_datazTestYamlParsing.registry_datav   s     ~~233    c                >    |dz  }|j                  t        d       |S )u%   tmp_path에 샘플 YAML 파일 생성zabsorption-registry.yamlutf-8encoding)
write_textr   )r   tmp_pathps      r   registry_filezTestYamlParsing.registry_file{   s%     11	)G<r   c                R    t        j                  |j                               }|J y)u4   YAML 파일이 오류 없이 로드되어야 한다.N)r   r   	read_text)r   r   datas      r   test_yaml_loads_without_errorz-TestYamlParsing.test_yaml_loads_without_error   s%    ~~m5578r   c                    t         j                         st        j                  d       t	        j
                  t         j                               }|J y)uF   실제 레지스트리가 존재하면 로드할 수 있어야 한다.u(   실제 registry 파일 없음 — 스킵N)REGISTRY_PATHexistspytestskipr   r   r!   )r   r"   s     r   "test_real_registry_loads_if_existsz2TestYamlParsing.test_real_registry_loads_if_exists   s>    ##%KKBC~~m5578r   c                    d|v sJ y)u&   version 필드가 존재해야 한다.versionN r   r   s     r   test_has_version_fieldz&TestYamlParsing.test_has_version_field   s    M)))r   c                :    d|v sJ t        |d   t              sJ y)u,   sources 필드가 딕셔너리여야 한다.sourcesN)
isinstancedictr-   s     r   test_has_sources_dictz%TestYamlParsing.test_has_sources_dict   s%    M)))-	2D999r   c                *    t        |d         dkD  sJ y)u=   sources 에 최소 1개 이상의 소스가 있어야 한다.r0   r   Nlenr-   s     r   test_sources_not_emptyz&TestYamlParsing.test_sources_not_empty   s    =+,q000r   c                    |d   j                         D ]2  \  }}d|v sJ d| d       t        |d   t              r)J d| d        y)u3   각 source 는 items 리스트를 가져야 한다.r0   items'u   ' 에 items 없음u   '.items 가 list 가 아님N)r9   r1   list)r   r   src_namesrcs       r   test_each_source_has_items_listz/TestYamlParsing.test_each_source_has_items_list   sc    *95;;= 	]MHcc>CQxj0B#CC>c'lD1\Qxj@[3\\1	]r   >   idnamestatuspriorityhealth_checksource_sectionc              #  t   K   |d   j                         D ]  }|j                  dg       E d {     y 7 w)Nr0   r9   )valuesget)r   r   r=   s      r   
_all_itemszTestYamlParsing._all_items   s:      +224 	,Cwww+++	,+s   ,868c                    | j                  |      D ]F  }| j                  t        |j                               z
  }|s,J d|j	                  d       d|         y)u5   모든 item 에 필수 필드가 존재해야 한다.item 'r?      ' 에 필드 누락: N)rH   REQUIRED_ITEM_FIELDSsetkeysrG   )r   r   itemmissings       r   test_items_have_required_fieldsz/TestYamlParsing.test_items_have_required_fields   s[    OOM2 	XD//#diik2BBGW&$(88MgY WW;	Xr   c                    | j                  |      D ]-  }|d   t        v rJ d|j                  d       d|d    d        y)u4   status 값이 허용된 값 중 하나여야 한다.rA   rJ   r?   u   ' 의 status '   ' 가 유효하지 않음N)rH   VALID_STATUSESrG   )r   r   rO   s      r   test_status_values_are_validz,TestYamlParsing.test_status_values_are_valid   sT    OOM2 	D>^3 $(tH~6FF_`3	r   c                    | j                  |      D ]2  }|j                  di       }d|v rJ d|j                  d       d        y)u=   health_check 딕셔너리에 type 필드가 있어야 한다.rC   typerJ   r?   u"   ' 의 health_check 에 type 없음N)rH   rG   )r   r   rO   hcs       r   test_health_check_has_typez*TestYamlParsing.test_health_check_has_type   sP    OOM2 	]D."-BR<\6$((4.)99[!\\<	]r   c                    | j                  |      D cg c]  }|d   	 }}t        |      t        t        |            k(  sJ d       yc c}w )u+   모든 item 의 id 가 고유해야 한다.r?   u   중복 id 발견N)rH   r6   rM   )r   r   rO   idss       r   test_id_values_are_uniquez)TestYamlParsing.test_id_values_are_unique   sH    &*oom&DEdtDzEE3x3s3x=(<*<<( Fs   A
c           
         |d   j                         D ];  \  }}|j                  dg       D ]!  }|d   |k(  rJ d|d    d|d    d| d        = y	)
u@   source_section 값이 해당 sources 키와 일치해야 한다.r0   r9   rD   rJ   r?   u   ' 의 source_section 'u   ' 이 source key 'u   ' 와 불일치N)r9   rG   )r   r   r<   r=   rO   s        r   &test_source_section_matches_source_keyz6TestYamlParsing.test_source_section_matches_source_key   s    *95;;= 	MHc, ,-9 T$ZL(>tDT?U>V W##+*O=9	r   Nreturnr2   )r`   r   )r   r   r`   Noner`   ra   )r   r2   r`   ra   )r   r2   )__name__
__module____qualname____doc__r'   fixturer   r   r#   r)   r.   r3   r7   r>   rL   rH   rQ   rU   rY   r\   r^   r,   r   r   r   r   s   s    'V^^4 4 V^^  
 *:
1] b,X]=
r   r   c                  @    e Zd ZdZd	dZd
dZd
dZd
dZd
dZd
dZ	y)TestHealthCheckFileExistszhealth_check type: file_existsc                ~    t         j                  j                  |      }t         j                  j                  |      S )uK   file_exists 체크 로직을 직접 구현하여 테스트 (모듈 독립).)ospath
expanduserr&   )r   rl   expandeds      r   
_run_checkz$TestHealthCheckFileExists._run_check   s)    77%%d+ww~~h''r   c                l    |dz  }|j                  d       | j                  t        |            du sJ y)u#   존재하는 파일 → pass (True)z
target.txtr"   TNr   ro   strr   r   fs      r   test_existing_file_passesz3TestHealthCheckFileExists.test_existing_file_passes   s3    |#	Vs1v&$...r   c                J    |dz  }| j                  t        |            du sJ y)u+   존재하지 않는 파일 → fail (False)zno_such_file.txtFNro   rr   )r   r   rP   s      r   test_nonexistent_file_failsz5TestHealthCheckFileExists.test_nonexistent_file_fails   s(    //s7|,555r   c                    |j                  dt        |             |dz  }|j                  d       | j                  d      du sJ y)u0   ~ 경로가 올바르게 확장되어야 한다.HOMEzsentinel.txtokz~/sentinel.txtTN)setenvrr   r   ro   )r   r   monkeypatchtargets       r   test_tilde_path_expansionz3TestHealthCheckFileExists.test_tilde_path_expansion   sF    63x=1N*$/0D888r   c                d    |j                  dt        |             | j                  d      du sJ y)u)   ~ 경로 확장 후 파일 없으면 failrz   z~/nonexistent_file.txtFN)r|   rr   ro   )r   r   r}   s      r   test_tilde_path_nonexistentz5TestHealthCheckFileExists.test_tilde_path_nonexistent   s.    63x=178EAAAr   c                @    | j                  t        |            du sJ y)u(   디렉토리 경로도 존재하면 passTNrw   r   r   s     r   test_existing_directory_passesz8TestHealthCheckFileExists.test_existing_directory_passes   s    s8}-555r   N)rl   rr   r`   boolr   r   r`   ra   )
rc   rd   re   rf   ro   ru   rx   r   r   r   r,   r   r   ri   ri      s$    ((
/6
9B
6r   ri   c                  B    e Zd ZdZd	d
dZddZddZddZddZddZ	y)!TestHealthCheckFileRecentActivityz'health_check type: file_recent_activityc                    t         j                  j                  |      }t         j                  j                  |      syt         j                  j	                  |      }t        j
                         |z
  dz  }||k  S )u   file_recent_activity 로직.F  )rk   rl   rm   r&   getmtimetime)r   rl   max_age_hoursrn   mtime	age_hourss         r   ro   z,TestHealthCheckFileRecentActivity._run_check   s]    77%%d+ww~~h'  *YY[5(D0	M))r   c                p    |dz  }|j                  d       | j                  t        |      d      du sJ y)u    방금 수정된 파일 → passz
recent.logz	log entry   r   TNrq   rs   s      r   "test_recently_modified_file_passeszDTestHealthCheckFileRecentActivity.test_recently_modified_file_passes  s7    |#	[!s1vQ74???r   c                    |dz  }|j                  d       t        j                         dz
  }t        j                  t	        |      ||f       | j                  t	        |      d      du sJ y)u   오래된 파일 → failzold.logz	old entryi    r   FNr   r   rk   utimerr   ro   )r   r   rt   old_times       r   test_old_file_failsz5TestHealthCheckFileRecentActivity.test_old_file_fails  s^    y 	[!99;*
Q(H-.s1vR8EAAAr   c                F    | j                  t        |dz              du sJ y)   파일 없음 → failzmissing.logFNrw   r   s     r   rx   z=TestHealthCheckFileRecentActivity.test_nonexistent_file_fails  s#    s8m#;<=FFFr   c                    |dz  }|j                  d       t        j                         dz
  }t        j                  t	        |      ||f       | j                  t	        |      d      du sJ | j                  t	        |      d      du sJ y	)
u5   max_age_hours 파라미터가 적용되어야 한다.z
medium.logr"   i 0   r   Tr   FNr   r   r   rt   pasts       r   test_custom_max_age_hoursz;TestHealthCheckFileRecentActivity.test_custom_max_age_hours  s|    |#	Vyy{Y&
Q$&s1vR8D@@@s1vR8EAAAr   c                    |dz  }|j                  d       t        j                         dz
  dz   }t        j                  t	        |      ||f       | j                  t	        |      d      du sJ y)	u1   정확히 경계(max_age_hours)에서 pass 처리zboundary.logr"   r   
   r   r   TNr   r   s       r   test_edge_age_at_boundaryz;TestHealthCheckFileRecentActivity.test_edge_age_at_boundary%  sb    ~%	Vyy{T!B&
Q$&s1vQ74???r   Nr   rl   rr   r   intr`   r   r   )
rc   rd   re   rf   ro   r   r   rx   r   r   r,   r   r   r   r      s(    1*@BG	B@r   r   c                  H    e Zd ZdZd
dZddZddZddZddZddZ	ddZ
y	)TestHealthCheckGrepPatternzhealth_check type: grep_patternc                v   ddl }t        j                  j                  |      }t        j                  j	                  |      rwt        j
                  |      D ]^  \  }}}|D ]S  }t        j                  j                  ||      }		 t        |	      j                  d      }
|j                  ||
      r  yU ` yt        j                  j                  |      sy	 t        |      j                  d      }
t        |j                  ||
            S # t        t        f$ r Y w xY w# t        t        f$ r Y yw xY w)u   grep_pattern 로직.r   NignoreerrorsTF)rerk   rl   rm   isdirwalkjoinr   r!   searchIOErrorOSErrorr&   r   )r   patternr~   r   rn   root_filesfnamefpathcontents              r   ro   z%TestHealthCheckGrepPattern._run_check2  s   77%%f-77==""$''("3 !a" !EGGLLu5E!"&u+"7"7x"7"H99Wg6#' 7	!! 77>>(+x.22(2CBIIgw788 $W- ! ! W% s$   -D5D& D#"D#&D87D8c                n    |dz  }|j                  d       | j                  dt        |            du sJ y)u"   파일에 패턴이 있으면 pass	config.pyzROTATION_POLICY = 90
ROTATION_POLICYTNrq   rs   s      r   $test_matching_pattern_in_file_passesz?TestHealthCheckGrepPattern.test_matching_pattern_in_file_passesK  s7    {"	-.0#a&9TAAAr   c                n    |dz  }|j                  d       | j                  dt        |            du sJ y)u   패턴이 없으면 failr   zSOME_OTHER_SETTING = 10
r   FNrq   rs   s      r   test_nonmatching_pattern_failsz9TestHealthCheckGrepPattern.test_nonmatching_pattern_failsQ  s7    {"	010#a&9UBBBr   c                    |dz  j                  d       |dz  j                  d       | j                  dt        |            du sJ y)u0   디렉토리 내 파일에 패턴 있으면 passza.pyzTARGET_KEY = True
zb.pyzOTHER = False

TARGET_KEYTNrq   r   s     r    test_pattern_in_directory_passesz;TestHealthCheckGrepPattern.test_pattern_in_directory_passesW  sG    	F	&&'<=	F	&&'89|S];tCCCr   c                    |dz  j                  d       |dz  j                  d       | j                  dt        |            du sJ y)u:   디렉토리 내 어떤 파일에도 패턴 없으면 failzx.pyzFOO = 1
zy.pyzBAR = 2
MISSING_PATTERN_XYZFNrq   r   s     r   #test_pattern_not_in_directory_failsz>TestHealthCheckGrepPattern.test_pattern_not_in_directory_fails]  sF    	F	&&{3	F	&&{34c(mDMMMr   c                n    |dz  }|j                  d       | j                  dt        |            du sJ y)u(   정규식 패턴이 동작해야 한다.zdata.txtzerror_code=404
zerror_code=\d+TNrq   rs   s      r   test_regex_pattern_worksz3TestHealthCheckGrepPattern.test_regex_pattern_worksc  s7    z!	'(0#a&9TAAAr   c                H    | j                  dt        |dz              du sJ y)u   파일 없으면 failr   zno_file.txtFNrw   r   s     r   rx   z6TestHealthCheckGrepPattern.test_nonexistent_file_failsi  s%    y#h.F*GHEQQQr   N)r   rr   r~   rr   r`   r   r   )rc   rd   re   rf   ro   r   r   r   r   r   rx   r,   r   r   r   r   /  s.    )2BCDNBRr   r   c                  R    e Zd ZdZdddZddZddZddZddZddZ	ddZ
dd	Zy
)TestHealthCheckAuditTrailRecentz%health_check type: audit_trail_recentc                   t         j                  j                  |      }t         j                  j                  |      syt	        |      j                  d      j                         j                         }|syt        j                  t        j                        j                  d      t        |      z
  }t        |      D ]  }|j                         }|s	 t        j                   |      }|j#                  d      xs$ |j#                  d      xs |j#                  d	      }|r;t        j$                  |j                  d
d      j'                  d            }	|	|k\  r y y# t        j(                  t*        f$ r Y w xY w)uC   audit_trail_recent 로직: JSONL 파일에서 최근 항목 확인.Fr   r   Ntzinfohours	timestamptsr   Zz+00:00T)rk   rl   rm   r&   r   r!   strip
splitlinesr   nowr   utcreplacer   reversedjsonloadsrG   fromisoformatrstripJSONDecodeError
ValueError)
r   rl   r   rn   linescutofflineentryts_strr   s
             r   ro   z*TestHealthCheckAuditTrailRecent._run_checkq  s=   77%%d+ww~~h'X(((9??ALLNhll+3343@9S`CaaUO 	D::<D

4(;/W599T?WeiiPVFW!//sH0M0T0TU]0^_BV|#	  ((*5 s   BE((FFc                V    |j                  dj                  d |D              d       y )N
c              3  F   K   | ]  }t        j                  |        y wN)r   dumps).0es     r   	<genexpr>z>TestHealthCheckAuditTrailRecent._make_jsonl.<locals>.<genexpr>  s     !AA$**Q-!As   !r   r   )r   r   )r   rl   entriess      r   _make_jsonlz+TestHealthCheckAuditTrailRecent._make_jsonl  s!    		!A!AAGTr   c                    t        j                  t        j                        j	                  d      j                         }|dz  }| j                  ||ddg       | j                  t        |      d      du sJ y)	u   최근 항목이 있으면 passNr   audit.jsonlloginr   eventr   r   T)	r   r   r   r   r   	isoformatr   ro   rr   )r   r   now_strrt   s       r   test_recent_entry_passesz8TestHealthCheckAuditTrailRecent.test_recent_entry_passes  sl    ,,x||,44D4AKKM}$7WEFGs1vR8D@@@r   c                   t        j                  t        j                        j	                  d      t        d      z
  j                         }|dz  }| j                  ||ddg       | j                  t        |      d	      d
u sJ y)u"   오래된 항목만 있으면 failNr   H   r   r   r   r   r   r   F
r   r   r   r   r   r   r   r   ro   rr   )r   r   old_strrt   s       r   test_old_entries_failz5TestHealthCheckAuditTrailRecent.test_old_entries_fail  sw    <<-55T5BYUWEXXcce}$7WEFGs1vR8EAAAr   c                l    |dz  }|j                  d       | j                  t        |            du sJ y)u   빈 파일 → failzempty.jsonl FNrq   rs   s      r   test_empty_file_failsz5TestHealthCheckAuditTrailRecent.test_empty_file_fails  s3    }$	Rs1v&%///r   c                F    | j                  t        |dz              du sJ y)r   zmissing.jsonlFNrw   r   s     r   rx   z;TestHealthCheckAuditTrailRecent.test_nonexistent_file_fails  s#    s8o#=>?5HHHr   c                   t        j                  t        j                        j	                  d      t        d      z
  j                         }t        j                  t        j                        j	                  d      j                         }|dz  }| j                  ||dd|ddg       | j                  t        |      d	
      du sJ y)u0   오래된 항목 + 최신 항목 혼재 → passNr   r   r   zmixed.jsonloldr   newr   r   Tr   )r   r   r   r   rt   s        r   *test_mixed_entries_passes_if_recent_existszJTestHealthCheckAuditTrailRecent.test_mixed_entries_passes_if_recent_exists  s    <<-55T5BYUWEXXcce,,x||,44D4AKKM}$%6%6	
 s1vR8D@@@r   c                V   t        j                  t        j                        j	                  d      t        d      z
  j                         }|dz  }| j                  ||ddg       | j                  t        |      d	      d
u sJ | j                  t        |      d	      du sJ y)u(   max_age_hours 파라미터 적용 확인Nr   $   r   r   evtr   r   r   Tr   Fr   )r   r   
entry_timert   s       r   r   z9TestHealthCheckAuditTrailRecent.test_custom_max_age_hours  s    ll8<<0888E	XZH[[ffh
}$:FGHs1vR8D@@@s1vR8EAAAr   Nr   r   )rl   r   r   
list[dict]r`   ra   r   )rc   rd   re   rf   ro   r   r   r   r   rx   r  r   r,   r   r   r   r   n  s3    /0UAB0IABr   r   c                  0    e Zd ZdZddZddZddZddZy)	TestHealthCheckProcessRunningz"health_check type: process_runningc                    	 t        j                  dd|gdd      }|j                  dk(  S # t        $ r+ t        j                  ddgdd      }||j                  v cY S w xY w)u7   process_running 로직: ps aux 로 프로세스 확인.pgrepz-fT)capture_outputtextr   psaux)
subprocessrun
returncodeFileNotFoundErrorstdout)r   process_nameresults      r   ro   z(TestHealthCheckProcessRunning._run_check  ss    	1^^$-#F
 $$))  	1^^u#F
  6==00	1s   ), 1A A c                V    | j                  d      du s| j                  d      du sJ yy)u*   현재 실행 중인 프로세스 → passpythonTpython3Nro   r   s    r   test_running_process_passesz9TestHealthCheckProcessRunning.test_running_process_passes  s2     x(D0DOOI4NRV4VVV4V0r   c                .    | j                  d      du sJ y)u)   존재하지 않는 프로세스 → fail!nonexistent_process_xyz_12345_abcFNr  r   s    r   test_nonrunning_process_failsz;TestHealthCheckProcessRunning.test_nonrunning_process_fails  s    BCuLLLr   c                .    | j                  d      du sJ y)u@   프로세스명 서브스트링으로도 매칭되어야 한다.r'   TNr  r   s    r   !test_process_name_substring_matchz?TestHealthCheckProcessRunning.test_process_name_substring_match  s     x(D000r   N)r  rr   r`   r   rb   )rc   rd   re   rf   ro   r  r  r!  r,   r   r   r
  r
    s    ,1$W
M1r   r
  c                      e Zd ZdZ ej
                         dd       ZddZddZddZ	ddZ
ddZddZdd	Zdd
ZddZddZddZddZddZddZddZy)TestOutputFormatu=   출력 JSON 구조 검증 (실제 스크립트 포맷 기준)c                4   t        j                  t        j                        j	                         dddddddddddddddddddddddddddddddgd	d
gdddddddddddddddddddddddddddddddddd d!dd"d#d$ddddd%ddd&d$d'd(d(dd)dgd*S )+uH   실제 스크립트가 생성하는 출력 포맷과 일치하는 샘플      r   )totalr   r   r   r	   r
   r   )memorysecurityinfrazinf-002zinf-003u   중복 활동 감지기)r9   descriptionmem-001zMemory Indexerr(  highr   passz5exists: /home/jay/workspace/scripts/memory-indexer.pyr?   r@   sourcerB   declared_statusrA   health_check_resulthealth_check_detailmem-002zMemory Janitormediumr   z"last modified 2.0h ago (limit 48h)sec-001zSecret Rotation Checkr)  r   z*pattern found in: secret-rotation-check.pysec-002zAudit Trailr	   failzno entries within last 24hzinf-001zActivity Watcherr*  z.process 'activity-watcher' found (1 match(es))zDuplicate Checkerlowr
   z7exists: /home/jay/workspace/scripts/activity-watcher.py)r   summary	by_source
duplicatesr9   )r   r   r   r   r   r   s    r   sample_outputzTestOutputFormat.sample_output  sf    "hll3==?   #$#$ !!" #$#$ !!" #$#$ !!"#6 %i0AZ[
 $,& &'/&+1+b	 $,& ('4++1+O	 $3( &'4++1+W	 $)( ('/(+1+G	 $.% &'/&+1+[	 $/% %'2)+1+d	g=Oe
 e	
r   c                H    h d}|t        |j                               k  sJ y)u3   출력에 필수 최상위 키가 있어야 한다.>   r9   r:  r;  r   r<  N)rM   rN   )r   r=  requireds      r   !test_output_has_required_top_keysz2TestOutputFormat.test_output_has_required_top_keysX  s#    O3}1134444r   c                \    |d   }t        j                  |      }t        |t               sJ y)u8   timestamp 가 유효한 ISO 8601 형식이어야 한다.r   N)r   r   r1   )r   r=  r   parseds       r   test_timestamp_is_valid_isoz,TestOutputFormat.test_timestamp_is_valid_iso]  s,    ;'''+&(+++r   c                    d|d   v sJ y)u-   summary 에 total 필드가 있어야 한다.r'  r:  Nr,   r   r=  s     r   test_summary_has_total_fieldz-TestOutputFormat.test_summary_has_total_fieldc  s    -	2222r   c                :    |d   }dD ]  }||v rJ d| d        y)u<   summary 에 모든 status 카운트 키가 있어야 한다.r:  r   r   r   r	   r
   u   summary 에 'u   ' 키 없음Nr,   )r   r=  srA   s       r   "test_summary_has_all_status_fieldsz3TestOutputFormat.test_summary_has_all_status_fieldsg  s7    )$W 	EFQ;D-x| DD;	Er   c                j    |d   t        fddD              }|d   k(  sJ d| dd    d       y)	u?   summary 내 status 카운트 합이 total 과 같아야 한다.r:  c              3  (   K   | ]	  }|     y wr   r,   )r   krI  s     r   r   zKTestOutputFormat.test_summary_status_counts_sum_to_total.<locals>.<genexpr>p  s     i!1is   rH  r'     status 합계() != total()N)sum)r   r=  
status_sumrI  s      @r   'test_summary_status_counts_sum_to_totalz8TestOutputFormat.test_summary_status_counts_sum_to_totalm  sM    )$i'hii
QwZ' 	
ZLAgJ<qA	
'r   c                6    |d   d   t        |d         k(  sJ y)u=   summary.total 이 items 리스트 길이와 같아야 한다.r:  r'  r9   Nr5   rE  s     r   %test_summary_total_matches_items_listz6TestOutputFormat.test_summary_total_matches_items_listu  s$    Y'0Cg8N4OOOOr   c                    t        j                  t              }t        |d   j	                               }|d   D ]  }||v rJ d|         y)u8   by_source 키가 YAML 소스 키와 일치해야 한다.r0   r;  u   알 수 없는 source: N)r   r   r   rM   rN   )r   r=  registrysource_keyssrc_keys        r   "test_by_source_grouping_is_correctz3TestOutputFormat.test_by_source_grouping_is_correcty  sZ    >>"67(9-2245$[1 	OGk)N-DWI+NN)	Or   c                    |d   j                         D ]/  \  }}t        |t              sJ d| d       d|v r&J d| d        y)u=   by_source 내 각 값은 카운트 딕셔너리여야 한다.r;  r:   u   ' 의 값이 dict 가 아님r'  u   ' 카운트에 total 없음N)r9   r1   r2   )r   r=  r=   countss       r   'test_by_source_each_entry_is_count_dictz8TestOutputFormat.test_by_source_each_entry_is_count_dict  sa    (5;;= 	KKCfd+Rq5Q-RR+f$J#.I&JJ$	Kr   c                d    t        d |d   j                         D              }||d   d   k(  sJ y)u>   by_source 의 total 합이 summary.total 과 같아야 한다.c              3  &   K   | ]	  }|d      ywr'  Nr,   )r   vs     r   r   zNTestOutputFormat.test_by_source_totals_sum_to_summary_total.<locals>.<genexpr>  s     S!1W:Ss   r;  r:  r'  N)rQ  rF   )r   r=  source_totals      r   *test_by_source_totals_sum_to_summary_totalz;TestOutputFormat.test_by_source_totals_sum_to_summary_total  s8    S}[/I/P/P/RSS}Y7@@@@r   c                T    |d   D ]   }d|v rJ d|j                  d       d        y)uS   items 리스트의 모든 항목에 health_check_result 필드가 있어야 한다.r9   r2  rJ   r?       ' 에 health_check_result 없음NrG   )r   r=  rO   s      r   &test_each_item_has_health_check_resultz7TestOutputFormat.test_each_item_has_health_check_result  sA    !'* 	D(D0 $((HI0	r   c                n    h d}|d   D ])  }|d   |v rJ d|j                  d       d|d    d        y)	B   health_check_result 값이 pass/fail/skip 중 하나여야 한다.>   r8  r.  r(   r9   r2  rJ   r?      ' 의 result 'rS   Nrf  )r   r=  validrO   s       r   )test_health_check_result_values_are_validz:TestOutputFormat.test_health_check_result_values_are_valid  sY    (!'* 	D-.%7 $(t<Q7R6SSlm7	r   c                    h d}|d   D ]<  }|t        |j                               z
  }|s"J d|j                  d       d|         y)u:   items 의 각 항목에 필수 필드가 있어야 한다.>   r?   r@   r0  rA   rB   r1  r3  r2  r9   rJ   r?   rK   N)rM   rN   rG   )r   r=  r?  rO   rP   s        r   rQ   z0TestOutputFormat.test_items_have_required_fields  sY    B!'* 	XDTYY[!11GW&$(88MgY WW;	Xr   c                X    t        |d   t              sJ |d   D ]  }d|v rJ d        y)uN   duplicates 는 리스트이고 각 항목에 items 필드가 있어야 한다.r<  r9   u)   duplicate 항목에 'items' 필드 없음N)r1   r;   )r   r=  dups      r   test_duplicate_detectionz)TestOutputFormat.test_duplicate_detection  s?    -5t<<< . 	OCc>N#NN>	Or   c                    |d   D cg c]  }|d   dk(  r
|d   dk(  r| }}|D ]  }|d   dk(  rJ d|d	    d
|d    d        yc c}w )uY   active 항목이 health check fail 이면 status 가 degraded 로 변경되어야 한다.r9   r1  r   r2  r8  rA   r	   rJ   r?   u!   ': active+fail 이지만 status='r:   Nr,   )r   r=  idegraded_itemsrO   s        r   !test_active_fail_becomes_degradedz2TestOutputFormat.test_active_fail_becomes_degraded  s     %W-
"#x/A6K4LPV4V 
 
 # 	D>Z/ d$Ed8nEUUVW/		
s   Ac                ~    t        j                  |      }t        j                  |      }|d   d   |d   d   k(  sJ y)u4   출력 전체가 JSON 직렬화 가능해야 한다.r:  r'  N)r   r   r   )r   r=  
serializedparsed_backs       r    test_output_is_json_serializablez1TestOutputFormat.test_output_is_json_serializable  s@    ZZ.
jj,9%g.-	2J72SSSSr   Nr_   )r=  r2   r`   ra   )rc   rd   re   rf   r'   rg   r=  r@  rC  rF  rJ  rS  rU  rZ  r]  rc  rg  rl  rQ   rp  rt  rx  r,   r   r   r#  r#    sv    GV^^g
 g
R5
,3E
POKA
XO	Tr   r#  c                      e Zd ZdZ ej
                         dd       ZddZddZddZ	ddZ
ddZddZdd	Zdd
ZddZddZddZddZddZddZy)TestCliFiltersu;   CLI 필터 옵션 테스트 (--source, --status, --summary)c                V    dddddddddd	d
dddddddddddddddddddddddddgS )u9   전체 items 픽스처 (실제 스크립트 items 포맷)r,  M1r(  r-  r   r.  r{   r/  r4  M2r5  r   r6  S1r)  r7  S2r   r8  z	not foundr,   r   s    r   
full_itemszTestCliFilters.full_items  sx     dhH$*4I dh!m}$*4I djH$*4I dj!m}$*;P
 	
r   c                >    |D cg c]  }|d   |k(  s| c}S c c}w )u&   --source 필터 로직 시뮬레이션r0  r,   )r   r9   r0  rr  s       r   _filter_by_sourcez TestCliFilters._filter_by_source  !     :aAhK6$9:::   c                >    |D cg c]  }|d   |k(  s| c}S c c}w )u&   --status 필터 로직 시뮬레이션rA   r,   )r   r9   rA   rr  s       r   _filter_by_statusz TestCliFilters._filter_by_status  r  r  c                    d}i }|D ]T  }|d   }||vr|D ci c]  }|d c}||<   d||   d<   ||   dxx   dz  cc<   |d   }|||   v sE||   |xx   dz  cc<   V |S c c}w )u:   items 리스트로 by_source 카운트 딕셔너리 구성rH  r0  r   r'  r   rA   r,   )r   r9   STATUS_KEYSr  rO   r=   rM  sts           r   _build_by_sourcezTestCliFilters._build_by_source  s    W"$ 	%Dx.C& -89q!t9s'(sG$3K A% hBVC[ sB1$	%  :s   
A c                J    | j                  |d      }|D ]  }|d   dk(  rJ  y)u+   --source memory → memory 항목만 반환r(  r0  Nr  r   r  r  rO   s       r   (test_source_filter_returns_only_matchingz7TestCliFilters.test_source_filter_returns_only_matching  5    ''
H= 	.D>X---	.r   c                P    | j                  |d      }t        d |D              sJ y)u<   --source memory 필터 후 security 항목 없어야 한다.r(  c              3  ,   K   | ]  }|d    dk7    yw)r0  r)  Nr,   r   rr  s     r   r   zKTestCliFilters.test_source_filter_excludes_other_sources.<locals>.<genexpr>  s     =1X;*,=s   N)r  allr   r  r  s      r   )test_source_filter_excludes_other_sourcesz8TestCliFilters.test_source_filter_excludes_other_sources  s(    ''
H==f====r   c                l    | j                  |d      }t        d |D              }t        |      |k(  sJ y)u2   --source 필터 후 count 가 정확해야 한다.r)  c              3  2   K   | ]  }|d    dk(  sd  yw)r0  r)  r   Nr,   r  s     r   r   zBTestCliFilters.test_source_filter_correct_count.<locals>.<genexpr>  s     JQ(z0IqJ   N)r  rQ  r6   r   r  r  expecteds       r    test_source_filter_correct_countz/TestCliFilters.test_source_filter_correct_count  s6    ''
J?J*JJ6{h&&&r   c                6    | j                  |d      }|g k(  sJ y)u3   존재하지 않는 source 필터 → 빈 리스트nonexistent_sourceNr  r  s      r   ,test_source_filter_nonexistent_returns_emptyz;TestCliFilters.test_source_filter_nonexistent_returns_empty  !    ''
4HI||r   c                    | j                  |d      }| j                  |      }d|v sJ d|vsJ |d   d   t        |      k(  sJ y)u;   필터된 items 로 by_source 구성이 올바른지 확인r(  r)  r'  N)r  r  r6   )r   r  filteredby_srcs       r   "test_by_source_build_from_filteredz1TestCliFilters.test_by_source_build_from_filtered  s\    ))*h?&&x06!!!'''h(CM999r   c                J    | j                  |d      }|D ]  }|d   dk(  rJ  y)u+   --status active → active 항목만 반환r   rA   Nr  r  s       r   (test_status_filter_returns_only_matchingz7TestCliFilters.test_status_filter_returns_only_matching  r  r   c                l    | j                  |d      }t        d |D              }t        |      |k(  sJ y)u$   --status 필터 후 카운트 검증r   c              3  2   K   | ]  }|d    dk(  sd  yw)rA   r   r   Nr,   r  s     r   r   zBTestCliFilters.test_status_filter_correct_count.<locals>.<genexpr>  s     HQ(x0GqHr  N)r  rQ  r6   r  s       r    test_status_filter_correct_countz/TestCliFilters.test_status_filter_correct_count  s6    ''
H=H*HH6{h&&&r   c                J    | j                  |d      }|D ]  }|d   dk(  rJ  y)u.   --status recommended → recommended 항목만r   rA   Nr  r  s       r   test_status_filter_recommendedz-TestCliFilters.test_status_filter_recommended  s5    ''
MB 	3D>]222	3r   c                6    | j                  |d      }|g k(  sJ y)u0   존재하지 않는 status 필터 → 빈 결과nonexistent_statusNr  r  s      r   ,test_status_filter_nonexistent_returns_emptyz;TestCliFilters.test_status_filter_nonexistent_returns_empty  r  r   c                    t        j                  t        j                        j	                         dt        |      i| j                  |      g d}d|vsJ y)u<   --summary 출력에 items 상세 목록이 없어야 한다.r'  )r   r:  r;  r<  r9   N)r   r   r   r   r   r6   r  )r   r  summary_onlys      r   'test_summary_output_excludes_items_listz6TestCliFilters.test_summary_output_excludes_items_list&  sQ     "hll3==?Z1..z:	
 l***r   c                P    dt        |      i}d|v sJ |d   t        |      k(  sJ y)u   summary 에 total 포함r'  Nr5   )r   r  r:  s      r   test_summary_has_totalsz&TestCliFilters.test_summary_has_totals1  s5    C
O,'!!!w3z?222r   N)r`   r  )r9   r  r0  rr   r`   r  )r9   r  rA   rr   r`   r  )r9   r  r`   r2   )r  r  r`   ra   )rc   rd   re   rf   r'   rg   r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r,   r   r   rz  rz    sf    EV^^
 
";;".>
'
:.'3	+3r   rz  c                      e Zd ZdZ ej
                  d      dd       ZddZ ej
                  d      dd       ZddZ	ddZ
dd	Zdd
ZddZddZddZddZddZddZddZy)TestIntegrationu+   실제 스크립트 실행 통합 테스트T)autousec                h    t         j                         st        j                  dt          d       yy)u8   스크립트가 없으면 통합 테스트 전체 스킵u   스크립트 없음:     — 통합 테스트 스킵N)SCRIPT_PATHr&   r'   r(   r   s    r   skip_if_no_scriptz!TestIntegration.skip_if_no_scriptB  s,     !!#KK/}<XYZ $r   c                    t         j                  t        t              gt	        |      z   }t        j                  |ddd      S )u[   스크립트를 실행한다. registry 경로는 스크립트에 하드코딩되어 있음.T   )r  r  timeout)sys
executablerr   r  r;   r  r  )r   
extra_argscmds      r   _run_scriptzTestIntegration._run_scriptH  s4    ~~s;/04
3CC~~c$T2NNr   c                h    t         j                         st        j                  dt          d       yy)uB   레지스트리 파일이 없으면 통합 테스트 전체 스킵u   레지스트리 없음: r  N)r%   r&   r'   r(   r   s    r   skip_if_no_registryz#TestIntegration.skip_if_no_registryM  s,     ##%KK2=/A]^_ &r   c                    | j                         }|j                  dk(  s'J d|j                  dd  d|j                  dd         y)u5   스크립트가 오류 없이 실행되어야 한다.r   u   스크립트 실패
stdout: N  z	
stderr: )r  r  r  stderr)r   r  s     r   test_script_runs_successfullyz-TestIntegration.test_script_runs_successfullyS  sU    !!#  A% 	
+FMM$3,?+@
6==Y]Z]K^J_`	
%r   c           	     :   | j                         }|j                  dk(  sJ 	 t        j                  |j                        }t        t              sJ y# t        j
                  $ r2}t        j                  d| d|j                  dd         Y d}~Sd}~ww xY w)u8   스크립트 stdout 이 유효한 JSON 이어야 한다.r   u&   stdout 이 유효한 JSON 이 아님: u	   
출력: Nr  )
r  r  r   r   r  r   r'   r8  r1   r2   )r   r  r"   excs       r   test_output_is_valid_jsonz)TestIntegration.test_output_is_valid_jsonZ  s    !!#  A%%%	g::fmm,D $%%% ## 	gKK@ZPVP]P]^b_bPcOdeff	gs   A B((BBc                    | j                         }|j                  dk(  sJ t        j                  |j                        }h d}|t        |j                               z
  }|r
J d|        y)u/   출력 JSON 에 필수 키가 있어야 한다.r   >   r9   r:  r;  r   r<  u   출력에 필수 키 누락: N)r  r  r   r   r  rM   rN   )r   r  r"   r?  rP   s        r   test_output_has_required_keysz-TestIntegration.test_output_has_required_keysd  sg    !!#  A%%%zz&--(OS--E;G9EE{7r   c                    | j                         }|j                  dk(  sJ t        j                  |j                        }|j                  dg       D ]   }d|v rJ d|j                  d       d        y)uT   출력 items 리스트의 모든 항목에 health_check_result 가 있어야 한다.r   r9   r2  rJ   r?   re  Nr  r  r   r   r  rG   r   r  r"   rO   s       r   'test_all_items_have_health_check_resultz7TestIntegration.test_all_items_have_health_check_resultm  s{    !!#  A%%%zz&--(HHWb) 	D(D0 $((HI0	r   c                   | j                         }|j                  dk(  sJ t        j                  |j                        }h d}|j                  dg       D ])  }|d   |v rJ d|j                  d       d|d    d        y	)
ri  r   >   r8  r.  r(   r9   r2  rJ   r?   rj  rS   Nr  )r   r  r"   rk  rO   s        r   %test_health_check_result_values_validz5TestIntegration.test_health_check_result_values_validw  s    !!#  A%%%zz&--((HHWb) 	D-.%7 $(t<Q7R6SSlm7	r   c                    | j                         }|j                  dk(  sJ t        j                  |j                        }|d   d   t        |d         k(  s J d|d   d    dt        |d          d       y)	u=   summary.total 이 items 리스트 개수와 같아야 한다.r   r:  r'  r9   zsummary.total(z) != len(items)(rP  N)r  r  r   r   r  r6   r   r  r"   s      r   &test_summary_total_matches_items_countz6TestIntegration.test_summary_total_matches_items_count  s    !!#  A%%%zz&--(Iw'3tG}+== 	
T)_W566Fs4PW=GYFZZ[\	
=r   c                    | j                         }|j                  dk(  sJ t        j                  |j                        }|d   }t        d |j                         D              }||d   k(  sJ d| d|d    d       y)	u?   summary 의 status 카운트 합이 total 과 같아야 한다.r   r:  c              3  \   K   | ]$  \  }}|d k7  rt        |t        t        f      r| & ywr`  )r1   r   float)r   rM  ra  s      r   r   zJTestIntegration.test_summary_status_counts_sum_to_total.<locals>.<genexpr>  s0      
!QG|
1sEl ; 
s   *,r'  rN  rO  rP  N)r  r  r   r   r  rQ  r9   )r   r  r"   rI  rR  s        r   rS  z7TestIntegration.test_summary_status_counts_sum_to_total  s    !!#  A%%%zz&--(O 
'')
 

 QwZ' 	
ZLAgJ<qA	
'r   c                   t        j                  t        j                               }t	        t        |d               }| j                  d|      }|j                  dk(  sJ d|j                          t        j                  |j                        }|j                  dg       D ]  }|d   |k(  rJ d|d    d        |j                  d	i       D ]  }||k(  r	J d
| d        y)u'   --source 옵션이 동작해야 한다.r0   z--sourcer   u   --source 실패: r9   r0  u   필터 후 다른 source 'u   ' 가 포함됨r;  u   by_source 에 다른 source 'N)r   r   r%   r!   nextiterr  r  r  r   r   r  rG   )r   r   first_sourcer  r"   rO   r=   s          r   test_source_filter_cliz&TestIntegration.test_source_filter_cli  s    }'>'>'@ADy!9:;!!*l;  A%J):6==/'JJ%zz&--(HHWb) 	D>\1 ,T(^,<OL1	
 88K, 	]C,&\*GuO(\\&	]r   c                   | j                  dd      }|j                  dk(  sJ d|j                          t        j                  |j
                        }|j                  dg       D ]  }|d   dk(  rJ d|d    d        y	)
u'   --status 옵션이 동작해야 한다.z--statusr   r   u   --status 실패: r9   rA   u   필터 후 status='u   ' 항목이 포함됨N)r  r  r  r   r   r  rG   r  s       r   test_status_filter_cliz&TestIntegration.test_status_filter_cli  s    !!*h7  A%J):6==/'JJ%zz&--(HHWb) 	D>X- %d8n%55JK-	r   c                    | j                  d      }|j                  dk(  sJ d|j                          t        j                  |j
                        }d|v sJ d|vsJ d       y)u:   --summary 옵션 실행 시 items 키가 없어야 한다.z	--summaryr   u   --summary 실패: r:  r9   u2   items 가 --summary 출력에 포함되면 안 됨N)r  r  r  r   r   r  r  s      r    test_summary_flag_excludes_itemsz0TestIntegration.test_summary_flag_excludes_items  sk    !!+.  A%K);FMM?'KK%zz&--(D   d"X$XX"r   c                    | j                         }|j                  dk(  sJ t        j                  |j                        }d|v sJ d|v sJ d|v sJ d|v sJ d|v sJ t        j                  |d          y)uL   실제 레지스트리로 실행한 출력의 전체 구조를 검증한다.r   r:  r;  r9   r<  r   N)r  r  r   r   r  r   r   r  s      r   #test_real_registry_output_structurez3TestIntegration.test_real_registry_output_structure  s    !!#  A%%%zz&--(D   d"""$t###d"""tK01r   Nrb   )r  rr   r`   zsubprocess.CompletedProcess)rc   rd   re   rf   r'   rg   r  r  r  r  r  r  r  r  r  rS  r  r  r  r  r,   r   r   r  r  ?  s    5V^^D![ "[
O
 V^^D!` "`

&F	

] Y2r   r  )rf   
__future__r   r   rk   r  r  textwrapr   r   r   r   pathlibr   r'   r   r  r%   dedentr   rT   r   ri   r   r   r   r
  r#  rz  r  r,   r   r   <module>r     s    #  	  
   2 2    KLJK 'x ?( ? B S[ [F"6 "6J1@ 1@h<R <R~JB JBZ!1 !1`LT LThv3 v3@I2 I2r   