diff --git a/hina/hina_agent.py b/hina/hina_agent.py index f94d09a..49e7534 100644 --- a/hina/hina_agent.py +++ b/hina/hina_agent.py @@ -151,8 +151,11 @@ def _parse_llm_response(raw: str) -> dict: end = text.index("```", start) if "```" in text[start:] else len(text) text = text[start:end].strip() - parsed = json.loads(text) - return _validate_result(parsed) + try: + parsed = json.loads(text) + return _validate_result(parsed) + except (json.JSONDecodeError, ValueError): + return _validate_result({}) def _validate_result(parsed: dict) -> dict: diff --git a/test-data/test_gap_coverage.py b/test-data/test_gap_coverage.py new file mode 100644 index 0000000..ab14481 --- /dev/null +++ b/test-data/test_gap_coverage.py @@ -0,0 +1,184 @@ +""" +テストギャップ穴埋め — 未検証モジュールの機能テスト +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +対象: hina.hina_agent, jcl.executor, jcl.parser +""" +import sys, json +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +PASS=0;FAIL=0;LOG=[] +def do(cat,name,fn): + global PASS,FAIL + try: fn(); PASS+=1; LOG.append(f' [{cat}] {name} -> PASS') + except Exception as e: FAIL+=1; LOG.append(f' [{cat}] {name} -> FAIL: {str(e)[:100]}') + +# ── hina.hina_agent: LLM応答パース ── +from hina.hina_agent import _parse_llm_response, _validate_result, _fallback_classification, CONFUSION_PROMPT + +do('HAG','_parse_llm_response: 生JSON', lambda: ( + r:=_parse_llm_response('{"category":"condition_heavy","confidence":0.85}'), + r['category']=='condition_heavy' and r['confidence']==0.85)) +do('HAG','_parse_llm_response: ```json ブロック', lambda: ( + r:=_parse_llm_response('```json\n{"category":"data_file_centric","confidence":0.9}\n```'), + r['category']=='data_file_centric' and r['confidence']==0.9)) +do('HAG','_parse_llm_response: ``` ブロック(無json)', lambda: ( + r:=_parse_llm_response('```\n{"category":"simple_sequential","confidence":0.7}\n```'), + r['category']=='simple_sequential')) +do('HAG','_parse_llm_response: 空文字', lambda: ( + r:=_parse_llm_response(''), + r['category']=='unknown')) +do('HAG','_parse_llm_response: 無効JSON', lambda: ( + r:=_parse_llm_response('not json at all'), + r['category']=='unknown')) +do('HAG','_validate_result: 最小値', lambda: ( + r:=_validate_result({}), + r['category']=='unknown' and r['confidence']==0.0 and r['required_tests']>=1)) +do('HAG','_validate_result: 信頼度クランプ', lambda: ( + r:=_validate_result({'confidence':5.0,'required_tests':0}), + r['confidence']<=1.0 and r['required_tests']>=1)) +do('HAG','_validate_result: 信頼度下限', lambda: ( + r:=_validate_result({'confidence':-1.0}), + r['confidence']>=0.0)) +do('HAG','_validate_result: 不正タイプ', lambda: ( + r:=_validate_result({'confidence':'abc','required_tests':'xyz'}), + r['confidence']==0.0 and r['required_tests']>=1)) +do('HAG','_fallback_classification: 分岐0', lambda: ( + r:=_fallback_classification({'decision_points':[],'paragraphs':[],'file_count':0}), + r['category']=='simple_sequential')) +do('HAG','_fallback_classification: SEARCH ALL', lambda: ( + r:=_fallback_classification({'decision_points':[{'kind':'IF'}],'paragraphs':[],'file_count':0,'has_search_all':True,'has_call':False,'has_break':False}), + r['category']=='search_intensive')) +do('HAG','_fallback_classification: CALLベース', lambda: ( + r:=_fallback_classification({'decision_points':[{'kind':'IF'}],'paragraphs':[],'file_count':0,'has_search_all':False,'has_call':True,'has_break':False}), + r['category']=='call_based')) +do('HAG','_fallback_classification: mixed_complex', lambda: ( + r:=_fallback_classification({'decision_points':[{'kind':'IF'}]*5,'paragraphs':[],'file_count':2,'has_search_all':True,'has_call':True,'has_break':True}), + r['category']=='mixed_complex')) +do('HAG','CONFUSION_PROMPT 書式', lambda: ( + p:=CONFUSION_PROMPT.format(paragraph_count=3,decision_count=2,if_count=1, + evaluate_count=1,file_count=1,open_directions='{}',has_search_all='false', + has_call='false',has_break='false',total_branches=2), + 'paragraph_count' not in p and 'IF' in p)) + +# ── jcl.parser: JCL解析 ── +from jcl.parser import parse_jcl + +SAMPLE_JCL = """//CREDIT25 JOB (CRD),'MONTHLY BILLING',CLASS=A,MSGCLASS=X +//STEP1 EXEC PGM=SORT +//SORTIN DD DSN=TRANSACTIONS.DATA,DISP=SHR +//SORTOUT DD DSN=SORTED.DATA,DISP=(NEW,PASS) +//SYSIN DD * + SORT FIELDS=(1,16,CH,A) +//STEP2 EXEC PGM=CRDVAL,COND=(0,NE) +//TRANSIN DD DSN=SORTED.DATA,DISP=(OLD,DELETE) +//MEMBER DD DSN=MEMBER.DATA,DISP=SHR +//VALIDOUT DD DSN=VALID.DATA,DISP=(NEW,CATLG) +//REJECT DD SYSOUT=* +//REPORTERR DD SYSOUT=* +//STEP3 EXEC PGM=CRDCALC,COND=(0,NE) +//VALIDIN DD DSN=VALID.DATA,DISP=(OLD,DELETE) +//RATE DD DSN=RATE.DATA,DISP=SHR +//CALCOUT DD DSN=CALC.DATA,DISP=(NEW,CATLG) +//STEP4 EXEC PGM=CRDRPT,COND=(0,NE) +//BILLING DD DSN=CALC.DATA,DISP=(OLD,DELETE) +//STMT DD DSN=STMT.DATA,DISP=(NEW,CATLG) +//SUMMARY DD DSN=SUMMARY.DATA,DISP=(NEW,CATLG) +// DD SYSOUT=* +""" + +do('JCL','parse_jcl 4STEP解析', lambda: ( + j:=parse_jcl(SAMPLE_JCL), + len(j.steps)==4)) +do('JCL','JOB情報解析', lambda: ( + j:=parse_jcl(SAMPLE_JCL), + j.job_name=='CREDIT25' and j.job_class=='A')) +do('JCL','STEP1:SORT PGM定義', lambda: ( + j:=parse_jcl(SAMPLE_JCL), + j.steps[0].program=='SORT' and j.steps[0].step_name=='STEP1')) +do('JCL','DD定義:入力ファイル', lambda: ( + j:=parse_jcl(SAMPLE_JCL), + any('TRANSACTIONS' in d.dsn for d in j.steps[0].dd_list))) +do('JCL','DD定義:出力ファイル', lambda: ( + j:=parse_jcl(SAMPLE_JCL), + any('VALID.DATA' in d.dsn for d in j.steps[1].dd_list))) +do('JCL','CONDパラメータ', lambda: ( + j:=parse_jcl(SAMPLE_JCL), + j.steps[1].cond is not None and '0' in str(j.steps[1].cond))) +do('JCL','SYSINインラインデータ', lambda: ( + j:=parse_jcl(SAMPLE_JCL), + len(j.steps[0].sysin_lines)>0 and 'SORT' in j.steps[0].sysin_lines[0])) +do('JCL','SYSOUT出力', lambda: ( + j:=parse_jcl(SAMPLE_JCL), + any('*' in d.dsn for d in j.steps[1].dd_list))) +do('JCL','空JCL', lambda: ( + j:=parse_jcl(''), + len(j.steps)==0)) +do('JCL','コメント行スキップ', lambda: ( + j:=parse_jcl('//* THIS IS COMMENT\n//STEP1 EXEC PGM=TEST\n'), + len(j.steps)==1 and j.steps[0].program=='TEST')) + +# ── jcl.executor ── +from jcl.executor import JclExecutor, CondEvaluator + +do('JEX','CondEvaluator: (0,NE)', lambda: ( + CondEvaluator().evaluate('(0,NE)', 0)==False)) +do('JEX','CondEvaluator: (0,NE) RC=4', lambda: ( + CondEvaluator().evaluate('(0,NE)', 4)==True)) +do('JEX','CondEvaluator: (0,GT) RC=0', lambda: ( + CondEvaluator().evaluate('(0,GT)', 0)==False)) +do('JEX','CondEvaluator: (0,GT) RC=4', lambda: ( + CondEvaluator().evaluate('(0,GT)', 4)==True)) +do('JEX','CondEvaluator: (4,LE) RC=4', lambda: ( + CondEvaluator().evaluate('(4,LE)', 4)==True)) +do('JEX','CondEvaluator: (4,LE) RC=8', lambda: ( + CondEvaluator().evaluate('(4,LE)', 8)==False)) +do('JEX','CondEvaluator: EVEN', lambda: ( + CondEvaluator().evaluate('EVEN', 0)==True)) +do('JEX','CondEvaluator: ONLY', lambda: ( + CondEvaluator().evaluate('ONLY', 0)==True)) +do('JEX','CondEvaluator: 空文字列', lambda: ( + CondEvaluator().evaluate('', 0)==None)) +do('JEX','JclExecutor インスタンス', lambda: ( + e:=JclExecutor(), + hasattr(e,'execute_step'))) +do('JEX','DD→環境変数マッピング', lambda: ( + e:=JclExecutor(), + m:=e._build_env({'TRANSIN':'/data/in.dat','VALIDOUT':'/data/out.dat'}), + 'TRANSIN' in m and 'VALIDOUT' in m)) + +# ── quality モジュール ── +from quality.l1_offset_validate import L1OffsetValidator +from quality.l2_value_roundtrip import L2RoundtripValidator + +do('QLT','L1OffsetValidator インスタンス', lambda: ( + v:=L1OffsetValidator(), + hasattr(v,'validate'))) +do('QLT','L2RoundtripValidator インスタンス', lambda: ( + v:=L2RoundtripValidator(), + hasattr(v,'validate'))) + +# ── HINA gate: エッジケース ── +from hina.gate import check as gate_check, _compute_score + +do('QG','スコア上限=1.0', lambda: _compute_score({'branch_rate':1.0,'paragraph_rate':1.0},{})<=1.0) +do('QG','スコア下限=0.4', lambda: _compute_score({'branch_rate':0.0,'paragraph_rate':0.0},{})>=0.4) +do('QG','境界:分岐率0.8999→不合格', lambda: ( + r:=gate_check([{'x':1}],{},{'branch_rate':0.8999,'paragraph_rate':1.0,'uncovered_decision_ids':[]}), + not r['passed'])) +do('QG','境界:分岐率0.9→合格', lambda: ( + r:=gate_check([{'x':1}],{},{'branch_rate':0.9,'paragraph_rate':1.0,'uncovered_decision_ids':[]}), + r['passed'])) +do('QG','issue:段落不足のみ', lambda: ( + r:=gate_check([{'x':1}],{},{'branch_rate':1.0,'paragraph_rate':0.5,'uncovered_decision_ids':[]}), + not r['passed'] and 'paragraph_gaps' in r['issues'])) + +# ── 集計 ── +print(); [print(l) for l in LOG] +total=PASS+FAIL +print(f'\n{"="*67}') +print(f' Gap Coverage Test Results') +print(f' Total: {total} | PASS: {PASS} | FAIL: {FAIL} | RATE: {PASS/max(total,1)*100:.1f}%') +print(f' Untested modules covered: hina.hina_agent ✅ jcl.parser ✅ jcl.executor ✅') +print(f'{"="*67}') +sys.exit(0 if FAIL==0 else 1)