from ops import *
import csv
import re

plan=json.loads((R/'plan.json').read_text())
out=json.loads((R/'recreate-result.json').read_text())
assert inventory()==plan['others']
assert all(x['state']=='stopped' for x in streams())
now=inspect('contextra-redis-1')
assert now['State']['Health']['Status']=='healthy'
assert now['HostConfig']['RestartPolicy']==plan['redis']['HostConfig']['RestartPolicy']
assert now['HostConfig']['PortBindings']==plan['redis']['HostConfig']['PortBindings']
acl=m.query('admin','ACL','GETUSER','contextra_reader')
assert set(acl['commands'].split())-set(plan['reader']['commands'].split())=={'+'+x for x in ADDED}
assert all(acl[k]==plan['reader'][k] for k in ['passwords','keys','channels','flags','selectors'])
assert all('+'+x in FILES['acl'].read_text().splitlines()[next(i for i,l in enumerate(FILES['acl'].read_text().splitlines()) if l.startswith('user contextra_reader '))].split() for x in ADDED)
for k in ['config','acl']:
    p=FILES[k];assert p.stat().st_uid==999 and p.stat().st_gid==1000 and p.stat().st_mode&0o777==0o600
for k in ['config','compose','env','access','firewall']: assert g.sha(FILES[k])==plan['hashes'][k]
cfg=json.loads(subprocess.check_output(compose_args()+['config','--format','json'],cwd=REPO))
cmd=cfg['services']['redis']['command'];assert cmd[cmd.index('--maxmemory')+1]=='8gb'
an=inspect('contextra-analytics-1');env=dict(e.split('=',1) for e in an['Config']['Env'] if '=' in e)
assert env['CONTEXTRA_METADATA_MAX_MESSAGE_BYTES']=='10485760' and env['CONTEXTRA_METADATA_MAX_BYTES']=='268435456'
pub=m.api('/api/v1/metadata/status')
assert pub['metrics'].get('send_error',0)==out['publisher']['metrics'].get('send_error',0)
assert pub['metrics']['sent_status']>out['publisher']['metrics']['sent_status']
assert pub['metrics']['sent_frames']==out['publisher']['metrics']['sent_frames']
assert pub['metrics']['queued']==0
reads=keys(); assert len(reads)==3
docs=[REPO/'docs/integration/metadata-web-review.ko.md',REPO/'docs/profiles/ltb1021/topics/runtime-lifecycle/decision/20260915T111200+0900-decision-reader-extension-html.md',REPO/'docs/profiles/ltb1021/topics/runtime-lifecycle/runs/20260915T111200+0900-run-redis-extension-html.md']
links=0
for path in docs:
    for target in re.findall(r'\]\(([^)]+)\)',path.read_text()):
        if target.startswith(('http:','https:','#')):continue
        assert (path.parent/target.split('#')[0]).resolve().exists(),target
        links+=1
sample=json.loads((REPO/'docs/integration/metadata-example.json').read_text())
t=sample['frameTime'];assert t['detectedAtWallMs']==t['startWallTimeMs']+t['rtpTimestamp']*1000//t['rtpClockRate']
htmlpath=Path('/workspace/architecture_docs/current/contextra_metadata_web/metadata-web-guide.ko.html')
assert g.sha(docs[0]) in htmlpath.read_text()
assert not re.search(r'\b(?:\d{1,3}\.){3}\d{1,3}\b', htmlpath.read_text())
secrets=json.loads((C/'secrets.json').read_text())
for path in docs+[htmlpath]:
    assert all(not isinstance(v,str) or v not in path.read_text() for v in secrets.values())
subprocess.run(['git','diff','--check'],cwd=REPO,check=True)
summary=dict(acl='PASS',added=ADDED,dryrun_cases=27,redis='HEALTHY',maxmemory=8589934592,container_start_arg='8gb',persistent='8gb',recreate=1,corrective_start=1,extra_restart_test=0,initial_failure='Config bind root0600 unreadable by Redis UID999; recovered after exact file ownership correction',rdb='Loaded;3 keys expired under original TTL;3 status/channel keys republished',data_volume_preserved=True,other_containers_preserved=True,ai_start=0,ai_streams_stopped=2,publisher_error_count_stable=68,publisher_queue=0,publisher_status_progress=True,frame_tests='NOT_RUN',tests=4,html_browser=json.loads((R/'browser-checks.json').read_text()),html_sha256=g.sha(htmlpath),html_bytes=htmlpath.stat().st_size,local_links=links,capacity_errors=0,web_runtime='USER_VERIFICATION_PENDING',windows='DEFERRED',portfolio='MEDIUM; Web integration acceptance pending',elapsed_seconds=round(time.time()-plan['started'],2))
save('summary.json',summary)
with (R/'summary.csv').open('w') as f:
    w=csv.writer(f);w.writerow(['item','result','scope']);w.writerows([['ACL','PASS','10 additions;27 DRYRUN'],['Redis','PASS','same image/volume;8GiB live/command/persistent'],['Data','PASS','shutdown RDB;3 normal TTL expirations;status republished'],['Recovery','PASS','root0600 permission corrected;one corrective start'],['Publisher','PASS','status progressing;queue0;errors stable'],['HTML','PASS','desktop/mobile;offline;examples embedded'],['Web runtime','PENDING','no new AI Start or actual Web sync test']])
save('report-receipt.json',dict(schema_version='ltb1021.codex_hub.report_receipt.v1',receipt_id='contextra-redis-html-20260915-111200',timestamp='2026-09-15T11:12:00+09:00',session_alias='work-session',task='Contextra Redis 조회 권한과 재생성 정합 및 Web HTML 가이드',results=['Reader 조회10개 추가와 기존 인증/key scope 유지','동일 image/volume Redis 재생성으로 live/시작 인자/persistent8GiB 정합화','첫 기동 bind 권한 오류를 exact config/ACL 소유권 보정으로 복구','합의된7개 Markdown 설명과 standalone HTML 및 내장 예제 준비'],verify=['ACL DRYRUN27개 PASS;읽기 소량 확인;쓰기 삭제 시험 없음','RDB 로딩과 기존 TTL3개 정상 만료 및 publisher 상태 재게시 확인','Reader4 tests 및 desktop/mobile HTML/JSON/link 검증 PASS','다른 container 보존;AI Start0;복구 뒤 queue0/send_error 증가 없음'],commits=[],remaining=['Windows RTSP와 실제 Web 수신/동기화 후속','Event/Artifact 별도 구현','컨테이너 단순 restart 추가 실험은 미실행;새 시작 인자와 재생성 부팅 확인']))
for path in R.iterdir():
    if path.is_file():path.chmod(0o600)
print(json.dumps(summary,ensure_ascii=False))
