fix: _parse_llm_response now handles empty/invalid JSON gracefully
test: add gap coverage tests (hina_agent/JCL/quality gate edge cases)
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user