from pathlib import Path import pytest from gfa_core.extension_stage.api_converter import ( ApiConversionError, _build_repair_prompt, _validate_spec, ) def test_validate_spec_preserves_input_roles_and_param_placeholders(): spec = _validate_spec({ "original_command": "seqtk subseq At.pep.fa At.id > out.fa", "command_template": "seqtk subseq {input_fasta} {input_file} > {output_dir}/hits.fa --min {param_min_length}", "input_roles": { "input_fasta": {"role": "target_pep_fasta", "resolve_by": "config_species"}, "input_file": {"role": "target_id_list", "resolve_by": "species_final_gfmembers_id"}, }, "parameters": { "min-length": {"default": 80, "editable": True}, "internal flag": {"default": "legacy", "editable": False}, }, }) assert spec["input_roles"]["input_fasta"]["role"] == "target_pep_fasta" assert spec["input_roles"]["input_file"]["required"] is True assert "min_length" in spec["parameters"] assert spec["parameters"]["internal_flag"]["editable"] is False assert spec["requires_user_confirmation"] is False def test_validate_spec_rejects_editable_parameter_not_used_by_template(): with pytest.raises(ApiConversionError, match="editable parameter"): _validate_spec({ "original_command": "blastp -evalue 1e-5", "command_template": "blastp -query {input_fasta} -out {output_dir}/blast.tsv -evalue 1e-5", "parameters": {"evalue": {"default": "1e-5", "editable": True}}, }) def test_repair_prompt_does_not_forbid_parameter_placeholders(tmp_path: Path): raw_code = tmp_path / "code" raw_data = tmp_path / "data" raw_code.mkdir() raw_data.mkdir() (raw_code / "run.sh").write_text("echo test\n", encoding="utf-8") (raw_data / "At.id").write_text("AT1G01010\n", encoding="utf-8") prompt = _build_repair_prompt( extension={ "template_type": "shell_pipeline", "entrypoint": {"command_template": "echo {input_file} > {output_dir}/out.txt"}, "parameters": {}, "inputs": {}, "outputs": {}, "requirements": {}, "runtime": {}, }, raw_code=raw_code, raw_data=raw_data, validation_context={"status": "failed", "error": "missing value"}, attempt=1, max_attempts=3, lessons=[], ) assert "For v1, do not put parameter placeholders" not in prompt assert "Every parameter with editable=true must be used" in prompt assert "input_roles" in prompt def test_repair_prompt_includes_phase_specific_rules(tmp_path: Path): raw_code = tmp_path / "code" raw_data = tmp_path / "data" raw_code.mkdir() raw_data.mkdir() (raw_code / "run.sh").write_text("echo test\n", encoding="utf-8") (raw_data / "input.fa").write_text(">g1\nMA\n", encoding="utf-8") prompt = _build_repair_prompt( extension={ "template_type": "shell_pipeline", "entrypoint": {"command_template": "bash {code_dir}/run.sh > {output_dir}/out.txt"}, "parameters": {}, "inputs": {}, "outputs": {}, "requirements": {}, "runtime": {}, }, raw_code=raw_code, raw_data=raw_data, validation_context={ "repair_phase": "publish_gate", "status": "failed", "error": "Publish gate failed", "publish_checks": {"errors": ["golden original-vs-converted comparison did not pass"]}, }, attempt=1, max_attempts=20, lessons=[], ) assert '"repair_phase": "publish_gate"' in prompt assert "If repair_phase is publish_gate" in prompt assert "writes outputs under work/current directory instead of {output_dir}" in prompt