import os import pytest from agent.rule_loader import RuleLoader, PGM_PATTERN_MAP from agent.models import ProgramMeta RULES_DIR = os.path.join(os.path.dirname(__file__), '..', 'rules') ALL_35_TYPES = [ 'マッチング(1:1)', 'マッチング(1:N)', 'マッチング(N:1)', 'レイアウト編集のみ(GETPUT)', '振り分け(IF文)', '振り分け(EVALUATE文)', 'キーブレイク(集計)', 'キーブレイク(集約)', 'DB更新', '50分割', '25分割', '100分割', '項目チェック(重複含まず)', 'オンラインPGM', 'CSV→FB変換(改行なし)', '2段階マッチング(1:1⇒1:1)', '2段階マッチング(N:1⇒N:1)', 'マッチングM:N⇒出力M件', 'マッチングM:N⇒出力N件', 'マッチングM:N⇒出力M×N件', 'CSV→FB変換(改行あり)', '2段階マッチング(M:N⇒M:N)', 'SELECT条件', '内部テーブル検索', 'サブプログラム使用', 'DB検索', '項目チェック(半角20桁/4桁)', 'SYSIN読込', 'ASCII→EBCDIC変換', 'キーブレイク(集計集約以外)', '項目チェック(重複含む)', '1:N+キーブレイク(同キー)', '1:N+キーブレイク(異キー)', 'SORT(INPUT/OUTPUT PROCEDURE)', 'MERGE(複数ファイル結合)', ] DETAILED_DESIGN_PATTERNS = [ '29(ASCII→EBCDIC変換)', '18(M:N→M件マッチング)', '20(M:N→M×N件直積出力)', 'マッチング(1:1→1:1 2段階)', 'マッチング(N:1→N:1 2段階)', 'マッチング(M:N→M:N 2段階)', '33(1:N+異キーキーブレイク)', '34(SORT INPUT/OUTPUT PROCEDURE)', '11(25分割)', '12(100分割)', '27(半角20桁/4桁チェック)', '19(M:N→N件マッチング+集計)', 'DB更新 + SYSIN読込(P28)', '編集出力(ランキング生成)', '32(1:N+同キーキーブレイク)', '編集出力(印刷制御)', ] def test_pgm_pattern_map_has_known_patterns(): assert 'マッチング(1:1)' in PGM_PATTERN_MAP assert 'マッチング(1:N)' in PGM_PATTERN_MAP assert 'DB更新' in PGM_PATTERN_MAP @pytest.mark.parametrize('pattern', ALL_35_TYPES) def test_all_classification_types_loadable(pattern): """程序分类说明.md の35类型すべてが規則に解決できること。""" loader = RuleLoader(RULES_DIR) assert loader._load_pgm_pattern_rule(pattern) is not None, f'未実装: {pattern}' @pytest.mark.parametrize('pattern', DETAILED_DESIGN_PATTERNS) def test_detailed_design_pattern_names_loadable(pattern): """詳細設計書の PGMパターン/プログラムタイプ 表記が規則に解決できること。""" loader = RuleLoader(RULES_DIR) assert loader._load_pgm_pattern_rule(pattern) is not None, f'マッピング不足: {pattern}' def test_non_aggregate_keybreak_uses_dedicated_rule(): loader = RuleLoader(RULES_DIR) rule = loader._load_pgm_pattern_rule('キーブレイク(集計集約以外)') assert rule is not None assert '非集計' in rule def test_two_stage_matching_split_by_form(): loader = RuleLoader(RULES_DIR) r1 = loader._load_pgm_pattern_rule('2段階マッチング(1:1⇒1:1)') r2 = loader._load_pgm_pattern_rule('2段階マッチング(N:1⇒N:1)') r3 = loader._load_pgm_pattern_rule('マッチング(M:N→M:N 2段階)') assert r1 is not None and r2 is not None and r3 is not None assert '1:1⇒1:1' in r1 assert 'N:1⇒N:1' in r2 assert 'M:N⇒M:N' in r3 assert r1 != r2 and r2 != r3 and r1 != r3 def test_load_matching_1_1_rule(): loader = RuleLoader(RULES_DIR) meta = ProgramMeta( program_id='TEST', program_name='', system_name='', pgm_type='メイン', pgm_pattern='マッチング(1:1)', summary_lines=[], prerequisites=[], files=[], keys=[], modules=[], process_detail='', output_records='', input_type='file', copy_fields={}, db_tables={} ) combined, descriptions, count = loader.load(meta) assert count > 0 assert len(combined) > 0 assert 'マッチング(1:1)' in combined assert '数据生成' in combined def test_load_matching_1_n_rule(): loader = RuleLoader(RULES_DIR) meta = ProgramMeta( program_id='TEST', program_name='', system_name='', pgm_type='メイン', pgm_pattern='マッチング(1:N)', summary_lines=[], prerequisites=[], files=[], keys=[], modules=[], process_detail='', output_records='', input_type='file', copy_fields={}, db_tables={} ) combined, descriptions, count = loader.load(meta) assert count > 0 assert len(combined) > 0 assert 'マッチング(1:N)' in combined def test_matching_n_1_has_only_n_1_imbalance(): loader = RuleLoader(RULES_DIR) rule = loader._load_pgm_pattern_rule('マッチング(N:1)') assert rule is not None _, count = loader._parse_group_info(rule) assert count == 9 assert 'N:1匹配(MT-N003)' in rule or 'N:1匹配(MT-N003)' in rule assert '极端不平衡 N:1' in rule assert '极端不平衡 1:N' not in rule def test_if_branching_rule_covers_if_benchmark(): loader = RuleLoader(RULES_DIR) rule = loader._load_pgm_pattern_rule('振り分け(IF文)') assert rule is not None _, count = loader._parse_group_info(rule) assert count == 5 for marker in ('B-N001', 'B-N005', 'IF 2路分支'): assert marker in rule assert 'B-N006' not in rule assert 'B-N010' not in rule assert 'B-R001' not in rule def test_evaluate_branching_rule_covers_evaluate_benchmark(): loader = RuleLoader(RULES_DIR) rule = loader._load_pgm_pattern_rule('振り分け(EVALUATE文)') assert rule is not None _, count = loader._parse_group_info(rule) assert count == 5 for marker in ('B-N006', 'B-N009', 'B-A002', 'EVALUATE WHEN 多值'): assert marker in rule assert 'B-N001' not in rule assert 'B-N010' not in rule assert 'B-R001' not in rule def test_keybreak_summary_rule_covers_benchmark(): loader = RuleLoader(RULES_DIR) rule = loader._load_pgm_pattern_rule('キーブレイク(集計)') assert rule is not None _, count = loader._parse_group_info(rule) assert count == 7 for marker in ('KB-N001', 'KB-N004', 'KB-N005', 'KB-N006', 'KB-A001', 'KB-A002', 'KB-R001'): assert marker in rule assert 'KB-N002' not in rule def test_keybreak_aggregation_rule_covers_benchmark(): loader = RuleLoader(RULES_DIR) rule = loader._load_pgm_pattern_rule('キーブレイク(集約)') assert rule is not None _, count = loader._parse_group_info(rule) assert count == 7 for marker in ('KB-N002', 'KB-N004', 'KB-N005', 'KB-N006', 'KB-A001', 'KB-A002', 'KB-R001'): assert marker in rule assert 'KB-N001' not in rule def test_combined_keybreak_rule_removed_and_remapped(): combined = os.path.join(RULES_DIR, 'pgm_pattern', 'キーブレイク(集計、集約).md') assert not os.path.exists(combined) loader = RuleLoader(RULES_DIR) summary = loader._load_pgm_pattern_rule('キーブレイク(集計)') for pattern in ('キーブレイク(集計、集約)', '1:N+キーブレイク(同キー)', '1:Nキーブレイク(同キー集約)', '32(1:N+同キーキーブレイク)'): rule = loader._load_pgm_pattern_rule(pattern) assert rule is not None assert rule == summary def test_item_check_no_dup_rule_covers_benchmark(): loader = RuleLoader(RULES_DIR) rule = loader._load_pgm_pattern_rule('項目チェック(重複含まず)') assert rule is not None _, count = loader._parse_group_info(rule) assert count == 5 for marker in ('VF-N001', 'VF-N002', 'VF-N003', 'VF-N004', 'VF-R001'): assert marker in rule assert 'VF-N005' not in rule assert not os.path.exists(os.path.join(RULES_DIR, 'pgm_pattern', '項目チェック.md')) generic = loader._load_pgm_pattern_rule('項目チェック') assert generic == rule def test_item_check_halfwidth_rule_covers_benchmark(): loader = RuleLoader(RULES_DIR) rule = loader._load_pgm_pattern_rule('項目チェック(半角20桁/4桁)') assert rule is not None _, count = loader._parse_group_info(rule) assert count == 4 for marker in ('VF-N005', 'VF-N006', 'VF-A001', 'VF-A002'): assert marker in rule assert 'VF-N001' not in rule def test_load_nonexistent_pattern_raises(): loader = RuleLoader(RULES_DIR) meta = ProgramMeta( program_id='TEST', program_name='', system_name='', pgm_type='メイン', pgm_pattern='存在しないパターン', summary_lines=[], prerequisites=[], files=[], keys=[], modules=[], process_detail='', output_records='', input_type='file', copy_fields={}, db_tables={} ) try: loader.load(meta) assert False, 'Should have raised FileNotFoundError' except FileNotFoundError: pass def test_detect_conditional_branch(): process = """ 2-1-2.ロジック分岐判定(EVALUATE) 2-1-2-1.STATUS='1'の場合 INSERT処理 """ assert RuleLoader._detect_feature(process, ['場合', 'EVALUATE', 'IF']) is True def test_no_keyword_match(): process = "1.初期処理\n2.主処理\n3.終了処理" assert RuleLoader._detect_feature(process, ['EVALUATE']) is False def test_rule_with_conditional_feature(): loader = RuleLoader(RULES_DIR) meta = ProgramMeta( program_id='TEST', program_name='', system_name='', pgm_type='メイン', pgm_pattern='マッチング(1:1)', summary_lines=[], prerequisites=[], files=[], keys=[], modules=[], process_detail='2-1-2.EVALUATEで条件分岐する。', output_records='', input_type='file', copy_fields={}, db_tables={} ) combined, descriptions, count = loader.load(meta) assert count > 0 assert '条件分支' in combined