codeplain 0.3.11.dev19__tar.gz → 0.3.11.dev20__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/PKG-INFO +1 -1
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/codeplain_REST_api.py +26 -32
- codeplain-0.3.11.dev20/render_machine/actions/fix_unit_tests.py +256 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/actions/run_unit_tests.py +9 -0
- codeplain-0.3.11.dev20/render_machine/agent_tools.py +270 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/render_context.py +9 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/render_types.py +45 -0
- codeplain-0.3.11.dev20/tests/test_agent_tools.py +166 -0
- codeplain-0.3.11.dev20/tests/test_fix_unit_tests_action.py +239 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_fix_unit_tests_conformance_context.py +120 -25
- codeplain-0.3.11.dev19/render_machine/actions/fix_unit_tests.py +0 -79
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/.claude/settings.json +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/.flake8 +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/.github/dependabot.yml +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/.github/workflows/e2e.yml +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/.github/workflows/lint-and-test.yml +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/.github/workflows/lint-powershell.yml +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/.github/workflows/nofity-slack-on-main-merge.yml +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/.github/workflows/publish-dev-to-pypi.yml +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/.github/workflows/publish-install-script.yml +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/.github/workflows/publish-to-pypi.yml +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/.gitignore +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/.vscode/launch.json +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/.vscode/settings.json +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/CLAUDE.md +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/CONTRIBUTING.md +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/LICENSE +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/README.md +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/change_detection.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/cli_output/__init__.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/cli_output/dry_run.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/cli_output/render_summary.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/cli_output/status.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/concept_utils.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/config/__init__.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/config/system_config.yaml +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/conformance_fix_journal.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/diff_utils.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/docs/generate_cli.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/docs/plain2code_cli.md +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/docs/starting_a_plain_project_from_scratch.md +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/event_bus.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/examples/example_hello_world_golang/config.yaml +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/examples/example_hello_world_golang/harness_tests/hello_world_test.go +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/examples/example_hello_world_golang/hello_world_golang.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/examples/example_hello_world_golang/run.sh +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/examples/example_hello_world_python/config.yaml +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/examples/example_hello_world_python/harness_tests/hello_world_display/test_hello_world.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/examples/example_hello_world_python/hello_world_python.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/examples/example_hello_world_python/run.sh +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/examples/example_hello_world_react/config.yaml +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress/e2e/hello_world.cy.ts +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress/support/e2e.ts +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress.config.ts +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/examples/example_hello_world_react/harness_tests/hello_world_display/package.json +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/examples/example_hello_world_react/harness_tests/hello_world_display/tsconfig.json +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/examples/example_hello_world_react/hello_world_react.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/examples/example_hello_world_react/run.sh +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/examples/run.sh +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/file_utils.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/git_preflight.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/git_utils.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/install/bash/examples.sh +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/install/bash/install.sh +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/install/bash/walkthrough.sh +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/install/powershell/examples.ps1 +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/install/powershell/install.ps1 +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/install/powershell/walkthrough.ps1 +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/memory_management.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/metadata_utils.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/module_renderer.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/partial_rendering.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/path_resolution.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/plain2code.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/plain2code_arguments.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/plain2code_cli.md +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/plain2code_console.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/plain2code_events.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/plain2code_exceptions.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/plain2code_logger.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/plain2code_nodes.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/plain2code_read_config.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/plain2code_state.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/plain2code_telemetry.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/plain2code_utils.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/plain_file.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/plain_modules.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/plain_spec.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/pyproject.toml +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/pytest.ini +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/__init__.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/actions/analyze_specification_ambiguity.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/actions/base_action.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/actions/commit_conformance_tests_changes.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/actions/commit_implementation_code_changes.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/actions/create_dist.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/actions/distill_conformance_test_memory.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/actions/exit_with_error.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/actions/finish_functional_requirement.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/actions/fix_conformance_test.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/actions/prepare_repositories.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/actions/prepare_testing_environment.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/actions/refactor_code.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/actions/render_conformance_tests.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/actions/render_functional_requirement.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/actions/run_conformance_tests.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/actions/summarize_conformance_tests.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/code_renderer.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/conformance_tests.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/implementation_code_helpers.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/render_utils.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/state_machine_config.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/states.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/render_machine/triggers.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/requirements.txt +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/resources/codeplain_overview.png +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/resources/plain_example.png +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/standard_template_library/golang-console-app-template.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/standard_template_library/python-console-app-template.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/standard_template_library/typescript-react-app-boilerplate.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/standard_template_library/typescript-react-app-template.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/system_config.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/test_scripts/run_conformance_tests_cypress.ps1 +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/test_scripts/run_conformance_tests_cypress.sh +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/test_scripts/run_conformance_tests_golang.ps1 +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/test_scripts/run_conformance_tests_golang.sh +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/test_scripts/run_conformance_tests_python.ps1 +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/test_scripts/run_conformance_tests_python.sh +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/test_scripts/run_unittests_flutter.ps1 +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/test_scripts/run_unittests_flutter.sh +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/test_scripts/run_unittests_golang.ps1 +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/test_scripts/run_unittests_golang.sh +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/test_scripts/run_unittests_python.ps1 +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/test_scripts/run_unittests_python.sh +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/test_scripts/run_unittests_react.ps1 +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/test_scripts/run_unittests_react.sh +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/__init__.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/conftest.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/acceptance_tests_warning/main_requiring_acceptance_tests.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/acceptance_tests_warning/required_with_acceptance_tests.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/imports/circular_imports_1.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/imports/circular_imports_2.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/imports/circular_imports_main.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/imports/diamond_import_1.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/imports/diamond_import_2.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/imports/diamond_import_common.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/imports/diamond_imports_main.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/imports/import_with_functionalities.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/imports/imports_module_with_functionalities.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/imports/non_existent_import.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/partial_rendering/pr_code_var.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/partial_rendering/pr_implement.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/partial_rendering/pr_leaf.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/partial_rendering/pr_middle.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/partial_rendering/pr_root.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/partial_rendering/pr_solo.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfile/add_new_task_modal_specification.yaml +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfile/duplicate_specification_heading.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfile/empty_functional_specs_section.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfile/invalid_specification_order.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfile/missing_non_functional_requirements.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfile/no_functional_specs_section.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfile/plain_source_with_absolute_link.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfile/plain_source_with_missing_link.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfile/plain_source_with_url_link.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfile/task_list_ui_specification.yaml +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfile/task_manager_with_reference_links.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfile/without_non_functional_requirement.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/concept_validation_acceptance_tests.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/concept_validation_acceptance_tests_nondefined.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/concept_validation_defined_nondefined.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/concept_validation_defined_nondefined_2.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/concept_validation_definition.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/concept_validation_noconcepts.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/concept_validation_nonconcept.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/concept_validation_nondefined.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/concept_validation_redefinition.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/concept_validation_several_concepts.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/concept_validation_valid.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/cyclic_definitions.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_base.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_defaults_exported.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_example.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_imported_exported.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_inherited_reexported.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_malformed.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_missing_definition.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_missing_definition_example.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_multiple_per_bullet_undefined.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_nested.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_nested_example.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_no_definitions.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_not_declared.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_not_declared_example.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_transitive_example.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_transitive_l1.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/exported_concepts_transitive_l2.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/plain_file_parser_with_comments.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/plain_file_with_comments_indented.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/regular_plain_source.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/required_concepts.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/required_concepts_defs.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/required_concepts_example.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/required_concepts_l1.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/required_concepts_l2.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/required_concepts_missing.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/required_concepts_module.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/required_concepts_partial.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/required_concepts_partial_duplicate.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/requires_without_definitions_example.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/topological_sort.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/plainfileparser/topological_sort_not_referenced.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/requires/circular_requires_main.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/requires/circular_requires_sub.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/requires/diamond_requires_1.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/requires/diamond_requires_2.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/requires/diamond_requires_common.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/requires/diamond_requires_main.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/requires/independent_requires_1.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/requires/independent_requires_2.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/requires/independent_requires_main.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/requires/non_existent_require.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/requires/normal_requires_1.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/requires/normal_requires_2.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/requires/normal_requires_common.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/requires/normal_requires_main.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/sample_base64_image.txt +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/simple.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/templates/block_level_include.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/templates/code_variables.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/templates/header.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/templates/implement.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/templates/implement_2.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/templates/template_include.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/data/templates/test_hardest_problem.plain +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/e2e/Dockerfile +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/e2e/conftest.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/e2e/test_hello_world_python.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/e2e/test_hello_world_python_windows.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_arg_provenance.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_change_detection.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_cli_output.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_conformance_fix_journal.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_console_log_styles.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_distill_conformance_test_memory.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_dry_run_link_validation.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_file_utils.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_folder_path_resolution.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_git_preflight.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_git_utils.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_headless_logging.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_imports.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_log_file_name_resolution.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_logging_config_path_resolution.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_partial_rendering.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_path_resolution.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_plain2code.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_plain2code_state.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_plain2code_utils.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_plain_modules.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_plainfile.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_plainfileparser.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_plainspec.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_prepare_repositories_layout.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_render_finished_report.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_requires.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_resolve_config_file.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_script_path_resolution.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_system_config.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_telemetry.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_tui_components.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_tui_usage.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tests/test_usage_summary.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tui/__init__.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tui/components.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tui/models.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tui/plain2code_tui.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tui/plain_module_render_choice_tui.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tui/spinner.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tui/state_handlers.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tui/styles.css +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/tui/widget_helpers.py +0 -0
- {codeplain-0.3.11.dev19 → codeplain-0.3.11.dev20}/usage_summary.py +0 -0
|
@@ -253,38 +253,6 @@ class CodeplainAPI:
|
|
|
253
253
|
|
|
254
254
|
return self.post_request(endpoint_url, headers, payload, run_state)
|
|
255
255
|
|
|
256
|
-
def fix_unittests_issue(
|
|
257
|
-
self,
|
|
258
|
-
frid,
|
|
259
|
-
plain_source_tree,
|
|
260
|
-
linked_resources,
|
|
261
|
-
existing_files_content,
|
|
262
|
-
module_name: str,
|
|
263
|
-
required_modules,
|
|
264
|
-
unittests_issue,
|
|
265
|
-
run_state: RunState,
|
|
266
|
-
conformance_tests_fixes: list[dict] | None = None,
|
|
267
|
-
):
|
|
268
|
-
endpoint_url = f"{self.api_url}/fix_unittests_issue"
|
|
269
|
-
headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
|
|
270
|
-
|
|
271
|
-
payload = {
|
|
272
|
-
"frid": frid,
|
|
273
|
-
"plain_source_tree": plain_source_tree,
|
|
274
|
-
"linked_resources": linked_resources,
|
|
275
|
-
"existing_files_content": existing_files_content,
|
|
276
|
-
"module_name": module_name,
|
|
277
|
-
"required_modules": required_modules,
|
|
278
|
-
"unittests_issue": unittests_issue,
|
|
279
|
-
"unittest_batch_id": run_state.unittest_batch_id,
|
|
280
|
-
}
|
|
281
|
-
# Implementation code changes made by the conformance tests fixer right before this unit tests run;
|
|
282
|
-
# only sent when unit tests are processed inside the conformance tests phase.
|
|
283
|
-
if conformance_tests_fixes is not None:
|
|
284
|
-
payload["conformance_tests_fixes"] = conformance_tests_fixes
|
|
285
|
-
|
|
286
|
-
return self.post_request(endpoint_url, headers, payload, run_state)
|
|
287
|
-
|
|
288
256
|
def distill_conformance_test_memory(
|
|
289
257
|
self,
|
|
290
258
|
frid,
|
|
@@ -558,3 +526,29 @@ class CodeplainAPI:
|
|
|
558
526
|
}
|
|
559
527
|
|
|
560
528
|
return self.post_request(endpoint_url, headers, payload, run_state)
|
|
529
|
+
|
|
530
|
+
def agent_start(self, task_type: str, task_params: dict, frid: str, module_name: str, run_state: RunState):
|
|
531
|
+
"""Start a server-side agent session; returns the first turn (tool calls or completion)."""
|
|
532
|
+
endpoint_url = f"{self.api_url}/agent/start"
|
|
533
|
+
headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
|
|
534
|
+
payload = {
|
|
535
|
+
"task_type": task_type,
|
|
536
|
+
"task_params": task_params,
|
|
537
|
+
"frid": frid,
|
|
538
|
+
"module_name": module_name,
|
|
539
|
+
}
|
|
540
|
+
return self.post_request(endpoint_url, headers, payload, run_state)
|
|
541
|
+
|
|
542
|
+
def agent_continue(
|
|
543
|
+
self, session_id: str, tool_results: list[dict], frid: str, module_name: str, run_state: RunState
|
|
544
|
+
):
|
|
545
|
+
"""Feed tool results into an agent session and run its next turn."""
|
|
546
|
+
endpoint_url = f"{self.api_url}/agent/continue"
|
|
547
|
+
headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
|
|
548
|
+
payload = {
|
|
549
|
+
"session_id": session_id,
|
|
550
|
+
"tool_results": tool_results,
|
|
551
|
+
"frid": frid,
|
|
552
|
+
"module_name": module_name,
|
|
553
|
+
}
|
|
554
|
+
return self.post_request(endpoint_url, headers, payload, run_state)
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from http import HTTPStatus
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
import file_utils
|
|
8
|
+
import plain_spec
|
|
9
|
+
from plain2code_console import console
|
|
10
|
+
from plain2code_exceptions import InternalClientError
|
|
11
|
+
from render_machine import agent_tools
|
|
12
|
+
from render_machine.actions.base_action import BaseAction
|
|
13
|
+
from render_machine.render_context import RenderContext
|
|
14
|
+
|
|
15
|
+
TASK_TYPE = "fix_unit_tests"
|
|
16
|
+
SUBMIT_FIX_TOOL = "submit_fix"
|
|
17
|
+
# Upper bound on LLM turns spent on one fix attempt; the server bounds the whole session.
|
|
18
|
+
MAX_AGENT_TURNS_PER_ATTEMPT = 40
|
|
19
|
+
# Answer to a submit_fix whose fix was accepted in an earlier unit-test loop of the conformance phase.
|
|
20
|
+
ACCEPTED_THEN_CHANGED_MESSAGE = (
|
|
21
|
+
"Your fix was accepted: the unit tests passed. Afterwards the implementation code was changed to fix the "
|
|
22
|
+
"conformance tests{see_below}, and the unit tests now fail again. Files may have changed since you last read "
|
|
23
|
+
"them."
|
|
24
|
+
)
|
|
25
|
+
# Seeding the first turn: the build folder's file list and the files changed for the FRID.
|
|
26
|
+
MAX_FILE_TREE_ENTRIES = 500
|
|
27
|
+
MAX_RELEVANT_FILES_CHARS = 60_000
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FixUnitTests(BaseAction):
|
|
31
|
+
"""Fix failing unit tests with a server-side agent session that spans the fix attempts.
|
|
32
|
+
|
|
33
|
+
The first failure starts a session; the agent then drives read/grep/edit/run tool calls
|
|
34
|
+
(executed here) until it calls submit_fix. The state machine re-runs the unit tests and,
|
|
35
|
+
if they still fail, the next execution of this action answers that submit_fix call with
|
|
36
|
+
the new failure output inside the same session, so earlier attempts stay in context.
|
|
37
|
+
|
|
38
|
+
During the conformance phase the session also spans unit-test loops: after the conformance
|
|
39
|
+
tests fixer changes implementation code, the unit tests fail again and the same session is
|
|
40
|
+
continued, told what the conformance tests fixer changed (which it must preserve).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
SUCCESSFUL_OUTCOME = "unit_tests_fix_generated"
|
|
44
|
+
|
|
45
|
+
def execute(self, render_context: RenderContext, previous_action_payload: Any | None):
|
|
46
|
+
if not previous_action_payload or not previous_action_payload.get("previous_unittests_issue"):
|
|
47
|
+
raise InternalClientError(
|
|
48
|
+
"Internal client error: Previous action payload does not contain previous unit tests issue."
|
|
49
|
+
)
|
|
50
|
+
unittests_issue = previous_action_payload["previous_unittests_issue"]
|
|
51
|
+
context = render_context.unit_tests_running_context
|
|
52
|
+
session = render_context.unit_tests_agent_session
|
|
53
|
+
api = render_context.codeplain_api
|
|
54
|
+
frid, module_name = render_context.frid_context.frid, render_context.module_name
|
|
55
|
+
changed_files_before = set(context.changed_files)
|
|
56
|
+
log_path = render_context.script_execution_history.latest_unit_test_output_path
|
|
57
|
+
if log_path:
|
|
58
|
+
agent_tools.register_log_path(log_path, render_context)
|
|
59
|
+
conformance_tests_fixes = self._get_conformance_tests_fixes(render_context)
|
|
60
|
+
new_conformance_tests_fixes = conformance_tests_fixes[session.conformance_fixes_handed_off :]
|
|
61
|
+
if new_conformance_tests_fixes:
|
|
62
|
+
console.info(
|
|
63
|
+
f"Unit tests are fixed while preserving {len(new_conformance_tests_fixes)} implementation code "
|
|
64
|
+
"change(s) made to fix the conformance tests."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
response = None
|
|
68
|
+
if session.session_id is not None:
|
|
69
|
+
if context.agent_used_in_this_loop:
|
|
70
|
+
console.info(f"Continuing agent session {session.session_id} with the new unit tests failure.")
|
|
71
|
+
output = "The fix was applied, but the unit tests still fail."
|
|
72
|
+
else:
|
|
73
|
+
# The session's last fix was accepted in an earlier unit-test loop of this conformance
|
|
74
|
+
# phase; since then the conformance tests fixer changed the code and the tests fail again.
|
|
75
|
+
console.info(
|
|
76
|
+
f"Continuing agent session {session.session_id}: the unit tests fail again after the "
|
|
77
|
+
"implementation was changed to fix the conformance tests."
|
|
78
|
+
)
|
|
79
|
+
output = ACCEPTED_THEN_CHANGED_MESSAGE.format(
|
|
80
|
+
see_below=" (see the Conformance Tests Fix below)" if new_conformance_tests_fixes else ""
|
|
81
|
+
)
|
|
82
|
+
# Files changed outside the session, so earlier read results are stale.
|
|
83
|
+
context.tool_result_cache.clear()
|
|
84
|
+
submit_result: dict = {
|
|
85
|
+
"call_id": session.pending_submit_call_id,
|
|
86
|
+
"output": output + agent_tools.full_log_pointer(log_path),
|
|
87
|
+
"test_output": unittests_issue,
|
|
88
|
+
}
|
|
89
|
+
if new_conformance_tests_fixes:
|
|
90
|
+
submit_result["conformance_tests_fixes"] = new_conformance_tests_fixes
|
|
91
|
+
tool_results = session.pending_tool_results + [submit_result]
|
|
92
|
+
session.pending_tool_results, session.pending_submit_call_id = [], None
|
|
93
|
+
response = self._continue_session(render_context, session.session_id, tool_results)
|
|
94
|
+
if response is None:
|
|
95
|
+
console.info("Starting an agent session to fix the unit tests.")
|
|
96
|
+
# Cached read results point at earlier turns, which a new session does not have.
|
|
97
|
+
context.tool_result_cache.clear()
|
|
98
|
+
response = api.agent_start(
|
|
99
|
+
TASK_TYPE,
|
|
100
|
+
self._build_task_params(render_context, unittests_issue, conformance_tests_fixes),
|
|
101
|
+
frid,
|
|
102
|
+
module_name,
|
|
103
|
+
render_context.run_state,
|
|
104
|
+
)
|
|
105
|
+
session.session_id = response["session_id"]
|
|
106
|
+
assert session.session_id is not None
|
|
107
|
+
session.conformance_fixes_handed_off = len(conformance_tests_fixes)
|
|
108
|
+
context.agent_used_in_this_loop = True
|
|
109
|
+
|
|
110
|
+
submitted = False
|
|
111
|
+
turns = 0
|
|
112
|
+
while response.get("status") == "tool_calls" and turns < MAX_AGENT_TURNS_PER_ATTEMPT:
|
|
113
|
+
turns += 1
|
|
114
|
+
calls = response["calls"]
|
|
115
|
+
submit_call = next((call for call in calls if call["name"] == SUBMIT_FIX_TOOL), None)
|
|
116
|
+
tool_results = agent_tools.execute_calls(
|
|
117
|
+
[call for call in calls if call is not submit_call], render_context
|
|
118
|
+
)
|
|
119
|
+
if submit_call is not None:
|
|
120
|
+
session.pending_submit_call_id = submit_call["id"]
|
|
121
|
+
session.pending_tool_results = tool_results
|
|
122
|
+
submitted = True
|
|
123
|
+
console.info(f"Agent submitted a fix: {submit_call['args'].get('changes_made', '')}")
|
|
124
|
+
break
|
|
125
|
+
response = api.agent_continue(session.session_id, tool_results, frid, module_name, render_context.run_state)
|
|
126
|
+
|
|
127
|
+
if not submitted:
|
|
128
|
+
# The session ended without a submission (finished in text, failed, or used up this
|
|
129
|
+
# attempt's turn budget) — a fresh session is started if the tests still fail.
|
|
130
|
+
status = response.get("status")
|
|
131
|
+
if status == "failed":
|
|
132
|
+
console.warning(f"Agent session failed: {response.get('error', 'unknown error')}")
|
|
133
|
+
elif status == "tool_calls":
|
|
134
|
+
console.warning(f"Agent used {MAX_AGENT_TURNS_PER_ATTEMPT} turns without submitting a fix.")
|
|
135
|
+
session.reset()
|
|
136
|
+
|
|
137
|
+
console.print_files(
|
|
138
|
+
"Files changed while fixing unit tests:",
|
|
139
|
+
render_context.build_folder,
|
|
140
|
+
{path: "" for path in sorted(context.changed_files - changed_files_before)},
|
|
141
|
+
style=console.OUTPUT_STYLE,
|
|
142
|
+
)
|
|
143
|
+
return self.SUCCESSFUL_OUTCOME, None
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def _get_conformance_tests_fixes(render_context: RenderContext) -> list[dict]:
|
|
147
|
+
"""Implementation code changes the conformance tests fixer made during this conformance phase, oldest
|
|
148
|
+
first. Empty outside the conformance phase (the implementation and refactoring unit-test loops)."""
|
|
149
|
+
conformance_tests_running_context = getattr(render_context, "conformance_tests_running_context", None)
|
|
150
|
+
if conformance_tests_running_context is None:
|
|
151
|
+
return []
|
|
152
|
+
return list(getattr(conformance_tests_running_context, "implementation_code_fixes", None) or [])
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _continue_session(render_context: RenderContext, session_id: str, tool_results: list[dict]) -> dict | None:
|
|
156
|
+
"""Continue the session; None if the server no longer has it (expired), so a new one is started."""
|
|
157
|
+
try:
|
|
158
|
+
return render_context.codeplain_api.agent_continue(
|
|
159
|
+
session_id,
|
|
160
|
+
tool_results,
|
|
161
|
+
render_context.frid_context.frid,
|
|
162
|
+
render_context.module_name,
|
|
163
|
+
render_context.run_state,
|
|
164
|
+
)
|
|
165
|
+
except requests.exceptions.HTTPError as e:
|
|
166
|
+
if e.response is None or e.response.status_code != HTTPStatus.NOT_FOUND:
|
|
167
|
+
raise
|
|
168
|
+
console.warning(f"Agent session {session_id} has expired; starting a new one.")
|
|
169
|
+
render_context.unit_tests_agent_session.reset()
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _build_task_params(
|
|
174
|
+
render_context: RenderContext, unittests_issue: str, conformance_tests_fixes: list[dict]
|
|
175
|
+
) -> dict:
|
|
176
|
+
frid = render_context.frid_context.frid
|
|
177
|
+
specifications, _ = plain_spec.get_specifications_for_frid(render_context.plain_source_tree, frid)
|
|
178
|
+
context = render_context.unit_tests_running_context
|
|
179
|
+
# The files the conformance tests fixes changed are always seeded, so the agent sees what to preserve.
|
|
180
|
+
conformance_fix_files = {name for fix in conformance_tests_fixes for name in (fix or {}).get("code_diff") or {}}
|
|
181
|
+
task_params = {
|
|
182
|
+
"definitions": "\n".join(specifications.get(plain_spec.DEFINITIONS, [])),
|
|
183
|
+
"non_functional_requirements": "\n".join(specifications.get(plain_spec.NON_FUNCTIONAL_REQUIREMENTS, [])),
|
|
184
|
+
"functional_requirements": FixUnitTests._functional_requirements_section(render_context, specifications),
|
|
185
|
+
"linked_resources": render_context.frid_context.linked_resources,
|
|
186
|
+
"build_folder": render_context.build_folder,
|
|
187
|
+
"module_name": render_context.module_name,
|
|
188
|
+
"unittests_script_content": FixUnitTests._read_script(render_context.unittests_script),
|
|
189
|
+
"unittests_issue": unittests_issue,
|
|
190
|
+
"unittests_log_path": render_context.script_execution_history.latest_unit_test_output_path,
|
|
191
|
+
"file_tree": FixUnitTests._file_tree(render_context.build_folder),
|
|
192
|
+
"relevant_files": FixUnitTests._relevant_files(
|
|
193
|
+
render_context.build_folder,
|
|
194
|
+
render_context.frid_context.changed_files | context.changed_files | conformance_fix_files,
|
|
195
|
+
),
|
|
196
|
+
}
|
|
197
|
+
if conformance_tests_fixes:
|
|
198
|
+
task_params["conformance_tests_fixes"] = conformance_tests_fixes
|
|
199
|
+
session = render_context.unit_tests_agent_session
|
|
200
|
+
if session.previous_session_id:
|
|
201
|
+
task_params["previous_session_id"] = session.previous_session_id
|
|
202
|
+
return task_params
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def _file_tree(build_folder: str) -> str:
|
|
206
|
+
paths = []
|
|
207
|
+
for root, dirs, files in os.walk(build_folder):
|
|
208
|
+
dirs[:] = sorted(d for d in dirs if d not in agent_tools.GREP_EXCLUDED_DIRS and not d.startswith("."))
|
|
209
|
+
paths.extend(os.path.relpath(os.path.join(root, name), build_folder) for name in sorted(files))
|
|
210
|
+
if len(paths) > MAX_FILE_TREE_ENTRIES:
|
|
211
|
+
return "\n".join(paths[:MAX_FILE_TREE_ENTRIES]) + "\n... [more files not listed]"
|
|
212
|
+
return "\n".join(paths)
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def _relevant_files(build_folder: str, file_names: set[str]) -> dict[str, str]:
|
|
216
|
+
"""Contents of the given build-relative files, smallest first, within MAX_RELEVANT_FILES_CHARS."""
|
|
217
|
+
contents = {}
|
|
218
|
+
for name in file_names:
|
|
219
|
+
full_path = os.path.join(build_folder, name)
|
|
220
|
+
if os.path.isfile(full_path):
|
|
221
|
+
with open(full_path, "r", encoding="utf-8", errors="replace") as f:
|
|
222
|
+
contents[name] = f.read()
|
|
223
|
+
relevant, total = {}, 0
|
|
224
|
+
for name in sorted(contents, key=lambda n: (len(contents[n]), n)):
|
|
225
|
+
if total + len(contents[name]) > MAX_RELEVANT_FILES_CHARS:
|
|
226
|
+
break
|
|
227
|
+
relevant[name] = contents[name]
|
|
228
|
+
total += len(contents[name])
|
|
229
|
+
return dict(sorted(relevant.items()))
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def _functional_requirements_section(render_context: RenderContext, specifications: dict) -> str:
|
|
233
|
+
sections = []
|
|
234
|
+
for module_name, functionalities in render_context.get_required_modules_functionalities().items():
|
|
235
|
+
sections.append(
|
|
236
|
+
f"### Module: {module_name} (Already Implemented, for context)\n" + "\n".join(functionalities)
|
|
237
|
+
)
|
|
238
|
+
current = specifications.get(plain_spec.FUNCTIONAL_REQUIREMENTS, [])
|
|
239
|
+
if len(current) > 1:
|
|
240
|
+
sections.append(
|
|
241
|
+
f"### Module: {render_context.module_name} (Already Implemented, for context)\n"
|
|
242
|
+
+ "\n".join(current[:-1])
|
|
243
|
+
)
|
|
244
|
+
if current:
|
|
245
|
+
sections.append(f"### Module: {render_context.module_name} (Currently Being Implemented)\n{current[-1]}")
|
|
246
|
+
return "\n\n".join(sections)
|
|
247
|
+
|
|
248
|
+
@staticmethod
|
|
249
|
+
def _read_script(script: str | None) -> str:
|
|
250
|
+
if not script:
|
|
251
|
+
return ""
|
|
252
|
+
try:
|
|
253
|
+
with open(file_utils.add_current_path_if_no_path(script), "r", encoding="utf-8") as f:
|
|
254
|
+
return f.read()
|
|
255
|
+
except OSError:
|
|
256
|
+
return ""
|
|
@@ -16,6 +16,15 @@ class RunUnitTests(BaseAction):
|
|
|
16
16
|
UNRECOVERABLE_ERROR_OUTCOME = "unrecoverable_error_occurred"
|
|
17
17
|
|
|
18
18
|
def execute(self, render_context: RenderContext, _previous_action_payload: Any | None):
|
|
19
|
+
context = render_context.unit_tests_running_context
|
|
20
|
+
if context.verified_passing:
|
|
21
|
+
# The fixing agent's own run passed and no file changed since; running again is redundant.
|
|
22
|
+
context.verified_passing = False
|
|
23
|
+
console.info("Unit tests already passed in the fixing agent's last run; not running them again.")
|
|
24
|
+
render_context.script_execution_history.latest_unit_test_output_path = context.verified_passing_log_path
|
|
25
|
+
render_context.script_execution_history.should_update_script_outputs = True
|
|
26
|
+
return self.SUCCESSFUL_OUTCOME, None
|
|
27
|
+
|
|
19
28
|
unittests_script = os.path.normpath(render_context.unittests_script)
|
|
20
29
|
|
|
21
30
|
console.info(
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Client-side implementations of the tools a server-side agent can call.
|
|
2
|
+
|
|
3
|
+
The server declares the tools to the LLM (codeplain-api: src/agent/tools.py) and forwards
|
|
4
|
+
the model's calls; this module executes them against the local build folder and returns
|
|
5
|
+
plain-text results. Relative paths resolve against the build folder. Reads are allowed only in the
|
|
6
|
+
build folder and the full test logs the agent was pointed to; writes only inside the build folder.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import glob
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import tempfile
|
|
14
|
+
from typing import Callable
|
|
15
|
+
|
|
16
|
+
from plain2code_console import console
|
|
17
|
+
from render_machine import render_utils
|
|
18
|
+
from render_machine.render_context import RenderContext
|
|
19
|
+
|
|
20
|
+
DEFAULT_READ_LIMIT = 200
|
|
21
|
+
MAX_LINE_CHARS = 10_000
|
|
22
|
+
MAX_OUTPUT_CHARS = 30_000
|
|
23
|
+
EDIT_SNIPPET_CONTEXT_LINES = 3
|
|
24
|
+
MAX_EDIT_SNIPPET_LINES = 60
|
|
25
|
+
MAX_GREP_CONTEXT_LINES = 20
|
|
26
|
+
# Tools without side effects; repeating one while no file changed returns a pointer instead.
|
|
27
|
+
READ_ONLY_TOOLS = ("read_file", "grep", "ls_files")
|
|
28
|
+
GREP_EXCLUDED_DIRS = (".git", "__pycache__", "node_modules", ".venv", "target", "dist", "build")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _build_folder(render_context: RenderContext) -> str:
|
|
32
|
+
return os.path.normpath(os.path.abspath(render_context.build_folder))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _resolve(file_path: str, render_context: RenderContext) -> str:
|
|
36
|
+
if os.path.isabs(file_path):
|
|
37
|
+
return os.path.normpath(file_path)
|
|
38
|
+
return os.path.normpath(os.path.join(_build_folder(render_context), file_path))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _within(path: str, folder: str) -> bool:
|
|
42
|
+
return path == folder or path.startswith(folder + os.sep)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _readable(path: str, render_context: RenderContext) -> bool:
|
|
46
|
+
return (
|
|
47
|
+
_within(path, _build_folder(render_context))
|
|
48
|
+
or path in render_context.unit_tests_agent_session.readable_log_paths
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def register_log_path(log_path: str, render_context: RenderContext) -> None:
|
|
53
|
+
"""Allow read_file/grep on a full test log the agent is pointed to."""
|
|
54
|
+
render_context.unit_tests_agent_session.readable_log_paths.add(os.path.normpath(os.path.abspath(log_path)))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _writable(path: str, render_context: RenderContext) -> bool:
|
|
58
|
+
return _within(path, _build_folder(render_context))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _bound(text: str) -> str:
|
|
62
|
+
"""Cap very long lines and the total size so one tool result cannot flood the context."""
|
|
63
|
+
lines = [
|
|
64
|
+
line if len(line) <= MAX_LINE_CHARS else line[:MAX_LINE_CHARS] + "... [line truncated]"
|
|
65
|
+
for line in text.split("\n")
|
|
66
|
+
]
|
|
67
|
+
text = "\n".join(lines)
|
|
68
|
+
if len(text) > MAX_OUTPUT_CHARS:
|
|
69
|
+
head, tail = int(MAX_OUTPUT_CHARS * 0.6), int(MAX_OUTPUT_CHARS * 0.4)
|
|
70
|
+
text = text[:head] + f"\n\n... [truncated {len(text) - head - tail:,} chars] ...\n\n" + text[-tail:]
|
|
71
|
+
return text
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _track_change(full_path: str, render_context: RenderContext) -> None:
|
|
75
|
+
context = render_context.unit_tests_running_context
|
|
76
|
+
context.changed_files.add(os.path.relpath(full_path, _build_folder(render_context)))
|
|
77
|
+
context.verified_passing, context.verified_passing_log_path = False, None
|
|
78
|
+
context.tool_result_cache.clear()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def read_file(args: dict, render_context: RenderContext) -> str:
|
|
82
|
+
full_path = _resolve(args.get("file_path", ""), render_context)
|
|
83
|
+
if not _readable(full_path, render_context):
|
|
84
|
+
return f"Error: read access denied for '{full_path}' (readable: build folder and test logs)."
|
|
85
|
+
if not os.path.isfile(full_path):
|
|
86
|
+
return f"Error: file not found: '{full_path}'."
|
|
87
|
+
with open(full_path, "r", encoding="utf-8", errors="replace") as f:
|
|
88
|
+
lines = f.read().split("\n")
|
|
89
|
+
offset = max(int(args.get("offset") or 1), 1)
|
|
90
|
+
limit = int(args.get("limit") or DEFAULT_READ_LIMIT)
|
|
91
|
+
selected = lines[offset - 1 : offset - 1 + limit]
|
|
92
|
+
if not selected:
|
|
93
|
+
return f"Error: offset {offset} is past the end of the file ({len(lines)} lines)."
|
|
94
|
+
numbered = "\n".join(f"{offset + i}: {line}" for i, line in enumerate(selected))
|
|
95
|
+
last = offset - 1 + len(selected)
|
|
96
|
+
note = (
|
|
97
|
+
f"\n[showing lines {offset}-{last} of {len(lines)}; use offset={last + 1} to continue]"
|
|
98
|
+
if last < len(lines)
|
|
99
|
+
else ""
|
|
100
|
+
)
|
|
101
|
+
return _bound(numbered) + note
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def grep(args: dict, render_context: RenderContext) -> str:
|
|
105
|
+
pattern = args.get("pattern", "")
|
|
106
|
+
if not pattern:
|
|
107
|
+
return "Error: pattern is required."
|
|
108
|
+
target = _resolve(args.get("file_path") or ".", render_context)
|
|
109
|
+
if not _readable(target, render_context):
|
|
110
|
+
return f"Error: read access denied for '{target}'."
|
|
111
|
+
if not os.path.exists(target):
|
|
112
|
+
return f"Error: path not found: '{target}'."
|
|
113
|
+
# Run from the build folder so matches inside it come back as build-relative paths, which
|
|
114
|
+
# is the form the other tools accept; a test log outside it is passed by absolute path.
|
|
115
|
+
cwd = _build_folder(render_context)
|
|
116
|
+
options = [f"--exclude-dir={d}" for d in GREP_EXCLUDED_DIRS]
|
|
117
|
+
context_lines = min(max(int(args.get("context_lines") or 0), 0), MAX_GREP_CONTEXT_LINES)
|
|
118
|
+
if context_lines:
|
|
119
|
+
options.append(f"-C{context_lines}")
|
|
120
|
+
if args.get("include"):
|
|
121
|
+
options.append(f"--include={args['include']}")
|
|
122
|
+
command = ["grep", "-rnI", *options, "-e", pattern, "--"]
|
|
123
|
+
command.append(os.path.relpath(target, cwd) if _within(target, cwd) else target)
|
|
124
|
+
result = subprocess.run(command, capture_output=True, text=True, cwd=cwd)
|
|
125
|
+
if result.returncode == 1:
|
|
126
|
+
return f"No matches for '{pattern}' in '{target}'."
|
|
127
|
+
if result.returncode != 0:
|
|
128
|
+
return f"Error: grep failed: {result.stderr.strip()}"
|
|
129
|
+
lines = [line[2:] if line.startswith("./") else line for line in result.stdout.rstrip("\n").split("\n")]
|
|
130
|
+
return _bound("\n".join(lines))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def ls_files(args: dict, render_context: RenderContext) -> str:
|
|
134
|
+
target = _resolve(args.get("pattern") or ".", render_context)
|
|
135
|
+
if not _readable(target, render_context):
|
|
136
|
+
return f"Error: read access denied for '{target}'."
|
|
137
|
+
if os.path.isdir(target):
|
|
138
|
+
entries = sorted(os.listdir(target))
|
|
139
|
+
listing = [entry + "/" if os.path.isdir(os.path.join(target, entry)) else entry for entry in entries]
|
|
140
|
+
return f"{target}:\n" + ("\n".join(listing) if listing else "(empty)")
|
|
141
|
+
matches = sorted(glob.glob(target, recursive=True))
|
|
142
|
+
return _bound("\n".join(matches)) if matches else f"No files match '{target}'."
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def edit_file(args: dict, render_context: RenderContext) -> str:
|
|
146
|
+
full_path = _resolve(args.get("file_path", ""), render_context)
|
|
147
|
+
search, replace = args.get("search", ""), args.get("replace", "")
|
|
148
|
+
if not search:
|
|
149
|
+
return "Error: search is required."
|
|
150
|
+
if not _writable(full_path, render_context):
|
|
151
|
+
return f"Error: write access denied for '{full_path}' (writable: build folder only)."
|
|
152
|
+
if not os.path.isfile(full_path):
|
|
153
|
+
return f"Error: file not found: '{full_path}'. Use write_file to create new files."
|
|
154
|
+
with open(full_path, "r", encoding="utf-8") as f:
|
|
155
|
+
content = f.read()
|
|
156
|
+
occurrences = content.count(search)
|
|
157
|
+
if occurrences != 1:
|
|
158
|
+
return (
|
|
159
|
+
f"Error: search text found {occurrences} times in '{full_path}'; it must appear exactly once. "
|
|
160
|
+
"Read the file and use a larger, unique snippet."
|
|
161
|
+
)
|
|
162
|
+
with open(full_path, "w", encoding="utf-8") as f:
|
|
163
|
+
f.write(content.replace(search, replace, 1))
|
|
164
|
+
_track_change(full_path, render_context)
|
|
165
|
+
return f"Edited '{full_path}'. The edited region now reads:\n" + _edit_snippet(content, search, replace)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _edit_snippet(content: str, search: str, replace: str) -> str:
|
|
169
|
+
"""Numbered lines of the replacement plus a little context, so the edit needs no re-read."""
|
|
170
|
+
lines = content.replace(search, replace, 1).split("\n")
|
|
171
|
+
first = content[: content.index(search)].count("\n")
|
|
172
|
+
last = first + replace.count("\n")
|
|
173
|
+
start = max(first - EDIT_SNIPPET_CONTEXT_LINES, 0)
|
|
174
|
+
end = min(last + EDIT_SNIPPET_CONTEXT_LINES + 1, len(lines), start + MAX_EDIT_SNIPPET_LINES)
|
|
175
|
+
snippet = "\n".join(f"{start + i + 1}: {line}" for i, line in enumerate(lines[start:end]))
|
|
176
|
+
return _bound(snippet)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def write_file(args: dict, render_context: RenderContext) -> str:
|
|
180
|
+
full_path = _resolve(args.get("file_path", ""), render_context)
|
|
181
|
+
if not _writable(full_path, render_context):
|
|
182
|
+
return f"Error: write access denied for '{full_path}' (writable: build folder only)."
|
|
183
|
+
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
184
|
+
with open(full_path, "w", encoding="utf-8") as f:
|
|
185
|
+
f.write(args.get("content", ""))
|
|
186
|
+
_track_change(full_path, render_context)
|
|
187
|
+
return f"Wrote '{full_path}'."
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def delete_file(args: dict, render_context: RenderContext) -> str:
|
|
191
|
+
full_path = _resolve(args.get("file_path", ""), render_context)
|
|
192
|
+
if not _writable(full_path, render_context):
|
|
193
|
+
return f"Error: write access denied for '{full_path}' (writable: build folder only)."
|
|
194
|
+
if not os.path.isfile(full_path):
|
|
195
|
+
return f"Error: file not found: '{full_path}'."
|
|
196
|
+
os.remove(full_path)
|
|
197
|
+
_track_change(full_path, render_context)
|
|
198
|
+
return f"Deleted '{full_path}'."
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def full_log_pointer(log_file_path: str | None) -> str:
|
|
202
|
+
return f" Full log: {log_file_path} (search it with grep, passing that path as file_path)." if log_file_path else ""
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def run_unit_tests(_args: dict, render_context: RenderContext) -> dict:
|
|
206
|
+
"""Returns a result dict: a short header in `output` and the raw failure output in
|
|
207
|
+
`test_output`, which the server condenses (summarizing it when it is long)."""
|
|
208
|
+
exit_code, output, log_file_path = render_utils.execute_script(
|
|
209
|
+
os.path.normpath(render_context.unittests_script),
|
|
210
|
+
[render_context.build_folder],
|
|
211
|
+
"Unit Tests",
|
|
212
|
+
timeout=render_context.test_script_timeout,
|
|
213
|
+
stop_event=render_context.stop_event,
|
|
214
|
+
)
|
|
215
|
+
context = render_context.unit_tests_running_context
|
|
216
|
+
if exit_code == 0:
|
|
217
|
+
context.verified_passing, context.verified_passing_log_path = True, log_file_path
|
|
218
|
+
return {"output": "All unit tests passed."}
|
|
219
|
+
if not log_file_path and output:
|
|
220
|
+
with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False, suffix=".unittest_output") as f:
|
|
221
|
+
f.write(output)
|
|
222
|
+
log_file_path = f.name
|
|
223
|
+
if log_file_path:
|
|
224
|
+
register_log_path(log_file_path, render_context)
|
|
225
|
+
return {
|
|
226
|
+
"output": f"Unit tests failed (exit code {exit_code}).{full_log_pointer(log_file_path)}",
|
|
227
|
+
"test_output": output,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
TOOLS: dict[str, Callable[[dict, RenderContext], str | dict]] = {
|
|
232
|
+
"read_file": read_file,
|
|
233
|
+
"grep": grep,
|
|
234
|
+
"ls_files": ls_files,
|
|
235
|
+
"edit_file": edit_file,
|
|
236
|
+
"write_file": write_file,
|
|
237
|
+
"delete_file": delete_file,
|
|
238
|
+
"run_unit_tests": run_unit_tests,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def execute_calls(calls: list[dict], render_context: RenderContext) -> list[dict]:
|
|
243
|
+
"""Execute the agent's tool calls in order; every call gets a result, errors included.
|
|
244
|
+
|
|
245
|
+
A repeated read-only call (same tool and arguments, no file changed in between) is not
|
|
246
|
+
re-executed: its result is already in the conversation, so a short pointer is returned."""
|
|
247
|
+
cache = render_context.unit_tests_running_context.tool_result_cache
|
|
248
|
+
results = []
|
|
249
|
+
for call in calls:
|
|
250
|
+
tool = TOOLS.get(call["name"])
|
|
251
|
+
cache_key = json.dumps([call["name"], call.get("args") or {}], sort_keys=True)
|
|
252
|
+
result: dict
|
|
253
|
+
if tool is None:
|
|
254
|
+
result = {"output": f"Error: unknown tool '{call['name']}'."}
|
|
255
|
+
elif call["name"] in READ_ONLY_TOOLS and cache_key in cache:
|
|
256
|
+
result = {
|
|
257
|
+
"output": "Same call as an earlier one and no file has changed since; "
|
|
258
|
+
"its result is unchanged (see the earlier result above)."
|
|
259
|
+
}
|
|
260
|
+
else:
|
|
261
|
+
try:
|
|
262
|
+
output = tool(call.get("args") or {}, render_context)
|
|
263
|
+
result = output if isinstance(output, dict) else {"output": output}
|
|
264
|
+
except Exception as e:
|
|
265
|
+
result = {"output": f"Error: tool '{call['name']}' failed: {type(e).__name__}: {e}"}
|
|
266
|
+
if call["name"] in READ_ONLY_TOOLS and not result["output"].startswith("Error"):
|
|
267
|
+
cache[cache_key] = result["output"]
|
|
268
|
+
console.debug(f"Agent tool {call['name']}({call.get('args')}) -> {result['output'][:200]!r}")
|
|
269
|
+
results.append({"call_id": call["id"], **result})
|
|
270
|
+
return results
|
|
@@ -19,6 +19,7 @@ from render_machine.render_types import (
|
|
|
19
19
|
FridContext,
|
|
20
20
|
ScriptExecutionHistory,
|
|
21
21
|
TestExecutionPhase,
|
|
22
|
+
UnitTestsAgentSession,
|
|
22
23
|
UnitTestsRunningContext,
|
|
23
24
|
)
|
|
24
25
|
|
|
@@ -188,6 +189,14 @@ class RenderContext:
|
|
|
188
189
|
def should_run_conformance_tests(self) -> bool:
|
|
189
190
|
return self.conformance_tests_script is not None
|
|
190
191
|
|
|
192
|
+
@property
|
|
193
|
+
def unit_tests_agent_session(self) -> UnitTestsAgentSession:
|
|
194
|
+
"""The agent session fixing the unit tests: per unit-test loop, except during the conformance
|
|
195
|
+
phase, where one session spans every loop (see UnitTestsAgentSession)."""
|
|
196
|
+
if self.conformance_tests_running_context is not None:
|
|
197
|
+
return self.conformance_tests_running_context.unit_tests_agent_session
|
|
198
|
+
return self.unit_tests_running_context.agent_session
|
|
199
|
+
|
|
191
200
|
def start_unittests_processing(self):
|
|
192
201
|
self.unit_tests_running_context = UnitTestsRunningContext(fix_attempts=0)
|
|
193
202
|
self.run_state.increment_unittest_batch_id()
|